mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-03 02:51:17 +08:00
refactor: remove superseded dead code (#5916)
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "統合プロバイダーを複製して同期しました",
|
||||
|
||||
@@ -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": "通用供應商已複製並同步",
|
||||
|
||||
@@ -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": "统一供应商已复制并同步",
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 解析失败",
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -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>;
|
||||
@@ -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>;
|
||||
Reference in New Issue
Block a user