Merge branch 'main' into feat/proxy-url-refactor-full-url-mode

This commit is contained in:
YoVinchen
2026-03-11 10:07:44 +08:00
8 changed files with 189 additions and 45 deletions

View File

@@ -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()),
};

View File

@@ -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 */}

View File

@@ -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",

View File

@@ -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 },

View File

@@ -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",

View File

@@ -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": "ツール名またはパターン",

View File

@@ -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": "工具名称或模式",

View 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 },
);
});
});