mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-06-16 13:38:35 +08:00
feat: overhaul OpenClaw config panels with JSON5 round-trip write engine
- Add json-five crate for JSON5 serialization preserving comments and formatting - Rewrite openclaw_config.rs with comment-preserving JSON5 read/write engine - Add Tauri commands: get_openclaw_live_provider, write_openclaw_config_section - Redesign EnvPanel as full JSON editor with structured error handling - Add tools.profile selection (minimal/coding/messaging/full) to ToolsPanel - Add legacy timeout migration support to AgentsDefaultsPanel - Add OpenClawHealthBanner component for config validation warnings - Add supporting hooks, mutations, utility functions, and unit tests
This commit is contained in:
Generated
+17
@@ -714,6 +714,7 @@ dependencies = [
|
||||
"futures",
|
||||
"hyper",
|
||||
"indexmap 2.11.4",
|
||||
"json-five",
|
||||
"json5",
|
||||
"log",
|
||||
"objc2 0.5.2",
|
||||
@@ -2510,6 +2511,16 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "json-five"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "865f2d01a4549c1fd8c60640c03ae5249eb374cd8cde8b905628d4b1af95c87c"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"unicode-general-category",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "json-patch"
|
||||
version = "3.0.1"
|
||||
@@ -6001,6 +6012,12 @@ dependencies = [
|
||||
"unic-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-general-category"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.19"
|
||||
|
||||
@@ -63,6 +63,7 @@ rust_decimal = "1.33"
|
||||
uuid = { version = "1.11", features = ["v4"] }
|
||||
sha2 = "0.10"
|
||||
json5 = "0.4"
|
||||
json-five = "0.3.1"
|
||||
|
||||
[target.'cfg(any(target_os = "macos", target_os = "windows", target_os = "linux"))'.dependencies]
|
||||
tauri-plugin-single-instance = "2"
|
||||
|
||||
@@ -26,6 +26,21 @@ pub fn get_openclaw_live_provider_ids() -> Result<Vec<String>, String> {
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Get a single OpenClaw provider fragment from live config.
|
||||
#[tauri::command]
|
||||
pub fn get_openclaw_live_provider(
|
||||
#[allow(non_snake_case)] providerId: String,
|
||||
) -> Result<Option<serde_json::Value>, String> {
|
||||
openclaw_config::get_provider(&providerId).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Scan openclaw.json for known configuration hazards.
|
||||
#[tauri::command]
|
||||
pub fn scan_openclaw_config_health(
|
||||
) -> Result<Vec<openclaw_config::OpenClawHealthWarning>, String> {
|
||||
openclaw_config::scan_openclaw_config_health().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Agents Configuration Commands
|
||||
// ============================================================================
|
||||
@@ -41,7 +56,7 @@ pub fn get_openclaw_default_model() -> Result<Option<openclaw_config::OpenClawDe
|
||||
#[tauri::command]
|
||||
pub fn set_openclaw_default_model(
|
||||
model: openclaw_config::OpenClawDefaultModel,
|
||||
) -> Result<(), String> {
|
||||
) -> Result<openclaw_config::OpenClawWriteOutcome, String> {
|
||||
openclaw_config::set_default_model(&model).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
@@ -56,7 +71,7 @@ pub fn get_openclaw_model_catalog(
|
||||
#[tauri::command]
|
||||
pub fn set_openclaw_model_catalog(
|
||||
catalog: HashMap<String, openclaw_config::OpenClawModelCatalogEntry>,
|
||||
) -> Result<(), String> {
|
||||
) -> Result<openclaw_config::OpenClawWriteOutcome, String> {
|
||||
openclaw_config::set_model_catalog(&catalog).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
@@ -71,7 +86,7 @@ pub fn get_openclaw_agents_defaults(
|
||||
#[tauri::command]
|
||||
pub fn set_openclaw_agents_defaults(
|
||||
defaults: openclaw_config::OpenClawAgentsDefaults,
|
||||
) -> Result<(), String> {
|
||||
) -> Result<openclaw_config::OpenClawWriteOutcome, String> {
|
||||
openclaw_config::set_agents_defaults(&defaults).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
@@ -87,7 +102,9 @@ pub fn get_openclaw_env() -> Result<openclaw_config::OpenClawEnvConfig, String>
|
||||
|
||||
/// Set OpenClaw env config (env section of openclaw.json)
|
||||
#[tauri::command]
|
||||
pub fn set_openclaw_env(env: openclaw_config::OpenClawEnvConfig) -> Result<(), String> {
|
||||
pub fn set_openclaw_env(
|
||||
env: openclaw_config::OpenClawEnvConfig,
|
||||
) -> Result<openclaw_config::OpenClawWriteOutcome, String> {
|
||||
openclaw_config::set_env_config(&env).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
@@ -103,6 +120,8 @@ pub fn get_openclaw_tools() -> Result<openclaw_config::OpenClawToolsConfig, Stri
|
||||
|
||||
/// Set OpenClaw tools config (tools section of openclaw.json)
|
||||
#[tauri::command]
|
||||
pub fn set_openclaw_tools(tools: openclaw_config::OpenClawToolsConfig) -> Result<(), String> {
|
||||
pub fn set_openclaw_tools(
|
||||
tools: openclaw_config::OpenClawToolsConfig,
|
||||
) -> Result<openclaw_config::OpenClawWriteOutcome, String> {
|
||||
openclaw_config::set_tools_config(&tools).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
@@ -1054,6 +1054,8 @@ pub fn run() {
|
||||
// OpenClaw specific
|
||||
commands::import_openclaw_providers_from_live,
|
||||
commands::get_openclaw_live_provider_ids,
|
||||
commands::get_openclaw_live_provider,
|
||||
commands::scan_openclaw_config_health,
|
||||
commands::get_openclaw_default_model,
|
||||
commands::set_openclaw_default_model,
|
||||
commands::get_openclaw_model_catalog,
|
||||
|
||||
+659
-211
File diff suppressed because it is too large
Load Diff
+19
-1
@@ -33,7 +33,7 @@ import {
|
||||
} from "@/lib/api";
|
||||
import { checkAllEnvConflicts, checkEnvConflicts } from "@/lib/api/env";
|
||||
import { useProviderActions } from "@/hooks/useProviderActions";
|
||||
import { openclawKeys } from "@/hooks/useOpenClaw";
|
||||
import { openclawKeys, useOpenClawHealth } from "@/hooks/useOpenClaw";
|
||||
import { useProxyStatus } from "@/hooks/useProxyStatus";
|
||||
import { useAutoCompact } from "@/hooks/useAutoCompact";
|
||||
import { useLastValidValue } from "@/hooks/useLastValidValue";
|
||||
@@ -70,6 +70,7 @@ import WorkspaceFilesPanel from "@/components/workspace/WorkspaceFilesPanel";
|
||||
import EnvPanel from "@/components/openclaw/EnvPanel";
|
||||
import ToolsPanel from "@/components/openclaw/ToolsPanel";
|
||||
import AgentsDefaultsPanel from "@/components/openclaw/AgentsDefaultsPanel";
|
||||
import OpenClawHealthBanner from "@/components/openclaw/OpenClawHealthBanner";
|
||||
|
||||
type View =
|
||||
| "providers"
|
||||
@@ -229,6 +230,17 @@ function App() {
|
||||
});
|
||||
const providers = useMemo(() => data?.providers ?? {}, [data]);
|
||||
const currentProviderId = data?.currentProviderId ?? "";
|
||||
const isOpenClawView =
|
||||
activeApp === "openclaw" &&
|
||||
(currentView === "providers" ||
|
||||
currentView === "workspace" ||
|
||||
currentView === "sessions" ||
|
||||
currentView === "openclawEnv" ||
|
||||
currentView === "openclawTools" ||
|
||||
currentView === "openclawAgents");
|
||||
const { data: openclawHealthWarnings = [] } = useOpenClawHealth(
|
||||
isOpenClawView,
|
||||
);
|
||||
const hasSkillsSupport = true;
|
||||
const hasSessionSupport =
|
||||
activeApp === "claude" ||
|
||||
@@ -544,6 +556,9 @@ function App() {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: openclawKeys.liveProviderIds,
|
||||
});
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: openclawKeys.health,
|
||||
});
|
||||
}
|
||||
toast.success(
|
||||
t("notifications.removeFromConfigSuccess", {
|
||||
@@ -1225,6 +1240,9 @@ function App() {
|
||||
</header>
|
||||
|
||||
<main className="flex-1 min-h-0 flex flex-col overflow-y-auto animate-fade-in">
|
||||
{isOpenClawView && openclawHealthWarnings.length > 0 && (
|
||||
<OpenClawHealthBanner warnings={openclawHealthWarnings} />
|
||||
)}
|
||||
{renderContent()}
|
||||
</main>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Save, Plus, Trash2 } from "lucide-react";
|
||||
import { Save, Plus, Trash2, TriangleAlert } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
useOpenClawAgentsDefaults,
|
||||
@@ -10,6 +10,7 @@ import { extractErrorMessage } from "@/utils/errorUtils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import type { OpenClawAgentsDefaults } from "@/types";
|
||||
import { useOpenClawModelOptions } from "./hooks/useOpenClawModelOptions";
|
||||
import { getOpenClawTimeoutInputValue } from "./utils";
|
||||
|
||||
const UNSET_SENTINEL = "__unset__";
|
||||
|
||||
@@ -50,9 +52,16 @@ const AgentsDefaultsPanel: React.FC = () => {
|
||||
|
||||
// Extract known extra fields
|
||||
setWorkspace(String(agentsData.workspace ?? ""));
|
||||
setTimeout_(String(agentsData.timeout ?? ""));
|
||||
setTimeout_(getOpenClawTimeoutInputValue(agentsData));
|
||||
setContextTokens(String(agentsData.contextTokens ?? ""));
|
||||
setMaxConcurrent(String(agentsData.maxConcurrent ?? ""));
|
||||
} else {
|
||||
setPrimaryModel("");
|
||||
setFallbacks([]);
|
||||
setWorkspace("");
|
||||
setTimeout_("");
|
||||
setContextTokens("");
|
||||
setMaxConcurrent("");
|
||||
}
|
||||
}, [agentsData]);
|
||||
|
||||
@@ -146,8 +155,9 @@ const AgentsDefaultsPanel: React.FC = () => {
|
||||
};
|
||||
|
||||
const timeoutNum = timeout.trim() ? parseNum(timeout) : undefined;
|
||||
if (timeoutNum !== undefined) updated.timeout = timeoutNum;
|
||||
else delete updated.timeout;
|
||||
if (timeoutNum !== undefined) updated.timeoutSeconds = timeoutNum;
|
||||
else delete updated.timeoutSeconds;
|
||||
delete updated.timeout;
|
||||
|
||||
const ctxNum = contextTokens.trim() ? parseNum(contextTokens) : undefined;
|
||||
if (ctxNum !== undefined) updated.contextTokens = ctxNum;
|
||||
@@ -159,8 +169,15 @@ const AgentsDefaultsPanel: React.FC = () => {
|
||||
if (concNum !== undefined) updated.maxConcurrent = concNum;
|
||||
else delete updated.maxConcurrent;
|
||||
|
||||
await saveAgentsMutation.mutateAsync(updated);
|
||||
toast.success(t("openclaw.agents.saveSuccess"));
|
||||
const outcome = await saveAgentsMutation.mutateAsync(updated);
|
||||
toast.success(t("openclaw.agents.saveSuccess"), {
|
||||
description: outcome.backupPath
|
||||
? t("openclaw.backupCreated", {
|
||||
path: outcome.backupPath,
|
||||
defaultValue: "Backup created: {{path}}",
|
||||
})
|
||||
: undefined,
|
||||
});
|
||||
} catch (error) {
|
||||
const detail = extractErrorMessage(error);
|
||||
toast.error(t("openclaw.agents.saveFailed"), {
|
||||
@@ -180,6 +197,11 @@ const AgentsDefaultsPanel: React.FC = () => {
|
||||
}
|
||||
|
||||
const noModels = modelOptions.length === 0 && !modelsLoading;
|
||||
const hasLegacyTimeout =
|
||||
agentsData !== undefined &&
|
||||
agentsData !== null &&
|
||||
typeof agentsData.timeout === "number" &&
|
||||
typeof agentsData.timeoutSeconds !== "number";
|
||||
|
||||
return (
|
||||
<div className="px-6 pt-4 pb-8">
|
||||
@@ -187,6 +209,23 @@ const AgentsDefaultsPanel: React.FC = () => {
|
||||
{t("openclaw.agents.description")}
|
||||
</p>
|
||||
|
||||
{hasLegacyTimeout && (
|
||||
<Alert className="mb-4 border-amber-500/30 bg-amber-500/5">
|
||||
<TriangleAlert className="h-4 w-4" />
|
||||
<AlertTitle>
|
||||
{t("openclaw.agents.legacyTimeoutTitle", {
|
||||
defaultValue: "Legacy timeout detected",
|
||||
})}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t("openclaw.agents.legacyTimeoutDescription", {
|
||||
defaultValue:
|
||||
"This config still uses agents.defaults.timeout. Saving here will migrate it to timeoutSeconds.",
|
||||
})}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Model Configuration Card */}
|
||||
<div className="rounded-xl border border-border bg-card p-5 mb-4">
|
||||
<h3 className="text-sm font-medium mb-4">
|
||||
|
||||
@@ -1,96 +1,77 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Plus, Trash2, Save, Eye, EyeOff } from "lucide-react";
|
||||
import { Save } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { useOpenClawEnv, useSaveOpenClawEnv } from "@/hooks/useOpenClaw";
|
||||
import { extractErrorMessage } from "@/utils/errorUtils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import type { OpenClawEnvConfig } from "@/types";
|
||||
|
||||
interface EnvEntry {
|
||||
id: string;
|
||||
key: string;
|
||||
value: string;
|
||||
isNew?: boolean;
|
||||
}
|
||||
import JsonEditor from "@/components/JsonEditor";
|
||||
import { parseOpenClawEnvEditorValue } from "./utils";
|
||||
|
||||
const EnvPanel: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { data: envData, isLoading } = useOpenClawEnv();
|
||||
const saveEnvMutation = useSaveOpenClawEnv();
|
||||
const [entries, setEntries] = useState<EnvEntry[]>([]);
|
||||
const [visibleKeys, setVisibleKeys] = useState<Set<string>>(new Set());
|
||||
const [editorValue, setEditorValue] = useState("{}");
|
||||
const [isDarkMode, setIsDarkMode] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (envData) {
|
||||
const items: EnvEntry[] = Object.entries(envData).map(([key, value]) => ({
|
||||
id: crypto.randomUUID(),
|
||||
key,
|
||||
value: String(value ?? ""),
|
||||
}));
|
||||
setEntries(items.length > 0 ? items : []);
|
||||
}
|
||||
const nextValue =
|
||||
envData && Object.keys(envData).length > 0
|
||||
? JSON.stringify(envData, null, 2)
|
||||
: "{}";
|
||||
setEditorValue(nextValue);
|
||||
}, [envData]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsDarkMode(document.documentElement.classList.contains("dark"));
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
setIsDarkMode(document.documentElement.classList.contains("dark"));
|
||||
});
|
||||
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ["class"],
|
||||
});
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
const env: OpenClawEnvConfig = {};
|
||||
const seen = new Set<string>();
|
||||
for (const entry of entries) {
|
||||
const trimmedKey = entry.key.trim();
|
||||
if (trimmedKey) {
|
||||
if (seen.has(trimmedKey)) {
|
||||
toast.error(t("openclaw.env.duplicateKey", { key: trimmedKey }));
|
||||
return;
|
||||
}
|
||||
seen.add(trimmedKey);
|
||||
env[trimmedKey] = entry.value;
|
||||
}
|
||||
}
|
||||
await saveEnvMutation.mutateAsync(env);
|
||||
toast.success(t("openclaw.env.saveSuccess"));
|
||||
const env = parseOpenClawEnvEditorValue(editorValue);
|
||||
const outcome = await saveEnvMutation.mutateAsync(env);
|
||||
toast.success(t("openclaw.env.saveSuccess"), {
|
||||
description: outcome.backupPath
|
||||
? t("openclaw.backupCreated", {
|
||||
path: outcome.backupPath,
|
||||
defaultValue: "Backup created: {{path}}",
|
||||
})
|
||||
: undefined,
|
||||
});
|
||||
} catch (error) {
|
||||
const detail = extractErrorMessage(error);
|
||||
let description = detail || undefined;
|
||||
if (detail === "OPENCLAW_ENV_EMPTY") {
|
||||
description = t("openclaw.env.empty", {
|
||||
defaultValue: "OpenClaw env cannot be empty. Use {} for an empty object.",
|
||||
});
|
||||
} else if (detail === "OPENCLAW_ENV_INVALID_JSON") {
|
||||
description = t("openclaw.env.invalidJson", {
|
||||
defaultValue: "OpenClaw env must be valid JSON.",
|
||||
});
|
||||
} else if (detail === "OPENCLAW_ENV_OBJECT_REQUIRED") {
|
||||
description = t("openclaw.env.objectRequired", {
|
||||
defaultValue: "OpenClaw env must be a JSON object.",
|
||||
});
|
||||
}
|
||||
toast.error(t("openclaw.env.saveFailed"), {
|
||||
description: detail || undefined,
|
||||
description,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const addEntry = () => {
|
||||
setEntries((prev) => [
|
||||
...prev,
|
||||
{ id: crypto.randomUUID(), key: "", value: "", isNew: true },
|
||||
]);
|
||||
};
|
||||
|
||||
const removeEntry = (index: number) => {
|
||||
setEntries((prev) => prev.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const updateEntry = (index: number, field: "key" | "value", val: string) => {
|
||||
setEntries((prev) =>
|
||||
prev.map((entry, i) =>
|
||||
i === index ? { ...entry, [field]: val } : entry,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const toggleVisibility = (key: string) => {
|
||||
setVisibleKeys((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(key)) {
|
||||
next.delete(key);
|
||||
} else {
|
||||
next.add(key);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const isApiKey = (key: string) => /key|token|secret|password/i.test(key);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="px-6 pt-4 pb-8 flex items-center justify-center min-h-[200px]">
|
||||
@@ -106,66 +87,23 @@ const EnvPanel: React.FC = () => {
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
{t("openclaw.env.description")}
|
||||
</p>
|
||||
|
||||
<div className="space-y-3">
|
||||
{entries.map((entry, index) => {
|
||||
const sensitive = isApiKey(entry.key);
|
||||
const visibilityId = entry.key || `__new_${index}`;
|
||||
const visible = visibleKeys.has(visibilityId);
|
||||
|
||||
return (
|
||||
<div key={entry.id} className="flex items-center gap-2">
|
||||
<div className="w-[200px] flex-shrink-0">
|
||||
<Input
|
||||
value={entry.key}
|
||||
onChange={(e) => updateEntry(index, "key", e.target.value)}
|
||||
placeholder={t("openclaw.env.keyPlaceholder")}
|
||||
className="font-mono text-xs"
|
||||
autoFocus={entry.isNew}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 flex items-center gap-1">
|
||||
<Input
|
||||
type={sensitive && !visible ? "password" : "text"}
|
||||
value={entry.value}
|
||||
onChange={(e) => updateEntry(index, "value", e.target.value)}
|
||||
placeholder={t("openclaw.env.valuePlaceholder")}
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
{sensitive && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="flex-shrink-0 h-9 w-9 text-muted-foreground"
|
||||
onClick={() => toggleVisibility(visibilityId)}
|
||||
>
|
||||
{visible ? (
|
||||
<EyeOff className="w-4 h-4" />
|
||||
) : (
|
||||
<Eye className="w-4 h-4" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="flex-shrink-0 h-9 w-9 text-muted-foreground hover:text-destructive"
|
||||
onClick={() => removeEntry(index)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
<p className="text-xs text-muted-foreground mb-4">
|
||||
{t("openclaw.env.editorHint", {
|
||||
defaultValue:
|
||||
"Edit the full env section as JSON. Nested objects such as env.vars and env.shellEnv are supported.",
|
||||
})}
|
||||
</div>
|
||||
</p>
|
||||
|
||||
<div className="flex items-center gap-2 mt-4">
|
||||
<Button variant="outline" size="sm" onClick={addEntry}>
|
||||
<Plus className="w-4 h-4 mr-1" />
|
||||
{t("openclaw.env.add")}
|
||||
</Button>
|
||||
<div className="flex-1" />
|
||||
<JsonEditor
|
||||
value={editorValue}
|
||||
onChange={setEditorValue}
|
||||
darkMode={isDarkMode}
|
||||
rows={18}
|
||||
showValidation={true}
|
||||
language="json"
|
||||
/>
|
||||
|
||||
<div className="flex justify-end mt-4">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleSave}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import React, { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TriangleAlert } from "lucide-react";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import type { OpenClawHealthWarning } from "@/types";
|
||||
|
||||
interface OpenClawHealthBannerProps {
|
||||
warnings: OpenClawHealthWarning[];
|
||||
}
|
||||
|
||||
function getWarningText(
|
||||
code: string,
|
||||
fallback: string,
|
||||
t: ReturnType<typeof useTranslation>["t"],
|
||||
) {
|
||||
switch (code) {
|
||||
case "invalid_tools_profile":
|
||||
return t("openclaw.health.invalidToolsProfile", {
|
||||
defaultValue:
|
||||
"tools.profile contains an unsupported value. OpenClaw currently expects minimal, coding, messaging, or full.",
|
||||
});
|
||||
case "legacy_agents_timeout":
|
||||
return t("openclaw.health.legacyTimeout", {
|
||||
defaultValue:
|
||||
"agents.defaults.timeout is deprecated. Save the Agents panel to migrate it to timeoutSeconds.",
|
||||
});
|
||||
case "stringified_env_vars":
|
||||
return t("openclaw.health.stringifiedEnvVars", {
|
||||
defaultValue:
|
||||
"env.vars should be an object, but the current value looks stringified or malformed.",
|
||||
});
|
||||
case "stringified_env_shell_env":
|
||||
return t("openclaw.health.stringifiedShellEnv", {
|
||||
defaultValue:
|
||||
"env.shellEnv should be an object, but the current value looks stringified or malformed.",
|
||||
});
|
||||
case "config_parse_failed":
|
||||
return t("openclaw.health.parseFailed", {
|
||||
defaultValue:
|
||||
"openclaw.json could not be parsed as valid JSON5. Fix the file before editing it here.",
|
||||
});
|
||||
default:
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
const OpenClawHealthBanner: React.FC<OpenClawHealthBannerProps> = ({
|
||||
warnings,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const items = useMemo(
|
||||
() =>
|
||||
warnings.map((warning) => ({
|
||||
...warning,
|
||||
text: getWarningText(warning.code, warning.message, t),
|
||||
})),
|
||||
[t, warnings],
|
||||
);
|
||||
|
||||
if (warnings.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="px-6 pt-4">
|
||||
<Alert className="border-amber-500/30 bg-amber-500/5">
|
||||
<TriangleAlert className="h-4 w-4" />
|
||||
<AlertTitle>
|
||||
{t("openclaw.health.title", {
|
||||
defaultValue: "OpenClaw config warnings detected",
|
||||
})}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
<ul className="list-disc space-y-1 pl-5">
|
||||
{items.map((warning) => (
|
||||
<li key={`${warning.code}:${warning.path ?? warning.message}`}>
|
||||
{warning.text}
|
||||
{warning.path ? ` (${warning.path})` : ""}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OpenClawHealthBanner;
|
||||
@@ -1,12 +1,13 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Plus, Trash2, Save } from "lucide-react";
|
||||
import { Plus, Trash2, Save, TriangleAlert } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { useOpenClawTools, useSaveOpenClawTools } from "@/hooks/useOpenClaw";
|
||||
import { extractErrorMessage } from "@/utils/errorUtils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -14,15 +15,20 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import type { OpenClawToolsConfig } from "@/types";
|
||||
import type { OpenClawToolsConfig, OpenClawToolsProfile } from "@/types";
|
||||
import {
|
||||
getOpenClawToolsProfileSelectValue,
|
||||
getOpenClawUnsupportedProfile,
|
||||
OPENCLAW_TOOL_PROFILES,
|
||||
OPENCLAW_UNSET_PROFILE,
|
||||
OPENCLAW_UNSUPPORTED_PROFILE,
|
||||
} from "./utils";
|
||||
|
||||
interface ListItem {
|
||||
id: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const PROFILE_OPTIONS = ["default", "strict", "permissive", "custom"];
|
||||
|
||||
const ToolsPanel: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { data: toolsData, isLoading } = useOpenClawTools();
|
||||
@@ -35,31 +41,59 @@ const ToolsPanel: React.FC = () => {
|
||||
if (toolsData) {
|
||||
setConfig(toolsData);
|
||||
setAllowList(
|
||||
(toolsData.allow ?? []).map((v) => ({
|
||||
(toolsData.allow ?? []).map((value) => ({
|
||||
id: crypto.randomUUID(),
|
||||
value: v,
|
||||
value,
|
||||
})),
|
||||
);
|
||||
setDenyList(
|
||||
(toolsData.deny ?? []).map((v) => ({
|
||||
(toolsData.deny ?? []).map((value) => ({
|
||||
id: crypto.randomUUID(),
|
||||
value: v,
|
||||
value,
|
||||
})),
|
||||
);
|
||||
}
|
||||
}, [toolsData]);
|
||||
|
||||
const unsupportedProfile = getOpenClawUnsupportedProfile(config.profile);
|
||||
|
||||
const profileLabels = useMemo<Record<OpenClawToolsProfile, string>>(
|
||||
() => ({
|
||||
minimal: t("openclaw.tools.profileMinimal", {
|
||||
defaultValue: "Minimal",
|
||||
}),
|
||||
coding: t("openclaw.tools.profileCoding", {
|
||||
defaultValue: "Coding",
|
||||
}),
|
||||
messaging: t("openclaw.tools.profileMessaging", {
|
||||
defaultValue: "Messaging",
|
||||
}),
|
||||
full: t("openclaw.tools.profileFull", {
|
||||
defaultValue: "Full",
|
||||
}),
|
||||
}),
|
||||
[t],
|
||||
);
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
const { profile, allow, deny, ...other } = config;
|
||||
const { allow, deny, ...other } = config;
|
||||
const newConfig: OpenClawToolsConfig = {
|
||||
...other,
|
||||
profile: config.profile,
|
||||
allow: allowList.map((item) => item.value).filter((s) => s.trim()),
|
||||
deny: denyList.map((item) => item.value).filter((s) => s.trim()),
|
||||
};
|
||||
await saveToolsMutation.mutateAsync(newConfig);
|
||||
toast.success(t("openclaw.tools.saveSuccess"));
|
||||
|
||||
const outcome = await saveToolsMutation.mutateAsync(newConfig);
|
||||
toast.success(t("openclaw.tools.saveSuccess"), {
|
||||
description: outcome.backupPath
|
||||
? t("openclaw.backupCreated", {
|
||||
path: outcome.backupPath,
|
||||
defaultValue: "Backup created: {{path}}",
|
||||
})
|
||||
: undefined,
|
||||
});
|
||||
} catch (error) {
|
||||
const detail = extractErrorMessage(error);
|
||||
toast.error(t("openclaw.tools.saveFailed"), {
|
||||
@@ -101,29 +135,63 @@ const ToolsPanel: React.FC = () => {
|
||||
{t("openclaw.tools.description")}
|
||||
</p>
|
||||
|
||||
{/* Profile selector */}
|
||||
{unsupportedProfile && (
|
||||
<Alert className="mb-6 border-amber-500/30 bg-amber-500/5">
|
||||
<TriangleAlert className="h-4 w-4" />
|
||||
<AlertTitle>
|
||||
{t("openclaw.tools.unsupportedProfileTitle", {
|
||||
defaultValue: "Unsupported tools profile",
|
||||
})}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t("openclaw.tools.unsupportedProfileDescription", {
|
||||
value: unsupportedProfile,
|
||||
defaultValue:
|
||||
"The current tools.profile value '{{value}}' is not in the supported OpenClaw list. It will be preserved until you choose a new value.",
|
||||
})}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="mb-6">
|
||||
<Label className="mb-2 block">{t("openclaw.tools.profile")}</Label>
|
||||
<Select
|
||||
value={config.profile ?? "default"}
|
||||
onValueChange={(val) =>
|
||||
setConfig((prev) => ({ ...prev, profile: val }))
|
||||
}
|
||||
value={getOpenClawToolsProfileSelectValue(config.profile)}
|
||||
onValueChange={(value) => {
|
||||
if (value === OPENCLAW_UNSUPPORTED_PROFILE) return;
|
||||
if (value === OPENCLAW_UNSET_PROFILE) {
|
||||
setConfig((prev) => ({ ...prev, profile: undefined }));
|
||||
return;
|
||||
}
|
||||
setConfig((prev) => ({ ...prev, profile: value }));
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-[200px]">
|
||||
<SelectTrigger className="w-[220px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{PROFILE_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt} value={opt}>
|
||||
{t(`openclaw.tools.profiles.${opt}`)}
|
||||
<SelectItem value={OPENCLAW_UNSET_PROFILE}>
|
||||
{t("openclaw.tools.profileUnset", {
|
||||
defaultValue: "Not set",
|
||||
})}
|
||||
</SelectItem>
|
||||
{unsupportedProfile && (
|
||||
<SelectItem
|
||||
value={OPENCLAW_UNSUPPORTED_PROFILE}
|
||||
disabled={true}
|
||||
>{`${unsupportedProfile} (${t("openclaw.tools.unsupportedProfileLabel", {
|
||||
defaultValue: "unsupported",
|
||||
})})`}</SelectItem>
|
||||
)}
|
||||
{OPENCLAW_TOOL_PROFILES.map((profile) => (
|
||||
<SelectItem key={profile} value={profile}>
|
||||
{profileLabels[profile]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Allow list */}
|
||||
<div className="mb-6">
|
||||
<Label className="mb-2 block">{t("openclaw.tools.allowList")}</Label>
|
||||
<div className="space-y-2">
|
||||
@@ -163,7 +231,6 @@ const ToolsPanel: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Deny list */}
|
||||
<div className="mb-6">
|
||||
<Label className="mb-2 block">{t("openclaw.tools.denyList")}</Label>
|
||||
<div className="space-y-2">
|
||||
@@ -203,7 +270,6 @@ const ToolsPanel: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Save button */}
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
size="sm"
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import type {
|
||||
OpenClawAgentsDefaults,
|
||||
OpenClawEnvConfig,
|
||||
OpenClawToolsProfile,
|
||||
} from "@/types";
|
||||
|
||||
export const OPENCLAW_TOOL_PROFILES: OpenClawToolsProfile[] = [
|
||||
"minimal",
|
||||
"coding",
|
||||
"messaging",
|
||||
"full",
|
||||
];
|
||||
|
||||
export const OPENCLAW_UNSUPPORTED_PROFILE = "__unsupported_profile__";
|
||||
export const OPENCLAW_UNSET_PROFILE = "__unset_profile__";
|
||||
|
||||
export function parseOpenClawEnvEditorValue(raw: string): OpenClawEnvConfig {
|
||||
if (!raw.trim()) {
|
||||
throw new Error("OPENCLAW_ENV_EMPTY");
|
||||
}
|
||||
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(raw);
|
||||
} catch {
|
||||
throw new Error("OPENCLAW_ENV_INVALID_JSON");
|
||||
}
|
||||
|
||||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
throw new Error("OPENCLAW_ENV_OBJECT_REQUIRED");
|
||||
}
|
||||
return parsed as OpenClawEnvConfig;
|
||||
}
|
||||
|
||||
export function isOpenClawToolsProfile(
|
||||
profile?: string,
|
||||
): profile is OpenClawToolsProfile {
|
||||
return (
|
||||
typeof profile === "string" &&
|
||||
OPENCLAW_TOOL_PROFILES.includes(profile as OpenClawToolsProfile)
|
||||
);
|
||||
}
|
||||
|
||||
export function getOpenClawToolsProfileSelectValue(profile?: string): string {
|
||||
if (!profile) {
|
||||
return OPENCLAW_UNSET_PROFILE;
|
||||
}
|
||||
return isOpenClawToolsProfile(profile)
|
||||
? profile
|
||||
: OPENCLAW_UNSUPPORTED_PROFILE;
|
||||
}
|
||||
|
||||
export function getOpenClawUnsupportedProfile(profile?: string): string | null {
|
||||
if (!profile || isOpenClawToolsProfile(profile)) {
|
||||
return null;
|
||||
}
|
||||
return profile;
|
||||
}
|
||||
|
||||
export function getOpenClawTimeoutInputValue(
|
||||
defaults?: OpenClawAgentsDefaults | null,
|
||||
): string {
|
||||
const timeoutSeconds =
|
||||
typeof defaults?.timeoutSeconds === "number"
|
||||
? defaults.timeoutSeconds
|
||||
: undefined;
|
||||
const legacyTimeout =
|
||||
typeof defaults?.timeout === "number" ? defaults.timeout : undefined;
|
||||
const value = timeoutSeconds ?? legacyTimeout;
|
||||
return value === undefined ? "" : String(value);
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
ProviderForm,
|
||||
type ProviderFormValues,
|
||||
} from "@/components/providers/forms/ProviderForm";
|
||||
import { providersApi, vscodeApi, type AppId } from "@/lib/api";
|
||||
import { openclawApi, providersApi, vscodeApi, type AppId } from "@/lib/api";
|
||||
|
||||
interface EditProviderDialogProps {
|
||||
open: boolean;
|
||||
@@ -73,6 +73,26 @@ export function EditProviderDialog({
|
||||
return;
|
||||
}
|
||||
|
||||
if (appId === "openclaw") {
|
||||
try {
|
||||
const live = await openclawApi.getLiveProvider(provider.id);
|
||||
if (!cancelled && live && typeof live === "object") {
|
||||
setLiveSettings(live);
|
||||
} else if (!cancelled) {
|
||||
setLiveSettings(null);
|
||||
}
|
||||
} catch {
|
||||
if (!cancelled) {
|
||||
setLiveSettings(null);
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setHasLoadedLive(true);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const currentId = await providersApi.getCurrent(appId);
|
||||
if (currentId && provider.id === currentId) {
|
||||
|
||||
@@ -18,6 +18,7 @@ export const openclawKeys = {
|
||||
env: ["openclaw", "env"] as const,
|
||||
tools: ["openclaw", "tools"] as const,
|
||||
agentsDefaults: ["openclaw", "agentsDefaults"] as const,
|
||||
health: ["openclaw", "health"] as const,
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
@@ -81,6 +82,15 @@ export function useOpenClawAgentsDefaults() {
|
||||
});
|
||||
}
|
||||
|
||||
export function useOpenClawHealth(enabled: boolean) {
|
||||
return useQuery({
|
||||
queryKey: openclawKeys.health,
|
||||
queryFn: () => openclawApi.scanHealth(),
|
||||
staleTime: 30_000,
|
||||
enabled,
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Mutation hooks
|
||||
// ============================================================
|
||||
@@ -95,6 +105,7 @@ export function useSaveOpenClawEnv() {
|
||||
mutationFn: (env: OpenClawEnvConfig) => openclawApi.setEnv(env),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: openclawKeys.env });
|
||||
queryClient.invalidateQueries({ queryKey: openclawKeys.health });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -109,6 +120,7 @@ export function useSaveOpenClawTools() {
|
||||
mutationFn: (tools: OpenClawToolsConfig) => openclawApi.setTools(tools),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: openclawKeys.tools });
|
||||
queryClient.invalidateQueries({ queryKey: openclawKeys.health });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -126,6 +138,7 @@ export function useSaveOpenClawAgentsDefaults() {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: openclawKeys.agentsDefaults });
|
||||
queryClient.invalidateQueries({ queryKey: openclawKeys.defaultModel });
|
||||
queryClient.invalidateQueries({ queryKey: openclawKeys.health });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -80,6 +80,9 @@ export function useProviderActions(activeApp: AppId) {
|
||||
const existingCatalog = (await openclawApi.getModelCatalog()) || {};
|
||||
const mergedCatalog = { ...existingCatalog, ...modelCatalog };
|
||||
await openclawApi.setModelCatalog(mergedCatalog);
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: openclawKeys.health,
|
||||
});
|
||||
modelsRegistered = true;
|
||||
}
|
||||
|
||||
@@ -88,6 +91,9 @@ export function useProviderActions(activeApp: AppId) {
|
||||
const existingDefault = await openclawApi.getDefaultModel();
|
||||
if (!existingDefault?.primary) {
|
||||
await openclawApi.setDefaultModel(model);
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: openclawKeys.health,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +115,7 @@ export function useProviderActions(activeApp: AppId) {
|
||||
}
|
||||
}
|
||||
},
|
||||
[addProviderMutation, activeApp, t],
|
||||
[addProviderMutation, activeApp, queryClient, t],
|
||||
);
|
||||
|
||||
// 更新供应商
|
||||
@@ -255,15 +261,26 @@ export function useProviderActions(activeApp: AppId) {
|
||||
};
|
||||
|
||||
try {
|
||||
await openclawApi.setDefaultModel(model);
|
||||
const outcome = await openclawApi.setDefaultModel(model);
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: openclawKeys.defaultModel,
|
||||
});
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: openclawKeys.health,
|
||||
});
|
||||
toast.success(
|
||||
t("notifications.openclawDefaultModelSet", {
|
||||
defaultValue: "已设为默认模型",
|
||||
}),
|
||||
{ closeButton: true },
|
||||
{
|
||||
closeButton: true,
|
||||
description: outcome.backupPath
|
||||
? t("openclaw.backupCreated", {
|
||||
path: outcome.backupPath,
|
||||
defaultValue: "Backup created: {{path}}",
|
||||
})
|
||||
: undefined,
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
const detail =
|
||||
|
||||
+19
-5
@@ -5,6 +5,8 @@ import type {
|
||||
OpenClawAgentsDefaults,
|
||||
OpenClawEnvConfig,
|
||||
OpenClawToolsConfig,
|
||||
OpenClawHealthWarning,
|
||||
OpenClawWriteOutcome,
|
||||
} from "@/types";
|
||||
|
||||
/**
|
||||
@@ -30,7 +32,7 @@ export const openclawApi = {
|
||||
/**
|
||||
* Set default model configuration (agents.defaults.model)
|
||||
*/
|
||||
async setDefaultModel(model: OpenClawDefaultModel): Promise<void> {
|
||||
async setDefaultModel(model: OpenClawDefaultModel): Promise<OpenClawWriteOutcome> {
|
||||
return await invoke("set_openclaw_default_model", { model });
|
||||
},
|
||||
|
||||
@@ -49,7 +51,7 @@ export const openclawApi = {
|
||||
*/
|
||||
async setModelCatalog(
|
||||
catalog: Record<string, OpenClawModelCatalogEntry>,
|
||||
): Promise<void> {
|
||||
): Promise<OpenClawWriteOutcome> {
|
||||
return await invoke("set_openclaw_model_catalog", { catalog });
|
||||
},
|
||||
|
||||
@@ -63,7 +65,9 @@ export const openclawApi = {
|
||||
/**
|
||||
* Set full agents.defaults config (all fields)
|
||||
*/
|
||||
async setAgentsDefaults(defaults: OpenClawAgentsDefaults): Promise<void> {
|
||||
async setAgentsDefaults(
|
||||
defaults: OpenClawAgentsDefaults,
|
||||
): Promise<OpenClawWriteOutcome> {
|
||||
return await invoke("set_openclaw_agents_defaults", { defaults });
|
||||
},
|
||||
|
||||
@@ -81,7 +85,7 @@ export const openclawApi = {
|
||||
/**
|
||||
* Set env config (env section of openclaw.json)
|
||||
*/
|
||||
async setEnv(env: OpenClawEnvConfig): Promise<void> {
|
||||
async setEnv(env: OpenClawEnvConfig): Promise<OpenClawWriteOutcome> {
|
||||
return await invoke("set_openclaw_env", { env });
|
||||
},
|
||||
|
||||
@@ -99,7 +103,17 @@ export const openclawApi = {
|
||||
/**
|
||||
* Set tools config (tools section of openclaw.json)
|
||||
*/
|
||||
async setTools(tools: OpenClawToolsConfig): Promise<void> {
|
||||
async setTools(tools: OpenClawToolsConfig): Promise<OpenClawWriteOutcome> {
|
||||
return await invoke("set_openclaw_tools", { tools });
|
||||
},
|
||||
|
||||
async scanHealth(): Promise<OpenClawHealthWarning[]> {
|
||||
return await invoke("scan_openclaw_config_health");
|
||||
},
|
||||
|
||||
async getLiveProvider(
|
||||
providerId: string,
|
||||
): Promise<Record<string, unknown> | null> {
|
||||
return await invoke("get_openclaw_live_provider", { providerId });
|
||||
},
|
||||
};
|
||||
|
||||
@@ -65,6 +65,12 @@ export const useAddProviderMutation = (appId: AppId) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (appId === "openclaw") {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: openclawKeys.health,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await providersApi.updateTrayMenu();
|
||||
} catch (trayError) {
|
||||
@@ -106,6 +112,11 @@ export const useUpdateProviderMutation = (appId: AppId) => {
|
||||
},
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["providers", appId] });
|
||||
if (appId === "openclaw") {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: openclawKeys.health,
|
||||
});
|
||||
}
|
||||
toast.success(
|
||||
t("notifications.updateSuccess", {
|
||||
defaultValue: "供应商更新成功",
|
||||
@@ -153,6 +164,12 @@ export const useDeleteProviderMutation = (appId: AppId) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (appId === "openclaw") {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: openclawKeys.health,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await providersApi.updateTrayMenu();
|
||||
} catch (trayError) {
|
||||
@@ -213,6 +230,9 @@ export const useSwitchProviderMutation = (appId: AppId) => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: openclawKeys.defaultModel,
|
||||
});
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: openclawKeys.health,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
+20
-1
@@ -483,6 +483,23 @@ export interface OpenClawModelCatalogEntry {
|
||||
alias?: string;
|
||||
}
|
||||
|
||||
export interface OpenClawHealthWarning {
|
||||
code: string;
|
||||
message: string;
|
||||
path?: string;
|
||||
}
|
||||
|
||||
export interface OpenClawWriteOutcome {
|
||||
backupPath?: string;
|
||||
warnings: OpenClawHealthWarning[];
|
||||
}
|
||||
|
||||
export type OpenClawToolsProfile =
|
||||
| "minimal"
|
||||
| "coding"
|
||||
| "messaging"
|
||||
| "full";
|
||||
|
||||
// OpenClaw 供应商配置(settings_config 结构)
|
||||
// 对应 OpenClaw 的 models.providers.<provider-id> 配置
|
||||
export interface OpenClawProviderConfig {
|
||||
@@ -496,6 +513,8 @@ export interface OpenClawProviderConfig {
|
||||
export interface OpenClawAgentsDefaults {
|
||||
model?: OpenClawDefaultModel;
|
||||
models?: Record<string, OpenClawModelCatalogEntry>;
|
||||
timeoutSeconds?: number;
|
||||
timeout?: number;
|
||||
[key: string]: unknown; // preserve unknown fields
|
||||
}
|
||||
|
||||
@@ -506,7 +525,7 @@ export interface OpenClawEnvConfig {
|
||||
|
||||
// OpenClaw tools 配置(openclaw.json 的 tools 节点)
|
||||
export interface OpenClawToolsConfig {
|
||||
profile?: string;
|
||||
profile?: OpenClawToolsProfile | string;
|
||||
allow?: string[];
|
||||
deny?: string[];
|
||||
[key: string]: unknown; // preserve unknown fields
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
getOpenClawTimeoutInputValue,
|
||||
getOpenClawToolsProfileSelectValue,
|
||||
getOpenClawUnsupportedProfile,
|
||||
OPENCLAW_UNSUPPORTED_PROFILE,
|
||||
parseOpenClawEnvEditorValue,
|
||||
} from "@/components/openclaw/utils";
|
||||
|
||||
describe("OpenClaw utils", () => {
|
||||
it("parses nested env objects without stringifying them", () => {
|
||||
const env = parseOpenClawEnvEditorValue(`{
|
||||
"API_KEY": "secret",
|
||||
"vars": { "HTTP_PROXY": "http://127.0.0.1:8080" },
|
||||
"shellEnv": { "NODE_OPTIONS": "--max-old-space-size=4096" }
|
||||
}`);
|
||||
|
||||
expect(env).toEqual({
|
||||
API_KEY: "secret",
|
||||
vars: { HTTP_PROXY: "http://127.0.0.1:8080" },
|
||||
shellEnv: { NODE_OPTIONS: "--max-old-space-size=4096" },
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects non-object env payloads", () => {
|
||||
expect(() => parseOpenClawEnvEditorValue(`["not", "an object"]`)).toThrow(
|
||||
"OPENCLAW_ENV_OBJECT_REQUIRED",
|
||||
);
|
||||
});
|
||||
|
||||
it("flags unsupported tools profiles without silently normalizing them", () => {
|
||||
expect(getOpenClawToolsProfileSelectValue("default")).toBe(
|
||||
OPENCLAW_UNSUPPORTED_PROFILE,
|
||||
);
|
||||
expect(getOpenClawUnsupportedProfile("default")).toBe("default");
|
||||
expect(getOpenClawUnsupportedProfile("coding")).toBeNull();
|
||||
});
|
||||
|
||||
it("prefers timeoutSeconds and falls back to legacy timeout", () => {
|
||||
expect(
|
||||
getOpenClawTimeoutInputValue({ timeoutSeconds: 120, timeout: 30 }),
|
||||
).toBe("120");
|
||||
expect(getOpenClawTimeoutInputValue({ timeout: 45 })).toBe("45");
|
||||
expect(getOpenClawTimeoutInputValue({})).toBe("");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user