mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-03-22 07:04:25 +08:00
Merge branch 'main' into feat/proxy-url-refactor-full-url-mode
This commit is contained in:
@@ -77,10 +77,10 @@ const ToolsPanel: React.FC = () => {
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
const { allow, deny, ...other } = config;
|
||||
const { profile, allow, deny, ...other } = config;
|
||||
const newConfig: OpenClawToolsConfig = {
|
||||
...other,
|
||||
profile: config.profile,
|
||||
profile,
|
||||
allow: allowList.map((item) => item.value).filter((s) => s.trim()),
|
||||
deny: denyList.map((item) => item.value).filter((s) => s.trim()),
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import { Plus, Trash2, ChevronDown, ChevronRight } from "lucide-react";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { ApiKeySection } from "./shared";
|
||||
import { openclawApiProtocols } from "@/config/openclawProviderPresets";
|
||||
import type { ProviderCategory, OpenClawModel } from "@/types";
|
||||
@@ -101,6 +102,7 @@ export function OpenClawFormFields({
|
||||
contextWindow: undefined,
|
||||
maxTokens: undefined,
|
||||
cost: undefined,
|
||||
input: ["text"],
|
||||
},
|
||||
]);
|
||||
};
|
||||
@@ -339,7 +341,66 @@ export function OpenClawFormFields({
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="space-y-3 pt-2">
|
||||
{/* Context Window, Max Tokens and Reasoning row */}
|
||||
{/* Reasoning, Input Types row */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 space-y-1">
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{t("openclaw.reasoning", {
|
||||
defaultValue: "推理模式",
|
||||
})}
|
||||
</label>
|
||||
<div className="flex items-center h-9 gap-2">
|
||||
<Switch
|
||||
checked={model.reasoning ?? false}
|
||||
onCheckedChange={(checked) =>
|
||||
handleModelChange(index, "reasoning", checked)
|
||||
}
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{model.reasoning
|
||||
? t("openclaw.reasoningOn", {
|
||||
defaultValue: "启用",
|
||||
})
|
||||
: t("openclaw.reasoningOff", {
|
||||
defaultValue: "关闭",
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 space-y-1">
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{t("openclaw.inputTypes", {
|
||||
defaultValue: "输入类型",
|
||||
})}
|
||||
</label>
|
||||
{/* "text" is checked by default but can be unchecked —
|
||||
some models genuinely don't support text input, and
|
||||
OpenClaw works fine with an empty or image-only array. */}
|
||||
<div className="flex items-center gap-4 h-9">
|
||||
{(["text", "image"] as const).map((type) => (
|
||||
<label
|
||||
key={type}
|
||||
className="flex items-center gap-1.5 cursor-pointer select-none"
|
||||
>
|
||||
<Checkbox
|
||||
checked={(model.input ?? ["text"]).includes(type)}
|
||||
onCheckedChange={(checked) => {
|
||||
const current = model.input ?? ["text"];
|
||||
const next = checked
|
||||
? [...new Set([...current, type])]
|
||||
: current.filter((v) => v !== type);
|
||||
handleModelChange(index, "input", next);
|
||||
}}
|
||||
/>
|
||||
<span className="text-xs">{type}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1" />
|
||||
</div>
|
||||
|
||||
{/* Context Window and Max Tokens row */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 space-y-1">
|
||||
<label className="text-xs text-muted-foreground">
|
||||
@@ -383,30 +444,7 @@ export function OpenClawFormFields({
|
||||
placeholder="32000"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 space-y-1">
|
||||
<label className="text-xs text-muted-foreground">
|
||||
{t("openclaw.reasoning", {
|
||||
defaultValue: "推理模式",
|
||||
})}
|
||||
</label>
|
||||
<div className="flex items-center h-9 gap-2">
|
||||
<Switch
|
||||
checked={model.reasoning ?? false}
|
||||
onCheckedChange={(checked) =>
|
||||
handleModelChange(index, "reasoning", checked)
|
||||
}
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{model.reasoning
|
||||
? t("openclaw.reasoningOn", {
|
||||
defaultValue: "启用",
|
||||
})
|
||||
: t("openclaw.reasoningOff", {
|
||||
defaultValue: "关闭",
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1" />
|
||||
</div>
|
||||
|
||||
{/* Cost row */}
|
||||
|
||||
@@ -458,6 +458,7 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
||||
baseUrl: "https://api.longcat.chat/v1",
|
||||
apiKey: "",
|
||||
api: "openai-completions",
|
||||
authHeader: true,
|
||||
models: [
|
||||
{
|
||||
id: "LongCat-Flash-Chat",
|
||||
|
||||
@@ -43,6 +43,8 @@ export function useProxyStatus() {
|
||||
onSuccess: (info) => {
|
||||
toast.success(
|
||||
t("proxy.server.started", {
|
||||
address: info.address,
|
||||
port: info.port,
|
||||
defaultValue: `代理服务已启动 - ${info.address}:${info.port}`,
|
||||
}),
|
||||
{ closeButton: true },
|
||||
|
||||
@@ -1340,6 +1340,7 @@
|
||||
"reasoning": "Reasoning Mode",
|
||||
"reasoningOn": "Enabled",
|
||||
"reasoningOff": "Disabled",
|
||||
"inputTypes": "Input Types",
|
||||
"inputCost": "Input Cost ($/M tokens)",
|
||||
"outputCost": "Output Cost ($/M tokens)",
|
||||
"advancedOptions": "Advanced Options",
|
||||
@@ -1376,12 +1377,6 @@
|
||||
"unsupportedProfileTitle": "Unsupported tools profile detected",
|
||||
"unsupportedProfileDescription": "The current tools.profile value '{{value}}' is not in the supported OpenClaw list. It will be preserved until you choose a new value.",
|
||||
"unsupportedProfileLabel": "unsupported",
|
||||
"profiles": {
|
||||
"default": "Default",
|
||||
"strict": "Strict",
|
||||
"permissive": "Permissive",
|
||||
"custom": "Custom"
|
||||
},
|
||||
"allowList": "Allow List",
|
||||
"denyList": "Deny List",
|
||||
"patternPlaceholder": "Tool name or pattern",
|
||||
|
||||
@@ -1340,6 +1340,7 @@
|
||||
"reasoning": "推論モード",
|
||||
"reasoningOn": "有効",
|
||||
"reasoningOff": "無効",
|
||||
"inputTypes": "入力タイプ",
|
||||
"inputCost": "入力コスト ($/M トークン)",
|
||||
"outputCost": "出力コスト ($/M トークン)",
|
||||
"advancedOptions": "詳細オプション",
|
||||
@@ -1376,12 +1377,6 @@
|
||||
"unsupportedProfileTitle": "未対応のツールプロファイルを検出しました",
|
||||
"unsupportedProfileDescription": "現在の tools.profile の値 '{{value}}' は OpenClaw の対応リストにありません。新しい値を選択するまでこの値を保持します。",
|
||||
"unsupportedProfileLabel": "未対応",
|
||||
"profiles": {
|
||||
"default": "デフォルト",
|
||||
"strict": "厳格",
|
||||
"permissive": "寛容",
|
||||
"custom": "カスタム"
|
||||
},
|
||||
"allowList": "許可リスト",
|
||||
"denyList": "拒否リスト",
|
||||
"patternPlaceholder": "ツール名またはパターン",
|
||||
|
||||
@@ -1340,6 +1340,7 @@
|
||||
"reasoning": "推理模式",
|
||||
"reasoningOn": "启用",
|
||||
"reasoningOff": "关闭",
|
||||
"inputTypes": "输入类型",
|
||||
"inputCost": "输入价格 ($/M tokens)",
|
||||
"outputCost": "输出价格 ($/M tokens)",
|
||||
"advancedOptions": "高级选项",
|
||||
@@ -1376,12 +1377,6 @@
|
||||
"unsupportedProfileTitle": "检测到不受支持的工具配置",
|
||||
"unsupportedProfileDescription": "当前 tools.profile 的值“{{value}}”不在 OpenClaw 支持列表内。在你手动选择新值之前,它会被保留。",
|
||||
"unsupportedProfileLabel": "不受支持",
|
||||
"profiles": {
|
||||
"default": "默认",
|
||||
"strict": "严格",
|
||||
"permissive": "宽松",
|
||||
"custom": "自定义"
|
||||
},
|
||||
"allowList": "允许列表",
|
||||
"denyList": "拒绝列表",
|
||||
"patternPlaceholder": "工具名称或模式",
|
||||
|
||||
118
tests/hooks/useProxyStatus.test.tsx
Normal file
118
tests/hooks/useProxyStatus.test.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { renderHook, act, waitFor } from "@testing-library/react";
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { useProxyStatus } from "@/hooks/useProxyStatus";
|
||||
import { createTestQueryClient } from "../utils/testQueryClient";
|
||||
|
||||
const toastSuccessMock = vi.fn();
|
||||
const toastErrorMock = vi.fn();
|
||||
const invokeMock = vi.fn();
|
||||
|
||||
vi.mock("sonner", () => ({
|
||||
toast: {
|
||||
success: (...args: unknown[]) => toastSuccessMock(...args),
|
||||
error: (...args: unknown[]) => toastErrorMock(...args),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@tauri-apps/api/core", () => ({
|
||||
invoke: (...args: unknown[]) => invokeMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("react-i18next", () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: Record<string, unknown>) => {
|
||||
if (key === "proxy.server.started") {
|
||||
return `代理服务已启动 - ${options?.address}:${options?.port}`;
|
||||
}
|
||||
|
||||
if (typeof options?.defaultValue === "string") {
|
||||
return options.defaultValue;
|
||||
}
|
||||
|
||||
return key;
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
interface WrapperProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
function createWrapper() {
|
||||
const queryClient = createTestQueryClient();
|
||||
|
||||
const wrapper = ({ children }: WrapperProps) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
|
||||
return { wrapper, queryClient };
|
||||
}
|
||||
|
||||
describe("useProxyStatus", () => {
|
||||
beforeEach(() => {
|
||||
invokeMock.mockReset();
|
||||
toastSuccessMock.mockReset();
|
||||
toastErrorMock.mockReset();
|
||||
|
||||
invokeMock.mockImplementation((command: string) => {
|
||||
if (command === "get_proxy_status") {
|
||||
return Promise.resolve({
|
||||
running: false,
|
||||
address: "127.0.0.1",
|
||||
port: 15721,
|
||||
active_connections: 0,
|
||||
total_requests: 0,
|
||||
success_requests: 0,
|
||||
failed_requests: 0,
|
||||
success_rate: 0,
|
||||
uptime_seconds: 0,
|
||||
current_provider: null,
|
||||
current_provider_id: null,
|
||||
last_request_at: null,
|
||||
last_error: null,
|
||||
failover_count: 0,
|
||||
});
|
||||
}
|
||||
|
||||
if (command === "get_proxy_takeover_status") {
|
||||
return Promise.resolve({
|
||||
claude: false,
|
||||
codex: false,
|
||||
gemini: false,
|
||||
opencode: false,
|
||||
openclaw: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (command === "start_proxy_server") {
|
||||
return Promise.resolve({
|
||||
address: "127.0.0.1",
|
||||
port: 15721,
|
||||
started_at: "2026-03-10T00:00:00Z",
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
});
|
||||
|
||||
it("shows interpolated address and port after proxy server starts", async () => {
|
||||
const { wrapper } = createWrapper();
|
||||
const { result } = renderHook(() => useProxyStatus(), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.startProxyServer();
|
||||
});
|
||||
|
||||
expect(toastSuccessMock).toHaveBeenCalledWith(
|
||||
"代理服务已启动 - 127.0.0.1:15721",
|
||||
{ closeButton: true },
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user