diff --git a/src/components/openclaw/ToolsPanel.tsx b/src/components/openclaw/ToolsPanel.tsx index 65b1f462..5d8ec09d 100644 --- a/src/components/openclaw/ToolsPanel.tsx +++ b/src/components/openclaw/ToolsPanel.tsx @@ -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()), }; diff --git a/src/components/providers/forms/OpenClawFormFields.tsx b/src/components/providers/forms/OpenClawFormFields.tsx index 74568264..8c35e0df 100644 --- a/src/components/providers/forms/OpenClawFormFields.tsx +++ b/src/components/providers/forms/OpenClawFormFields.tsx @@ -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({ - {/* Context Window, Max Tokens and Reasoning row */} + {/* Reasoning, Input Types row */} +
+
+ +
+ + handleModelChange(index, "reasoning", checked) + } + /> + + {model.reasoning + ? t("openclaw.reasoningOn", { + defaultValue: "启用", + }) + : t("openclaw.reasoningOff", { + defaultValue: "关闭", + })} + +
+
+
+ + {/* "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. */} +
+ {(["text", "image"] as const).map((type) => ( + + ))} +
+
+
+
+ + {/* Context Window and Max Tokens row */}
-
- -
- - handleModelChange(index, "reasoning", checked) - } - /> - - {model.reasoning - ? t("openclaw.reasoningOn", { - defaultValue: "启用", - }) - : t("openclaw.reasoningOff", { - defaultValue: "关闭", - })} - -
-
+
{/* Cost row */} diff --git a/src/config/openclawProviderPresets.ts b/src/config/openclawProviderPresets.ts index f9b5539f..d20b55ba 100644 --- a/src/config/openclawProviderPresets.ts +++ b/src/config/openclawProviderPresets.ts @@ -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", diff --git a/src/hooks/useProxyStatus.ts b/src/hooks/useProxyStatus.ts index e2cf3c36..aa6d4a08 100644 --- a/src/hooks/useProxyStatus.ts +++ b/src/hooks/useProxyStatus.ts @@ -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 }, diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index b840ed50..a1cbf340 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -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", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index abcf6044..56616802 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -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": "ツール名またはパターン", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 7fea24d7..ed7ad8be 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -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": "工具名称或模式", diff --git a/tests/hooks/useProxyStatus.test.tsx b/tests/hooks/useProxyStatus.test.tsx new file mode 100644 index 00000000..ce18e4a0 --- /dev/null +++ b/tests/hooks/useProxyStatus.test.tsx @@ -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) => { + 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) => ( + {children} + ); + + 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 }, + ); + }); +}); \ No newline at end of file