refactor: remove superseded dead code (#5916)

This commit is contained in:
SaladDay
2026-07-30 10:32:10 -04:00
committed by GitHub
parent c0ff89b9b2
commit 3c1154bed9
34 changed files with 8 additions and 3166 deletions
-79
View File
@@ -1,79 +0,0 @@
import React from "react";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils";
import { useTranslation } from "react-i18next";
interface ColorPickerProps {
value?: string;
onValueChange: (color: string) => void;
label?: string;
presets?: string[];
}
const DEFAULT_PRESETS = [
"#00A67E",
"#D4915D",
"#4285F4",
"#FF6A00",
"#00A4FF",
"#FF9900",
"#0078D4",
"#FF0000",
"#1E88E5",
"#6366F1",
"#0F62FE",
"#2932E1",
];
export const ColorPicker: React.FC<ColorPickerProps> = ({
value = "#4285F4",
onValueChange,
label,
presets = DEFAULT_PRESETS,
}) => {
const { t } = useTranslation();
const displayLabel = label ?? t("providerIcon.color", "图标颜色");
return (
<div className="space-y-3">
<Label>{displayLabel}</Label>
{/* 颜色预设 */}
<div className="grid grid-cols-6 gap-2">
{presets.map((color) => (
<button
key={color}
type="button"
onClick={() => onValueChange(color)}
className={cn(
"w-full aspect-square rounded-lg border-2 transition-all",
"hover:scale-110 hover:shadow-lg",
value === color
? "border-primary ring-2 ring-primary/20"
: "border-border",
)}
style={{ backgroundColor: color }}
title={color}
/>
))}
</div>
{/* 自定义颜色输入 */}
<div className="flex items-center gap-2">
<Input
type="color"
value={value}
onChange={(e) => onValueChange(e.target.value)}
className="w-16 h-10 p-1 cursor-pointer"
/>
<Input
type="text"
value={value}
onChange={(e) => onValueChange(e.target.value)}
placeholder="#4285F4"
className="flex-1 font-mono"
/>
</div>
</div>
);
};
-190
View File
@@ -1,190 +0,0 @@
import { SVGProps } from "react";
export function ITermIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<title>iTerm2</title>
<path d="M24 5.359v13.282A5.36 5.36 0 0 1 18.641 24H5.359A5.36 5.36 0 0 1 0 18.641V5.359A5.36 5.36 0 0 1 5.359 0h13.282A5.36 5.36 0 0 1 24 5.359m-.932-.233A4.196 4.196 0 0 0 18.874.932H5.126A4.196 4.196 0 0 0 .932 5.126v13.748a4.196 4.196 0 0 0 4.194 4.194h13.748a4.196 4.196 0 0 0 4.194-4.194zm-.816.233v13.282a3.613 3.613 0 0 1-3.611 3.611H5.359a3.613 3.613 0 0 1-3.611-3.611V5.359a3.613 3.613 0 0 1 3.611-3.611h13.282a3.613 3.613 0 0 1 3.611 3.611M8.854 4.194v6.495h.962V4.194zM5.483 9.493v1.085h.597V9.48q.283-.037.508-.133.373-.165.575-.448.208-.284.208-.649a.9.9 0 0 0-.171-.568 1.4 1.4 0 0 0-.426-.388 3 3 0 0 0-.544-.261 32 32 0 0 0-.545-.209 1.8 1.8 0 0 1-.426-.216q-.164-.12-.164-.284 0-.223.179-.351.18-.126.485-.127.344 0 .575.105.239.105.5.298l.433-.5a2.3 2.3 0 0 0-.605-.433 1.6 1.6 0 0 0-.582-.159v-.968h-.597v.978a2 2 0 0 0-.477.127 1.2 1.2 0 0 0-.545.411q-.194.268-.194.634 0 .335.164.56.164.224.418.38a4 4 0 0 0 .552.262q.291.104.545.209.261.104.425.238a.39.39 0 0 1 .165.321q0 .225-.187.359-.18.134-.537.134-.381 0-.717-.134a4.4 4.4 0 0 1-.649-.351l-.388.589q.209.173.477.306.276.135.575.217.191.046.373.064" />
</svg>
);
}
export function AlacrittyIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<title>Alacritty</title>
<path d="m10.065 0-8.57 21.269h3.595l6.91-16.244 6.91 16.244h3.594l-8.57-21.269zm1.935 9.935c-0.76666 1.8547-1.5334 3.7094-2.298 5.565 1.475 4.54 1.475 4.54 2.298 8.5 0.823-3.96 0.823-3.96 2.297-8.5-0.76637-1.8547-1.5315-3.7099-2.297-5.565z" />
</svg>
);
}
export function WezTermIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<title>WezTerm</title>
<path d="M3.27 8.524c0-.623.62-1.007 2.123-1.007l-.5 2.757c-.931-.623-1.624-1.199-1.624-1.75zm4.008 6.807c0 .647-.644 1.079-2.123 1.15l.524-2.924c.931.624 1.6 1.175 1.6 1.774zm-2.625 5.992.454-2.708c3.603-.336 5.01-1.798 5.01-3.404 0-1.653-2.004-2.948-3.841-4.074l.668-3.548c.764.072 1.67.216 2.744.432l.31-2.469c-.81-.12-1.575-.168-2.29-.216L8.257 2.7l-2.363-.024-.453 2.684C1.838 5.648.43 7.158.43 8.764c0 1.63 2.004 2.876 3.841 3.954l-.668 3.716c-.859-.048-1.908-.192-3.125-.408L0 18.495c1.026.12 1.98.192 2.84.216l-.525 2.588zm15.553-1.894h2.673c.334-2.804.81-8.46 1.121-14.86h-2.553c-.071 1.51-.334 10.498-.43 11.241h-.071c-.644-2.42-1.169-4.386-1.813-6.782h-1.456c-.62 2.396-1.05 4.194-1.694 6.782h-.096c-.071-.743-.477-9.73-.525-11.24h-2.648c.31 6.399.763 12.055 1.097 14.86h2.625l1.838-7.12z" />
</svg>
);
}
export function GhosttyIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<title>Ghostty</title>
<path d="M12 0C6.7 0 2.4 4.3 2.4 9.6v11.146c0 1.772 1.45 3.267 3.222 3.254a3.18 3.18 0 0 0 1.955-.686 1.96 1.96 0 0 1 2.444 0 3.18 3.18 0 0 0 1.976.686c.75 0 1.436-.257 1.98-.686.715-.563 1.71-.587 2.419-.018.59.476 1.355.743 2.182.699 1.705-.094 3.022-1.537 3.022-3.244V9.601C21.6 4.3 17.302 0 12 0M6.069 6.562a1 1 0 0 1 .46.131l3.578 2.065v.002a.974.974 0 0 1 0 1.687L6.53 12.512a.975.975 0 0 1-.976-1.687L7.67 9.602 5.553 8.38a.975.975 0 0 1 .515-1.818m7.438 2.063h4.7a.975.975 0 1 1 0 1.95h-4.7a.975.975 0 0 1 0-1.95" />
</svg>
);
}
export function KittyIcon(props: SVGProps<SVGSVGElement>) {
// Official icon is complex and has fixed width/height/viewBox in original.
// Simplifying viewBox to 0 0 256 256 effectively as original was 240x240 but translated.
// Original viewBox="0 0 240 240" with g transform="translate(0 -812.362)" and elements around y=850.
// 850 - 812 = 38. So it's confusing.
// Let's copy the raw SVG content but adapt it to be a component.
// To make it behave like an icon, we should probably set viewBox="0 0 240 240" and keep the transform.
// It relies on fill colors. If we want it to be monochrome (currentColor), we should remove fills or set them to currentColor.
// However, official icons often have brand colors. The user said "official icon", which implies color.
// But usually in a dropdown we might want monochrome or original color.
// simple-icons are usually monochrome.
// Let's keep Kitty as original color since it's complex, OR mono if it works?
// The kitty icon has multiple paths with different colors. I'll preserve them for now as it's "official".
// If it looks weird in dark mode/light mode, we might need to adjust.
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 240 240"
{...props} // Allow overriding width/height
>
<g transform="translate(0 -812.362)">
<rect
width="100.446"
height="161.551"
x="72.824"
y="850.13"
ry="0"
style={{
fill: "#ddd",
fillOpacity: 1,
fillRule: "evenodd",
stroke: "none",
strokeWidth: 5.86876726,
strokeLinecap: "round",
strokeLinejoin: "round",
strokeMiterlimit: 4,
strokeDasharray: "none",
strokeOpacity: 1,
}}
/>
<path
d="M67.896 1029.71h104.208a7.065 7.065 0 0 0 7.065-7.066V918.436a7.065 7.065 0 0 0-7.065-7.065H67.896a7.065 7.065 0 0 0-7.065 7.065v104.208a7.065 7.065 0 0 0 7.065 7.065m55.813-38.35h37.444a4.239 4.239 0 0 1 0 8.479H123.71a4.239 4.239 0 0 1 0-8.478m-45.032-45.71a4.239 4.239 0 0 1 5.991-5.99l26.48 26.464a4.24 4.24 0 0 1 0 5.992l-26.48 26.48a4.239 4.239 0 0 1-5.991-5.992l23.484-23.484z"
style={{ strokeWidth: 1.41299629 }}
/>
<path
d="M96.085 898.143c1.881 0 3.386-3.574 3.386-8.17 0-4.595-1.505-8.169-3.386-8.169-1.88 0-3.385 3.574-3.385 8.17 0 4.595 1.504 8.17 3.385 8.17"
style={{
clipRule: "evenodd",
fill: "#c0c81f",
fillOpacity: 1,
fillRule: "evenodd",
strokeWidth: 3.09913683,
}}
/>
<path
d="M193.128 836.886c-4.596-4.85-25.53 1.022-38.295 8.936-9.957-5.106-21.956-8.17-34.721-8.17-13.02 0-25.02 3.064-34.977 8.17-12.765-7.914-33.955-14.042-38.295-8.936-4.595 5.106 3.32 26.296 12.765 38.04-.766 3.064-1.276 6.128-1.276 9.446 0 10.212 4.34 19.659 11.744 27.318h42.124c-1.276-2.553.511-4.085 8.17-4.085 7.659.255 9.19 1.532 8.17 4.085h42.124c7.404-7.66 11.744-17.36 11.744-27.318 0-3.318-.51-6.382-1.276-9.446 8.935-11.744 16.594-33.189 11.999-38.04m-97.015 67.4c-8.935 0-16.339-7.404-16.339-16.34s7.404-16.339 16.34-16.339 16.339 7.404 16.339 16.34-7.404 16.339-16.34 16.339m47.997 0c-8.936 0-16.34-7.404-16.34-16.34s7.404-16.339 16.34-16.339 16.34 7.404 16.34 16.34-7.15 16.339-16.34 16.339"
style={{
clipRule: "evenodd",
fill: "#784421",
fillOpacity: 1,
fillRule: "evenodd",
strokeWidth: 2.55301046,
}}
/>
<g style={{ fill: "#2b1100", fillOpacity: 1 }}>
<path
d="M168.507 903.265c15.318-19.148 46.72-28.339 67.655-15.063-24.509-3.83-46.72 2.553-67.655 15.063"
style={{
clipRule: "evenodd",
fillRule: "evenodd",
strokeWidth: 2.55301046,
fill: "#2b1100",
fillOpacity: 1,
}}
/>
<path
d="M167.486 898.67c8.68-20.425 34.466-33.7 55.145-26.552-21.7 2.808-39.316 11.233-55.145 26.551m-.256 9.957c15.83-15.063 50.806-20.169 61.528-4.34-21.7-6.893-40.593-3.83-61.527 4.34"
style={{
clipRule: "evenodd",
fillRule: "evenodd",
strokeWidth: 2.55301046,
fill: "#2b1100",
fillOpacity: 1,
}}
/>
</g>
<g style={{ fill: "#2b1100", fillOpacity: 1 }}>
<path
d="M71.493 903.265c-15.318-19.148-46.72-28.339-67.655-15.063 24.509-3.83 46.72 2.553 67.655 15.063"
style={{
clipRule: "evenodd",
fillRule: "evenodd",
strokeWidth: 2.55301046,
fill: "#2b1100",
fillOpacity: 1,
}}
/>
<path
d="M72.514 898.67c-8.68-20.425-34.466-33.7-55.145-26.552 21.7 2.808 39.316 11.233 55.145 26.551m.256 9.957c-15.83-15.063-50.806-20.169-61.528-4.34 21.7-6.893 40.593-3.83 61.527 4.34"
style={{
clipRule: "evenodd",
fillRule: "evenodd",
strokeWidth: 2.55301046,
fill: "#2b1100",
fillOpacity: 1,
}}
/>
</g>
<path
d="M52.6 893.563c-6.382 0-11.743 3.32-14.296 8.425h-.766c-6.893 0-12.765 5.106-12.765 11.489 0 8.935 9.19 13.786 17.615 10.722 5.106 7.404 16.084 7.915 20.17 0 6.126-.255 16.083-1.276 17.615-10.722 1.021-6.383-5.617-11.489-12.765-11.489h-.766c-2.042-5.106-7.659-8.425-14.041-8.425m134.8 0c6.382 0 11.743 3.32 14.296 8.425h.766c3.574 0 12.765 5.106 12.765 11.489 0 8.935-9.19 13.786-17.615 10.722-5.107 7.404-16.084 7.915-20.17 0-6.126-.255-16.083-1.276-17.615-10.722-1.021-6.383 9.19-11.489 12.765-11.489h.766c2.042-5.106 7.659-8.425 14.041-8.425"
style={{
clipRule: "evenodd",
fill: "#483737",
fillOpacity: 1,
fillRule: "evenodd",
strokeWidth: 2.55301046,
}}
/>
<path
d="M143.542 898.143c1.881 0 3.386-3.574 3.386-8.17 0-4.595-1.505-8.169-3.386-8.169-1.88 0-3.386 3.574-3.386 8.17 0 4.595 1.505 8.17 3.386 8.17"
style={{
clipRule: "evenodd",
fill: "#c0c81f",
fillOpacity: 1,
fillRule: "evenodd",
strokeWidth: 3.09913683,
}}
/>
</g>
</svg>
);
}
-25
View File
@@ -1,25 +0,0 @@
import { Moon, Sun } from "lucide-react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import { useTheme } from "@/components/theme-provider";
export function ModeToggle() {
const { theme, setTheme } = useTheme();
const { t } = useTranslation();
const toggleTheme = () => {
if (theme === "dark") {
setTheme("light");
} else {
setTheme("dark");
}
};
return (
<Button variant="outline" size="icon" onClick={toggleTheme}>
<Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">{t("common.toggleTheme")}</span>
</Button>
);
}
-164
View File
@@ -1,164 +0,0 @@
import React, { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import MarkdownEditor from "@/components/MarkdownEditor";
import type { Prompt, AppId } from "@/lib/api";
interface PromptFormModalProps {
appId: AppId;
editingId?: string;
initialData?: Prompt;
onSave: (id: string, prompt: Prompt) => Promise<void>;
onClose: () => void;
}
const PromptFormModal: React.FC<PromptFormModalProps> = ({
appId,
editingId,
initialData,
onSave,
onClose,
}) => {
const { t } = useTranslation();
const appName = t(`apps.${appId}`);
const filenameMap: Record<Exclude<AppId, "openclaw">, string> = {
claude: "CLAUDE.md",
"claude-desktop": "CLAUDE.md",
codex: "AGENTS.md",
gemini: "GEMINI.md",
grokbuild: "AGENTS.md",
opencode: "AGENTS.md",
hermes: "AGENTS.md",
};
const filename = filenameMap[appId as Exclude<AppId, "openclaw">];
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [content, setContent] = useState("");
const [saving, setSaving] = useState(false);
const [isDarkMode, setIsDarkMode] = useState(false);
useEffect(() => {
// 检测初始暗色模式状态
setIsDarkMode(document.documentElement.classList.contains("dark"));
// 监听 html 元素的 class 变化以实时响应主题切换
const observer = new MutationObserver(() => {
setIsDarkMode(document.documentElement.classList.contains("dark"));
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class"],
});
return () => observer.disconnect();
}, []);
useEffect(() => {
if (initialData) {
setName(initialData.name);
setDescription(initialData.description || "");
setContent(initialData.content);
}
}, [initialData]);
const handleSave = async () => {
if (!name.trim()) {
return;
}
setSaving(true);
try {
const id = editingId || `prompt-${Date.now()}`;
const timestamp = Math.floor(Date.now() / 1000);
const prompt: Prompt = {
id,
name: name.trim(),
description: description.trim() || undefined,
content: content.trim(),
enabled: initialData?.enabled || false,
createdAt: initialData?.createdAt || timestamp,
updatedAt: timestamp,
};
await onSave(id, prompt);
onClose();
} catch (error) {
// Error handled by hook
} finally {
setSaving(false);
}
};
return (
<Dialog open onOpenChange={onClose}>
<DialogContent className="max-w-2xl max-h-[85vh] flex flex-col">
<DialogHeader>
<DialogTitle>
{editingId
? t("prompts.editTitle", { appName })
: t("prompts.addTitle", { appName })}
</DialogTitle>
</DialogHeader>
<div className="flex-1 overflow-y-auto space-y-4 px-6 py-4">
<div>
<Label htmlFor="name">{t("prompts.name")}</Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder={t("prompts.namePlaceholder")}
/>
</div>
<div>
<Label htmlFor="description">{t("prompts.description")}</Label>
<Input
id="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder={t("prompts.descriptionPlaceholder")}
/>
</div>
<div>
<Label htmlFor="content" className="mb-2 block">
{t("prompts.content")}
</Label>
<MarkdownEditor
value={content}
onChange={setContent}
placeholder={t("prompts.contentPlaceholder", { filename })}
darkMode={isDarkMode}
minHeight="300px"
/>
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={onClose}>
{t("common.cancel")}
</Button>
<Button
type="button"
onClick={handleSave}
disabled={!name.trim() || saving}
>
{saving ? t("common.saving") : t("common.save")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export default PromptFormModal;
@@ -1,51 +0,0 @@
import React from "react";
import { cn } from "@/lib/utils";
import type { HealthStatus } from "@/lib/api/connectivity-check";
import { useTranslation } from "react-i18next";
interface HealthStatusIndicatorProps {
status: HealthStatus;
responseTimeMs?: number;
className?: string;
}
const statusConfig = {
operational: {
color: "bg-emerald-500",
labelKey: "health.operational",
labelFallback: "正常",
textColor: "text-emerald-600 dark:text-emerald-400",
},
degraded: {
color: "bg-yellow-500",
labelKey: "health.degraded",
labelFallback: "降级",
textColor: "text-yellow-600 dark:text-yellow-400",
},
failed: {
color: "bg-red-500",
labelKey: "health.failed",
labelFallback: "失败",
textColor: "text-red-600 dark:text-red-400",
},
};
export const HealthStatusIndicator: React.FC<HealthStatusIndicatorProps> = ({
status,
responseTimeMs,
className,
}) => {
const { t } = useTranslation();
const config = statusConfig[status];
const label = t(config.labelKey, { defaultValue: config.labelFallback });
return (
<div className={cn("flex items-center gap-2", className)}>
<div className={cn("w-2 h-2 rounded-full", config.color)} />
<span className={cn("text-xs font-medium", config.textColor)}>
{label}
{responseTimeMs !== undefined && ` (${responseTimeMs}ms)`}
</span>
</div>
);
};
@@ -4,7 +4,6 @@ export { useBaseUrlState } from "./useBaseUrlState";
export { useModelState } from "./useModelState";
export { useCodexConfigState } from "./useCodexConfigState";
export { useApiKeyLink } from "./useApiKeyLink";
export { useCustomEndpoints } from "./useCustomEndpoints";
export { useTemplateValues } from "./useTemplateValues";
export { useCommonConfigSnippet } from "./useCommonConfigSnippet";
export { useCodexCommonConfig } from "./useCodexCommonConfig";
@@ -1,92 +0,0 @@
import { useMemo } from "react";
import type { AppId } from "@/lib/api";
import type { CustomEndpoint } from "@/types";
import type { ProviderPreset } from "@/config/claudeProviderPresets";
import type { CodexProviderPreset } from "@/config/codexProviderPresets";
type PresetEntry = {
id: string;
preset: ProviderPreset | CodexProviderPreset;
};
interface UseCustomEndpointsProps {
appId: AppId;
selectedPresetId: string | null;
presetEntries: PresetEntry[];
draftCustomEndpoints: string[];
baseUrl: string;
codexBaseUrl: string;
}
/**
* 收集和管理自定义端点
*
* 收集来源:
* 1. 用户在测速弹窗中新增的自定义端点
* 2. 预设中的 endpointCandidates
* 3. 当前选中的 Base URL
*/
export function useCustomEndpoints({
appId,
selectedPresetId,
presetEntries,
draftCustomEndpoints,
baseUrl,
codexBaseUrl,
}: UseCustomEndpointsProps) {
const customEndpointsMap = useMemo(() => {
const urlSet = new Set<string>();
// 辅助函数:标准化并添加 URL
const push = (raw?: string) => {
const url = (raw || "").trim().replace(/\/+$/, "");
if (url) urlSet.add(url);
};
// 1. 自定义端点(来自用户新增)
for (const u of draftCustomEndpoints) push(u);
// 2. 预设端点候选
if (selectedPresetId && selectedPresetId !== "custom") {
const entry = presetEntries.find((item) => item.id === selectedPresetId);
if (entry) {
const preset = entry.preset as any;
if (Array.isArray(preset?.endpointCandidates)) {
for (const u of preset.endpointCandidates as string[]) push(u);
}
}
}
// 3. 当前 Base URL
if (appId === "codex") {
push(codexBaseUrl);
} else {
push(baseUrl);
}
// 构建 CustomEndpoint map
const urls = Array.from(urlSet.values());
if (urls.length === 0) {
return null;
}
const now = Date.now();
const customMap: Record<string, CustomEndpoint> = {};
for (const url of urls) {
if (!customMap[url]) {
customMap[url] = { url, addedAt: now, lastUsed: undefined };
}
}
return customMap;
}, [
appId,
selectedPresetId,
presetEntries,
draftCustomEndpoints,
baseUrl,
codexBaseUrl,
]);
return customEndpointsMap;
}
@@ -1,356 +0,0 @@
import {
useCircuitBreakerConfig,
useUpdateCircuitBreakerConfig,
} from "@/lib/query/failover";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { useState, useEffect } from "react";
import { toast } from "sonner";
import { useTranslation } from "react-i18next";
/**
* 熔断器配置面板
* 允许用户调整熔断器参数
*/
export function CircuitBreakerConfigPanel() {
const { t } = useTranslation();
const { data: config, isLoading } = useCircuitBreakerConfig();
const updateConfig = useUpdateCircuitBreakerConfig();
// 使用字符串状态以支持完全清空输入框
const [formData, setFormData] = useState({
failureThreshold: "5",
successThreshold: "2",
timeoutSeconds: "60",
errorRateThreshold: "50", // 存储百分比值
minRequests: "10",
});
// 当配置加载完成时更新表单数据
useEffect(() => {
if (config) {
setFormData({
failureThreshold: String(config.failureThreshold),
successThreshold: String(config.successThreshold),
timeoutSeconds: String(config.timeoutSeconds),
errorRateThreshold: String(Math.round(config.errorRateThreshold * 100)),
minRequests: String(config.minRequests),
});
}
}, [config]);
const handleSave = async () => {
// 解析数字,返回 NaN 表示无效输入
const parseNum = (val: string) => {
const trimmed = val.trim();
// 必须是纯数字
if (!/^-?\d+$/.test(trimmed)) return NaN;
return parseInt(trimmed);
};
// 定义各字段的有效范围
const ranges = {
failureThreshold: { min: 1, max: 20 },
successThreshold: { min: 1, max: 10 },
timeoutSeconds: { min: 0, max: 300 },
errorRateThreshold: { min: 0, max: 100 },
minRequests: { min: 5, max: 100 },
};
// 解析原始值
const raw = {
failureThreshold: parseNum(formData.failureThreshold),
successThreshold: parseNum(formData.successThreshold),
timeoutSeconds: parseNum(formData.timeoutSeconds),
errorRateThreshold: parseNum(formData.errorRateThreshold),
minRequests: parseNum(formData.minRequests),
};
// 校验是否超出范围(NaN 也视为无效)
const errors: string[] = [];
const checkRange = (
value: number,
range: { min: number; max: number },
label: string,
) => {
if (isNaN(value) || value < range.min || value > range.max) {
errors.push(`${label}: ${range.min}-${range.max}`);
}
};
checkRange(
raw.failureThreshold,
ranges.failureThreshold,
t("circuitBreaker.failureThreshold", "失败阈值"),
);
checkRange(
raw.successThreshold,
ranges.successThreshold,
t("circuitBreaker.successThreshold", "成功阈值"),
);
checkRange(
raw.timeoutSeconds,
ranges.timeoutSeconds,
t("circuitBreaker.timeoutSeconds", "超时时间"),
);
checkRange(
raw.errorRateThreshold,
ranges.errorRateThreshold,
t("circuitBreaker.errorRateThreshold", "错误率阈值"),
);
checkRange(
raw.minRequests,
ranges.minRequests,
t("circuitBreaker.minRequests", "最小请求数"),
);
if (errors.length > 0) {
toast.error(
t("circuitBreaker.validationFailed", {
fields: errors.join("; "),
defaultValue: `以下字段超出有效范围: ${errors.join("; ")}`,
}),
);
return;
}
try {
await updateConfig.mutateAsync({
failureThreshold: raw.failureThreshold,
successThreshold: raw.successThreshold,
timeoutSeconds: raw.timeoutSeconds,
errorRateThreshold: raw.errorRateThreshold / 100,
minRequests: raw.minRequests,
});
toast.success(t("circuitBreaker.configSaved", "熔断器配置已保存"), {
closeButton: true,
});
} catch (error) {
toast.error(
t("circuitBreaker.saveFailed", "保存失败") + ": " + String(error),
);
}
};
const handleReset = () => {
if (config) {
setFormData({
failureThreshold: String(config.failureThreshold),
successThreshold: String(config.successThreshold),
timeoutSeconds: String(config.timeoutSeconds),
errorRateThreshold: String(Math.round(config.errorRateThreshold * 100)),
minRequests: String(config.minRequests),
});
}
};
if (isLoading) {
return (
<div className="text-sm text-muted-foreground">
{t("circuitBreaker.loading", "加载中...")}
</div>
);
}
return (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold">
{t("circuitBreaker.title", "熔断器配置")}
</h3>
<p className="text-sm text-muted-foreground mt-1">
{t(
"circuitBreaker.description",
"调整熔断器参数以控制故障检测和恢复行为",
)}
</p>
</div>
<div className="h-px bg-border my-4" />
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* 失败阈值 */}
<div className="space-y-2">
<Label htmlFor="failureThreshold">
{t("circuitBreaker.failureThreshold", "失败阈值")}
</Label>
<Input
id="failureThreshold"
type="number"
min="1"
max="20"
value={formData.failureThreshold}
onChange={(e) =>
setFormData({ ...formData, failureThreshold: e.target.value })
}
/>
<p className="text-xs text-muted-foreground">
{t(
"circuitBreaker.failureThresholdHint",
"连续失败多少次后打开熔断器",
)}
</p>
</div>
{/* 超时时间 */}
<div className="space-y-2">
<Label htmlFor="timeoutSeconds">
{t("circuitBreaker.timeoutSeconds", "超时时间(秒)")}
</Label>
<Input
id="timeoutSeconds"
type="number"
min="0"
max="300"
value={formData.timeoutSeconds}
onChange={(e) =>
setFormData({ ...formData, timeoutSeconds: e.target.value })
}
/>
<p className="text-xs text-muted-foreground">
{t(
"circuitBreaker.timeoutSecondsHint",
"熔断器打开后多久尝试恢复(半开状态)",
)}
</p>
</div>
{/* 成功阈值 */}
<div className="space-y-2">
<Label htmlFor="successThreshold">
{t("circuitBreaker.successThreshold", "成功阈值")}
</Label>
<Input
id="successThreshold"
type="number"
min="1"
max="10"
value={formData.successThreshold}
onChange={(e) =>
setFormData({ ...formData, successThreshold: e.target.value })
}
/>
<p className="text-xs text-muted-foreground">
{t(
"circuitBreaker.successThresholdHint",
"半开状态下成功多少次后关闭熔断器",
)}
</p>
</div>
{/* 错误率阈值 */}
<div className="space-y-2">
<Label htmlFor="errorRateThreshold">
{t("circuitBreaker.errorRateThreshold", "错误率阈值 (%)")}
</Label>
<Input
id="errorRateThreshold"
type="number"
min="0"
max="100"
step="5"
value={formData.errorRateThreshold}
onChange={(e) =>
setFormData({ ...formData, errorRateThreshold: e.target.value })
}
/>
<p className="text-xs text-muted-foreground">
{t(
"circuitBreaker.errorRateThresholdHint",
"错误率超过此值时打开熔断器",
)}
</p>
</div>
{/* 最小请求数 */}
<div className="space-y-2">
<Label htmlFor="minRequests">
{t("circuitBreaker.minRequests", "最小请求数")}
</Label>
<Input
id="minRequests"
type="number"
min="5"
max="100"
value={formData.minRequests}
onChange={(e) =>
setFormData({ ...formData, minRequests: e.target.value })
}
/>
<p className="text-xs text-muted-foreground">
{t("circuitBreaker.minRequestsHint", "计算错误率前的最小请求数")}
</p>
</div>
</div>
<div className="flex gap-3">
<Button onClick={handleSave} disabled={updateConfig.isPending}>
{updateConfig.isPending
? t("common.saving", "保存中...")
: t("circuitBreaker.saveConfig", "保存配置")}
</Button>
<Button
variant="outline"
onClick={handleReset}
disabled={updateConfig.isPending}
>
{t("common.reset", "重置")}
</Button>
</div>
{/* 说明信息 */}
<div className="p-4 bg-muted/50 rounded-lg space-y-2 text-sm">
<h4 className="font-medium">
{t("circuitBreaker.instructionsTitle", "配置说明")}
</h4>
<ul className="space-y-1 text-muted-foreground">
<li>
{" "}
<strong>{t("circuitBreaker.failureThreshold", "失败阈值")}</strong>
{t(
"circuitBreaker.instructions.failureThreshold",
"连续失败达到此次数时,熔断器打开",
)}
</li>
<li>
<strong>{t("circuitBreaker.timeoutSeconds", "超时时间")}</strong>
{t(
"circuitBreaker.instructions.timeout",
"熔断器打开后,等待此时间后尝试半开",
)}
</li>
<li>
{" "}
<strong>{t("circuitBreaker.successThreshold", "成功阈值")}</strong>
{t(
"circuitBreaker.instructions.successThreshold",
"半开状态下,成功达到此次数时关闭熔断器",
)}
</li>
<li>
{" "}
<strong>
{t("circuitBreaker.errorRateThreshold", "错误率阈值")}
</strong>
{t(
"circuitBreaker.instructions.errorRate",
"错误率超过此值时,熔断器打开",
)}
</li>
<li>
<strong>{t("circuitBreaker.minRequests", "最小请求数")}</strong>
{t(
"circuitBreaker.instructions.minRequests",
"只有请求数达到此值后才计算错误率",
)}
</li>
</ul>
</div>
</div>
);
}
-202
View File
@@ -1,202 +0,0 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Trash2, ExternalLink, Plus } from "lucide-react";
import { settingsApi } from "@/lib/api";
import type { DiscoverableSkill, SkillRepo } from "@/lib/api/skills";
interface RepoManagerProps {
open: boolean;
onOpenChange: (open: boolean) => void;
repos: SkillRepo[];
skills: DiscoverableSkill[];
onAdd: (repo: SkillRepo) => Promise<void>;
onRemove: (owner: string, name: string) => Promise<void>;
}
export function RepoManager({
open: isOpen,
onOpenChange,
repos,
skills,
onAdd,
onRemove,
}: RepoManagerProps) {
const { t } = useTranslation();
const [repoUrl, setRepoUrl] = useState("");
const [branch, setBranch] = useState("");
const [error, setError] = useState("");
const getSkillCount = (repo: SkillRepo) =>
skills.filter(
(skill) =>
skill.repoOwner === repo.owner &&
skill.repoName === repo.name &&
(skill.repoBranch || "main") === (repo.branch || "main"),
).length;
const parseRepoUrl = (
url: string,
): { owner: string; name: string } | null => {
// 支持格式:
// - https://github.com/owner/name
// - owner/name
// - https://github.com/owner/name.git
let cleaned = url.trim();
cleaned = cleaned.replace(/^https?:\/\/github\.com\//, "");
cleaned = cleaned.replace(/\.git$/, "");
const parts = cleaned.split("/");
if (parts.length === 2 && parts[0] && parts[1]) {
return { owner: parts[0], name: parts[1] };
}
return null;
};
const handleAdd = async () => {
setError("");
const parsed = parseRepoUrl(repoUrl);
if (!parsed) {
setError(t("skills.repo.invalidUrl"));
return;
}
try {
await onAdd({
owner: parsed.owner,
name: parsed.name,
branch: branch || "main",
enabled: true,
});
setRepoUrl("");
setBranch("");
} catch (e) {
setError(e instanceof Error ? e.message : t("skills.repo.addFailed"));
}
};
const handleOpenRepo = async (owner: string, name: string) => {
try {
await settingsApi.openExternal(`https://github.com/${owner}/${name}`);
} catch (error) {
console.error("Failed to open URL:", error);
}
};
return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl max-h-[80vh] flex flex-col p-0">
{/* 固定头部 */}
<DialogHeader className="flex-shrink-0 border-b border-border-default px-6 py-4">
<DialogTitle>{t("skills.repo.title")}</DialogTitle>
<DialogDescription>{t("skills.repo.description")}</DialogDescription>
</DialogHeader>
{/* 可滚动内容区域 */}
<div className="flex-1 min-h-0 overflow-y-auto px-6 py-4">
{/* 添加仓库表单 */}
<div className="space-y-5">
<div className="space-y-2">
<Label htmlFor="repo-url">{t("skills.repo.url")}</Label>
<div className="flex flex-col gap-3">
<Input
id="repo-url"
placeholder={t("skills.repo.urlPlaceholder")}
value={repoUrl}
onChange={(e) => setRepoUrl(e.target.value)}
className="flex-1"
/>
<div className="flex flex-col gap-3 sm:flex-row">
<Input
id="branch"
placeholder={t("skills.repo.branchPlaceholder")}
value={branch}
onChange={(e) => setBranch(e.target.value)}
className="flex-1"
/>
<Button
onClick={handleAdd}
className="w-full sm:w-auto sm:px-4"
variant="mcp"
type="button"
>
<Plus className="h-4 w-4 mr-2" />
{t("skills.repo.add")}
</Button>
</div>
</div>
{error && <p className="text-xs text-destructive">{error}</p>}
</div>
{/* 仓库列表 */}
<div className="space-y-3">
<h4 className="text-sm font-medium">{t("skills.repo.list")}</h4>
{repos.length === 0 ? (
<p className="text-sm text-muted-foreground">
{t("skills.repo.empty")}
</p>
) : (
<div className="space-y-3">
{repos.map((repo) => (
<div
key={`${repo.owner}/${repo.name}`}
className="flex items-center justify-between rounded-xl border border-border-default bg-card px-4 py-3"
>
<div>
<div className="text-sm font-medium text-foreground">
{repo.owner}/{repo.name}
</div>
<div className="mt-1 text-xs text-muted-foreground">
{t("skills.repo.branch")}: {repo.branch || "main"}
<span className="ml-3 inline-flex items-center rounded-full border border-border-default px-2 py-0.5 text-[11px]">
{t("skills.repo.skillCount", {
count: getSkillCount(repo),
})}
</span>
</div>
</div>
<div className="flex gap-2">
<Button
variant="ghost"
size="icon"
type="button"
onClick={() => handleOpenRepo(repo.owner, repo.name)}
title={t("common.view", { defaultValue: "查看" })}
>
<ExternalLink className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
type="button"
onClick={() => onRemove(repo.owner, repo.name)}
title={t("common.delete")}
className="hover:text-red-500 hover:bg-red-100 dark:hover:text-red-400 dark:hover:bg-red-500/10"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
</DialogContent>
</Dialog>
);
}
-126
View File
@@ -1,126 +0,0 @@
import { useTranslation } from "react-i18next";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { usageApi } from "@/lib/api/usage";
import { usageKeys } from "@/lib/query/usage";
import { Database, FileText, RefreshCw, Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useState } from "react";
import { toast } from "sonner";
interface DataSourceBarProps {
refreshIntervalMs: number;
}
const DATA_SOURCE_ICONS: Record<string, React.ReactNode> = {
proxy: <Database className="h-3.5 w-3.5" />,
session_log: <FileText className="h-3.5 w-3.5" />,
codex_db: <Database className="h-3.5 w-3.5" />,
codex_session: <FileText className="h-3.5 w-3.5" />,
gemini_session: <FileText className="h-3.5 w-3.5" />,
opencode_session: <FileText className="h-3.5 w-3.5" />,
grok_session: <FileText className="h-3.5 w-3.5" />,
};
export function DataSourceBar({ refreshIntervalMs }: DataSourceBarProps) {
const { t } = useTranslation();
const queryClient = useQueryClient();
const [syncing, setSyncing] = useState(false);
const { data: sources } = useQuery({
queryKey: [...usageKeys.all, "data-sources"],
queryFn: usageApi.getDataSourceBreakdown,
refetchInterval: refreshIntervalMs > 0 ? refreshIntervalMs : false,
refetchIntervalInBackground: false,
});
const handleSync = async () => {
setSyncing(true);
try {
const result = await usageApi.syncSessionUsage();
if (result.imported > 0) {
toast.success(
t("usage.sessionSync.imported", {
count: result.imported,
defaultValue: "Imported {{count}} records from session logs",
}),
);
// Refresh all usage data
queryClient.invalidateQueries({ queryKey: usageKeys.all });
} else {
toast.info(
t("usage.sessionSync.upToDate", {
defaultValue: "Session logs are up to date",
}),
);
}
} catch {
toast.error(
t("usage.sessionSync.failed", {
defaultValue: "Session sync failed",
}),
);
} finally {
setSyncing(false);
}
};
if (!sources || sources.length === 0) {
return null;
}
const hasNonProxy = sources.some((s) => s.dataSource !== "proxy");
return (
<div className="flex items-center gap-3 text-xs text-muted-foreground bg-muted/30 rounded-lg px-4 py-2">
<span className="font-medium text-foreground/70">
{t("usage.dataSources", { defaultValue: "Data Sources" })}:
</span>
<div className="flex items-center gap-3 flex-wrap">
{sources.map((source) => (
<div
key={source.dataSource}
className="flex items-center gap-1.5 bg-background/50 rounded-md px-2 py-1"
>
{DATA_SOURCE_ICONS[source.dataSource] ?? (
<Database className="h-3.5 w-3.5" />
)}
<span>
{t(`usage.dataSource.${source.dataSource}`, {
defaultValue: source.dataSource,
})}
</span>
<span className="font-mono font-medium text-foreground/80">
{source.requestCount.toLocaleString()}
</span>
</div>
))}
</div>
<div className="ml-auto">
<Button
variant="ghost"
size="sm"
className="h-7 px-2 text-xs"
onClick={handleSync}
disabled={syncing}
title={t("usage.sessionSync.trigger", {
defaultValue: "Sync session logs",
})}
>
{syncing ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<RefreshCw className="h-3.5 w-3.5" />
)}
<span className="ml-1">
{hasNonProxy
? t("usage.sessionSync.resync", { defaultValue: "Sync" })
: t("usage.sessionSync.import", {
defaultValue: "Import Sessions",
})}
</span>
</Button>
</div>
</div>
);
}
-77
View File
@@ -1,77 +0,0 @@
/**
* 根据供应商名称智能推断图标配置
*/
const iconMappings = {
// AI 服务商
claude: { icon: "claude", iconColor: "#D4915D" },
anthropic: { icon: "anthropic", iconColor: "#D4915D" },
deepseek: { icon: "deepseek", iconColor: "#1E88E5" },
zhipu: { icon: "zhipu", iconColor: "#0F62FE" },
glm: { icon: "zhipu", iconColor: "#0F62FE" },
qwen: { icon: "qwen", iconColor: "#FF6A00" },
bailian: { icon: "bailian", iconColor: "#624AFF" },
alibaba: { icon: "alibaba", iconColor: "#FF6A00" },
aliyun: { icon: "alibaba", iconColor: "#FF6A00" },
kimi: { icon: "kimi", iconColor: "#6366F1" },
moonshot: { icon: "moonshot", iconColor: "#6366F1" },
stepfun: { icon: "stepfun", iconColor: "#005AFF" },
step: { icon: "stepfun", iconColor: "#005AFF" },
baidu: { icon: "baidu", iconColor: "#2932E1" },
tencent: { icon: "tencent", iconColor: "#00A4FF" },
hunyuan: { icon: "hunyuan", iconColor: "#00A4FF" },
minimax: { icon: "minimax", iconColor: "#FF6B6B" },
google: { icon: "google", iconColor: "#4285F4" },
meta: { icon: "meta", iconColor: "#0081FB" },
mistral: { icon: "mistral", iconColor: "#FF7000" },
cohere: { icon: "cohere", iconColor: "#39594D" },
perplexity: { icon: "perplexity", iconColor: "#20808D" },
huggingface: { icon: "huggingface", iconColor: "#FFD21E" },
novita: { icon: "novita", iconColor: "#000000" },
// 云平台
aws: { icon: "aws", iconColor: "#FF9900" },
azure: { icon: "azure", iconColor: "#0078D4" },
huawei: { icon: "huawei", iconColor: "#FF0000" },
cloudflare: { icon: "cloudflare", iconColor: "#F38020" },
};
/**
* 根据预设名称推断图标
*/
export function inferIconForPreset(presetName: string): {
icon?: string;
iconColor?: string;
} {
const nameLower = presetName.toLowerCase();
// 精确匹配或模糊匹配
for (const [key, config] of Object.entries(iconMappings)) {
if (nameLower.includes(key)) {
return config;
}
}
return {};
}
/**
* 批量为预设添加图标配置
*/
export function addIconsToPresets<
T extends { name: string; icon?: string; iconColor?: string },
>(presets: T[]): T[] {
return presets.map((preset) => {
// 如果已经配置了图标,则保留原配置
if (preset.icon) {
return preset;
}
// 否则根据名称推断
const inferred = inferIconForPreset(preset.name);
return {
...preset,
...inferred,
};
});
}
-48
View File
@@ -1,48 +0,0 @@
/**
* 代理配置管理 Hook
*/
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { invoke } from "@tauri-apps/api/core";
import { toast } from "sonner";
import { useTranslation } from "react-i18next";
import type { ProxyConfig } from "@/types/proxy";
/**
* 代理配置管理
*/
export function useProxyConfig() {
const queryClient = useQueryClient();
const { t } = useTranslation();
// 查询配置
const { data: config, isLoading } = useQuery({
queryKey: ["proxyConfig"],
queryFn: () => invoke<ProxyConfig>("get_proxy_config"),
});
// 更新配置
const updateMutation = useMutation({
mutationFn: (newConfig: ProxyConfig) =>
invoke("update_proxy_config", { config: newConfig }),
onSuccess: () => {
toast.success(t("proxy.settings.toast.saved"), { closeButton: true });
queryClient.invalidateQueries({ queryKey: ["proxyConfig"] });
queryClient.invalidateQueries({ queryKey: ["proxyStatus"] });
},
onError: (error: Error) => {
toast.error(
t("proxy.settings.toast.saveFailed", {
error: error.message,
}),
);
},
});
return {
config,
isLoading,
updateConfig: updateMutation.mutateAsync,
isUpdating: updateMutation.isPending,
};
}
+1 -48
View File
@@ -23,7 +23,6 @@
"unknown": "Unknown",
"enterValidValue": "Please enter a valid value",
"clear": "Clear",
"toggleTheme": "Toggle theme",
"format": "Format",
"formatSuccess": "Formatted successfully",
"formatError": "Format failed: {{error}}",
@@ -1507,24 +1506,6 @@
"grokbuild": "Grok Build"
},
"rawInputLabel": "Raw",
"dataSources": "Data Sources",
"dataSource": {
"proxy": "Routing",
"session_log": "Session Log",
"codex_db": "Codex DB",
"codex_session": "Codex Session",
"gemini_session": "Gemini Session",
"opencode_session": "OpenCode Session",
"grok_session": "Grok Build Session"
},
"sessionSync": {
"trigger": "Sync session logs",
"import": "Import Sessions",
"resync": "Sync",
"imported": "Imported {{count}} records from session logs",
"upToDate": "Session logs are up to date",
"failed": "Session sync failed"
},
"rebuildCodex": {
"title": "Codex Usage Maintenance",
"description": "Rebuild Codex session usage from local rollout logs",
@@ -2539,8 +2520,7 @@
"selectIcon": "Select Icon",
"preview": "Preview",
"clickToChange": "Click to change icon",
"clickToSelect": "Click to select icon",
"color": "Icon Color"
"clickToSelect": "Click to select icon"
},
"migration": {
"success": "Configuration migrated successfully",
@@ -2782,33 +2762,6 @@
"streamingIdle": "Streaming Idle Timeout",
"nonStreaming": "Non-Streaming Timeout"
},
"circuitBreaker": {
"failureThreshold": "Failure Threshold",
"successThreshold": "Success Threshold",
"timeoutSeconds": "Timeout (seconds)",
"errorRateThreshold": "Error Rate Threshold (%)",
"minRequests": "Minimum Requests",
"validationFailed": "The following fields are out of valid range: {{fields}}",
"configSaved": "Circuit breaker configuration saved",
"saveFailed": "Save failed",
"loading": "Loading...",
"title": "Circuit Breaker Configuration",
"description": "Adjust circuit breaker parameters to control fault detection and recovery behavior",
"failureThresholdHint": "How many consecutive failures trigger the circuit breaker",
"timeoutSecondsHint": "How long to wait before attempting recovery (half-open state)",
"successThresholdHint": "How many successes in half-open state to close the circuit breaker",
"errorRateThresholdHint": "Open circuit breaker when error rate exceeds this value",
"minRequestsHint": "Minimum requests before calculating error rate",
"saveConfig": "Save Configuration",
"instructionsTitle": "Configuration Instructions",
"instructions": {
"failureThreshold": "Circuit breaker opens when consecutive failures reach this count",
"timeout": "After circuit breaker opens, wait this time before attempting half-open",
"successThreshold": "In half-open state, close circuit breaker when successes reach this count",
"errorRate": "Circuit breaker opens when error rate exceeds this value",
"minRequests": "Error rate is only calculated after request count reaches this value"
}
},
"universalProvider": {
"duplicate": "Duplicate",
"duplicatedAndSynced": "Universal provider duplicated and synced",
+1 -48
View File
@@ -23,7 +23,6 @@
"unknown": "不明",
"enterValidValue": "有効な値を入力してください",
"clear": "クリア",
"toggleTheme": "テーマを切り替え",
"format": "フォーマット",
"formatSuccess": "整形しました",
"formatError": "整形に失敗しました: {{error}}",
@@ -1507,24 +1506,6 @@
"grokbuild": "Grok Build"
},
"rawInputLabel": "原始",
"dataSources": "データソース",
"dataSource": {
"proxy": "ルーティング",
"session_log": "セッションログ",
"codex_db": "Codex DB",
"codex_session": "Codex セッション",
"gemini_session": "Gemini セッション",
"opencode_session": "OpenCode セッション",
"grok_session": "Grok Build セッション"
},
"sessionSync": {
"trigger": "セッションログを同期",
"import": "セッションをインポート",
"resync": "同期",
"imported": "セッションログから {{count}} 件のレコードをインポートしました",
"upToDate": "セッションログは最新です",
"failed": "セッション同期に失敗しました"
},
"rebuildCodex": {
"title": "Codex 使用量メンテナンス",
"description": "ローカルの rollout ログから Codex セッション使用量を再構築します",
@@ -2539,8 +2520,7 @@
"selectIcon": "アイコンを選択",
"preview": "プレビュー",
"clickToChange": "クリックでアイコンを変更",
"clickToSelect": "クリックでアイコンを選択",
"color": "アイコンカラー"
"clickToSelect": "クリックでアイコンを選択"
},
"migration": {
"success": "設定の移行が完了しました",
@@ -2782,33 +2762,6 @@
"streamingIdle": "ストリーミングアイドルタイムアウト",
"nonStreaming": "非ストリーミングタイムアウト"
},
"circuitBreaker": {
"failureThreshold": "失敗閾値",
"successThreshold": "成功閾値",
"timeoutSeconds": "タイムアウト(秒)",
"errorRateThreshold": "エラー率閾値 (%)",
"minRequests": "最小リクエスト数",
"validationFailed": "以下のフィールドが有効範囲外です: {{fields}}",
"configSaved": "サーキットブレーカー設定が保存されました",
"saveFailed": "保存に失敗しました",
"loading": "読み込み中...",
"title": "サーキットブレーカー設定",
"description": "サーキットブレーカーパラメータを調整して、障害検出と復旧動作を制御します",
"failureThresholdHint": "連続失敗後にサーキットブレーカーを開く回数",
"timeoutSecondsHint": "サーキットブレーカーを開いた後、復旧を試みるまでの時間(半開状態)",
"successThresholdHint": "半開状態で成功してサーキットブレーカーを閉じる回数",
"errorRateThresholdHint": "エラー率がこの値を超えるとサーキットブレーカーを開く",
"minRequestsHint": "エラー率を計算する前の最小リクエスト数",
"saveConfig": "設定を保存",
"instructionsTitle": "設定説明",
"instructions": {
"failureThreshold": "連続失敗がこの回数に達するとサーキットブレーカーが開く",
"timeout": "サーキットブレーカーを開いた後、この時間待機してから半開を試みる",
"successThreshold": "半開状態で成功がこの回数に達するとサーキットブレーカーを閉じる",
"errorRate": "エラー率がこの値を超えるとサーキットブレーカーが開く",
"minRequests": "リクエスト数がこの値に達した後にのみエラー率が計算される"
}
},
"universalProvider": {
"duplicate": "複製",
"duplicatedAndSynced": "統合プロバイダーを複製して同期しました",
+1 -48
View File
@@ -23,7 +23,6 @@
"unknown": "未知",
"enterValidValue": "請輸入有效的內容",
"clear": "清除",
"toggleTheme": "切換主題",
"format": "格式化",
"formatSuccess": "格式化成功",
"formatError": "格式化失敗:{{error}}",
@@ -1478,24 +1477,6 @@
"grokbuild": "Grok Build"
},
"rawInputLabel": "原始",
"dataSources": "資料來源",
"dataSource": {
"proxy": "路由",
"session_log": "工作階段日誌",
"codex_db": "Codex 資料庫",
"codex_session": "Codex 工作階段日誌",
"gemini_session": "Gemini 工作階段日誌",
"opencode_session": "OpenCode 工作階段日誌",
"grok_session": "Grok Build 工作階段日誌"
},
"sessionSync": {
"trigger": "同步工作階段日誌",
"import": "匯入工作階段",
"resync": "同步",
"imported": "從工作階段日誌匯入了 {{count}} 筆紀錄",
"upToDate": "工作階段日誌已是最新",
"failed": "工作階段同步失敗"
},
"rebuildCodex": {
"title": "Codex 用量維護",
"description": "從本機 rollout 日誌重新建構 Codex 工作階段用量",
@@ -2510,8 +2491,7 @@
"selectIcon": "選擇圖示",
"preview": "預覽",
"clickToChange": "點擊更換圖示",
"clickToSelect": "點擊選擇圖示",
"color": "圖示顏色"
"clickToSelect": "點擊選擇圖示"
},
"migration": {
"success": "設定遷移成功",
@@ -2753,33 +2733,6 @@
"streamingIdle": "串流閒置逾時",
"nonStreaming": "非串流逾時"
},
"circuitBreaker": {
"failureThreshold": "失敗閾值",
"successThreshold": "成功閾值",
"timeoutSeconds": "逾時時間(秒)",
"errorRateThreshold": "錯誤率閾值 (%)",
"minRequests": "最小請求數",
"validationFailed": "以下欄位超出有效範圍:{{fields}}",
"configSaved": "斷路器設定已儲存",
"saveFailed": "儲存失敗",
"loading": "載入中...",
"title": "斷路器設定",
"description": "調整斷路器參數以控制故障檢測和恢復行為",
"failureThresholdHint": "連續失敗多少次後打開斷路器",
"timeoutSecondsHint": "斷路器打開後多久嘗試恢復(半開狀態)",
"successThresholdHint": "半開狀態下成功多少次後關閉斷路器",
"errorRateThresholdHint": "錯誤率超過此值時打開斷路器",
"minRequestsHint": "計算錯誤率前的最小請求數",
"saveConfig": "儲存設定",
"instructionsTitle": "設定說明",
"instructions": {
"failureThreshold": "連續失敗達到此次數時,斷路器打開",
"timeout": "斷路器打開後,等待此時間後嘗試半開",
"successThreshold": "半開狀態下,成功達到此次數時關閉斷路器",
"errorRate": "錯誤率超過此值時,斷路器打開",
"minRequests": "只有請求數達到此值後才計算錯誤率"
}
},
"universalProvider": {
"duplicate": "複製",
"duplicatedAndSynced": "通用供應商已複製並同步",
+1 -48
View File
@@ -23,7 +23,6 @@
"unknown": "未知",
"enterValidValue": "请输入有效的内容",
"clear": "清除",
"toggleTheme": "切换主题",
"format": "格式化",
"formatSuccess": "格式化成功",
"formatError": "格式化失败:{{error}}",
@@ -1507,24 +1506,6 @@
"grokbuild": "Grok Build"
},
"rawInputLabel": "原始",
"dataSources": "数据来源",
"dataSource": {
"proxy": "路由",
"session_log": "会话日志",
"codex_db": "Codex 数据库",
"codex_session": "Codex 会话日志",
"gemini_session": "Gemini 会话日志",
"opencode_session": "OpenCode 会话日志",
"grok_session": "Grok Build 会话日志"
},
"sessionSync": {
"trigger": "同步会话日志",
"import": "导入会话",
"resync": "同步",
"imported": "从会话日志导入了 {{count}} 条记录",
"upToDate": "会话日志已是最新",
"failed": "会话同步失败"
},
"rebuildCodex": {
"title": "Codex 用量维护",
"description": "从本地 rollout 日志重新构建 Codex 会话用量",
@@ -2539,8 +2520,7 @@
"selectIcon": "选择图标",
"preview": "预览",
"clickToChange": "点击更换图标",
"clickToSelect": "点击选择图标",
"color": "图标颜色"
"clickToSelect": "点击选择图标"
},
"migration": {
"success": "配置迁移成功",
@@ -2782,33 +2762,6 @@
"streamingIdle": "流式静默超时",
"nonStreaming": "非流式超时"
},
"circuitBreaker": {
"failureThreshold": "失败阈值",
"successThreshold": "成功阈值",
"timeoutSeconds": "超时时间(秒)",
"errorRateThreshold": "错误率阈值 (%)",
"minRequests": "最小请求数",
"validationFailed": "以下字段超出有效范围: {{fields}}",
"configSaved": "熔断器配置已保存",
"saveFailed": "保存失败",
"loading": "加载中...",
"title": "熔断器配置",
"description": "调整熔断器参数以控制故障检测和恢复行为",
"failureThresholdHint": "连续失败多少次后打开熔断器",
"timeoutSecondsHint": "熔断器打开后多久尝试恢复(半开状态)",
"successThresholdHint": "半开状态下成功多少次后关闭熔断器",
"errorRateThresholdHint": "错误率超过此值时打开熔断器",
"minRequestsHint": "计算错误率前的最小请求数",
"saveConfig": "保存配置",
"instructionsTitle": "配置说明",
"instructions": {
"failureThreshold": "连续失败达到此次数时,熔断器打开",
"timeout": "熔断器打开后,等待此时间后尝试半开",
"successThreshold": "半开状态下,成功达到此次数时关闭熔断器",
"errorRate": "错误率超过此值时,熔断器打开",
"minRequests": "只有请求数达到此值后才计算错误率"
}
},
"universalProvider": {
"duplicate": "复制",
"duplicatedAndSynced": "统一供应商已复制并同步",
+2 -2
View File
@@ -1,5 +1,5 @@
// Auto-generated icon index
// Do not edit manually
// Hand-curated icon index with optimized SVG content and custom name mappings.
// Update entries deliberately; automatic regeneration is intentionally unsupported.
import _a6api from "./a6-icon.png";
import _apikeyfun from "./apikeyfun.png";
-98
View File
@@ -1,98 +0,0 @@
import { z } from "zod";
import { validateToml, tomlToMcpServer } from "@/utils/tomlUtils";
/**
* 解析 JSON 语法错误,返回更友好的位置信息。
*/
function parseJsonError(error: unknown): string {
if (!(error instanceof SyntaxError)) {
return "JSON 格式错误";
}
const message = error.message || "JSON 解析失败";
// Chrome/V8: "Unexpected token ... in JSON at position 123"
const positionMatch = message.match(/at position (\d+)/i);
if (positionMatch) {
const position = parseInt(positionMatch[1], 10);
return `JSON 格式错误(位置:${position}`;
}
// Firefox: "JSON.parse: unexpected character at line 1 column 23"
const lineColumnMatch = message.match(/line (\d+) column (\d+)/i);
if (lineColumnMatch) {
const line = lineColumnMatch[1];
const column = lineColumnMatch[2];
return `JSON 格式错误:第 ${line} 行,第 ${column}`;
}
return `JSON 格式错误:${message}`;
}
/**
* 通用的 JSON 配置文本校验:
* - 非空
* - 可解析且为对象(非数组)
*/
export const jsonConfigSchema = z
.string()
.min(1, "配置不能为空")
.superRefine((value, ctx) => {
try {
const obj = JSON.parse(value);
if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "需为单个对象配置",
});
}
} catch (e) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: parseJsonError(e),
});
}
});
/**
* 通用的 TOML 配置文本校验:
* - 允许为空(由上层业务决定是否必填)
* - 语法与结构有效
* - 针对 stdio/http/sse 的必填字段(command/url)进行提示
*/
export const tomlConfigSchema = z.string().superRefine((value, ctx) => {
const err = validateToml(value);
if (err) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `TOML 无效:${err}`,
});
return;
}
if (!value.trim()) return;
try {
const server = tomlToMcpServer(value);
if (server.type === "stdio" && !server.command?.trim()) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "stdio 类型需填写 command",
});
}
if (
(server.type === "http" || server.type === "sse") &&
!server.url?.trim()
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `${server.type} 类型需填写 url`,
});
}
} catch (e: any) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: e?.message || "TOML 解析失败",
});
}
});
-42
View File
@@ -1,42 +0,0 @@
import { z } from "zod";
const mcpServerSpecSchema = z
.object({
type: z.enum(["stdio", "http", "sse"]).optional(),
command: z.string().trim().optional(),
args: z.array(z.string()).optional(),
env: z.record(z.string(), z.string()).optional(),
cwd: z.string().optional(),
url: z.string().trim().url("请输入有效的 URL").optional(),
headers: z.record(z.string(), z.string()).optional(),
})
.superRefine((server, ctx) => {
const type = server.type ?? "stdio";
if (type === "stdio" && !server.command?.trim()) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "stdio 类型需填写 command",
path: ["command"],
});
}
if ((type === "http" || type === "sse") && !server.url?.trim()) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `${type} 类型需填写 url`,
path: ["url"],
});
}
});
export const mcpServerSchema = z.object({
id: z.string().min(1, "请输入服务器 ID"),
name: z.string().optional(),
description: z.string().optional(),
tags: z.array(z.string()).optional(),
homepage: z.string().url().optional(),
docs: z.string().url().optional(),
enabled: z.boolean().optional(),
server: mcpServerSpecSchema,
});
export type McpServerFormData = z.infer<typeof mcpServerSchema>;
-80
View File
@@ -1,80 +0,0 @@
import { z } from "zod";
const directorySchema = z
.string()
.trim()
.min(1, "路径不能为空")
.optional()
.or(z.literal(""));
export const settingsSchema = z.object({
// 设备级 UI 设置
showInTray: z.boolean(),
minimizeToTrayOnClose: z.boolean(),
enableClaudePluginIntegration: z.boolean().optional(),
skipClaudeOnboarding: z.boolean().optional(),
launchOnStartup: z.boolean().optional(),
enableLocalProxy: z.boolean().optional(),
usageDashboardRefreshIntervalMs: z.number().optional(),
preserveCodexOfficialAuthOnSwitch: z.boolean().optional(),
unifyCodexSessionHistory: z.boolean().optional(),
language: z.enum(["en", "zh", "zh-TW", "ja"]).optional(),
// 设备级目录覆盖
claudeConfigDir: directorySchema.nullable().optional(),
codexConfigDir: directorySchema.nullable().optional(),
geminiConfigDir: directorySchema.nullable().optional(),
grokConfigDir: directorySchema.nullable().optional(),
opencodeConfigDir: directorySchema.nullable().optional(),
openclawConfigDir: directorySchema.nullable().optional(),
// 当前供应商 ID(设备级)
currentProviderClaude: z.string().optional(),
currentProviderClaudeDesktop: z.string().optional(),
currentProviderCodex: z.string().optional(),
currentProviderGemini: z.string().optional(),
// Skill 同步设置
skillSyncMethod: z.enum(["auto", "symlink", "copy"]).optional(),
skillStorageLocation: z.enum(["cc_switch", "unified"]).optional(),
// WebDAV v2 同步设置(通过专用命令保存,schema 仅用于读取)
webdavSync: z
.object({
enabled: z.boolean().optional(),
autoSync: z.boolean().optional(),
baseUrl: z.string().trim().optional().or(z.literal("")),
username: z.string().trim().optional().or(z.literal("")),
password: z.string().optional(),
remoteRoot: z.string().trim().optional().or(z.literal("")),
profile: z.string().trim().optional().or(z.literal("")),
status: z
.object({
lastSyncAt: z.number().nullable().optional(),
lastError: z.string().nullable().optional(),
lastErrorSource: z.string().nullable().optional(),
lastRemoteEtag: z.string().nullable().optional(),
lastLocalManifestHash: z.string().nullable().optional(),
lastRemoteManifestHash: z.string().nullable().optional(),
})
.optional(),
})
.optional(),
// 本机自动迁移状态(后端维护且保存时后端忽略前端值,仅供读取展示)
localMigrations: z
.object({
codexThirdPartyHistoryProviderBucketV1: z
.object({
completedAt: z.string(),
targetProviderId: z.string(),
sourceProviderIds: z.array(z.string()).optional(),
migratedJsonlFiles: z.number().optional(),
migratedStateRows: z.number().optional(),
})
.optional(),
})
.optional(),
});
export type SettingsFormData = z.infer<typeof settingsSchema>;