fix(openclaw): address code review findings across P0-P3 issues

- Add 25 missing i18n keys for OpenClawFormFields in all 3 locales (P0)
- Replace key={index} with stable crypto.randomUUID() keys in EnvPanel,
  ToolsPanel, and OpenClawFormFields to prevent list state bugs (P1)
- Exclude openclaw from ProxyToggle/FailoverToggle in App.tsx (P1)
- Add merge_additive_config() for openclaw/opencode deep link imports (P1)
- Normalize serde(flatten) field naming to `extra` + HashMap (P2)
- Add directory existence check in remove_openclaw_provider_from_live (P2)
- Remove dead code in import_default_config and openclaw API methods (P2)
- Add duplicate key validation in EnvPanel before save (P2)
- Add openclawConfigDir to Settings type (P2)
- Add staleTime to OpenClaw query hooks (P3)
- Fix type-unsafe delete via destructuring in mutations.ts (P3)
This commit is contained in:
Jason
2026-02-08 17:35:29 +08:00
parent 00b424628f
commit 5081206176
15 changed files with 241 additions and 102 deletions
+13 -2
View File
@@ -9,6 +9,7 @@ import { Input } from "@/components/ui/input";
import type { OpenClawEnvConfig } from "@/types";
interface EnvEntry {
id: string;
key: string;
value: string;
isNew?: boolean;
@@ -24,6 +25,7 @@ const EnvPanel: React.FC = () => {
useEffect(() => {
if (envData) {
const items: EnvEntry[] = Object.entries(envData).map(([key, value]) => ({
id: crypto.randomUUID(),
key,
value: String(value ?? ""),
}));
@@ -34,9 +36,15 @@ const EnvPanel: React.FC = () => {
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;
}
}
@@ -51,7 +59,10 @@ const EnvPanel: React.FC = () => {
};
const addEntry = () => {
setEntries((prev) => [...prev, { key: "", value: "", isNew: true }]);
setEntries((prev) => [
...prev,
{ id: crypto.randomUUID(), key: "", value: "", isNew: true },
]);
};
const removeEntry = (index: number) => {
@@ -103,7 +114,7 @@ const EnvPanel: React.FC = () => {
const visible = visibleKeys.has(visibilityId);
return (
<div key={index} className="flex items-center gap-2">
<div key={entry.id} className="flex items-center gap-2">
<div className="w-[200px] flex-shrink-0">
<Input
value={entry.key}
+47 -22
View File
@@ -16,6 +16,11 @@ import {
} from "@/components/ui/select";
import type { OpenClawToolsConfig } from "@/types";
interface ListItem {
id: string;
value: string;
}
const PROFILE_OPTIONS = ["default", "strict", "permissive", "custom"];
const ToolsPanel: React.FC = () => {
@@ -23,14 +28,24 @@ const ToolsPanel: React.FC = () => {
const { data: toolsData, isLoading } = useOpenClawTools();
const saveToolsMutation = useSaveOpenClawTools();
const [config, setConfig] = useState<OpenClawToolsConfig>({});
const [allowList, setAllowList] = useState<string[]>([]);
const [denyList, setDenyList] = useState<string[]>([]);
const [allowList, setAllowList] = useState<ListItem[]>([]);
const [denyList, setDenyList] = useState<ListItem[]>([]);
useEffect(() => {
if (toolsData) {
setConfig(toolsData);
setAllowList(toolsData.allow ?? []);
setDenyList(toolsData.deny ?? []);
setAllowList(
(toolsData.allow ?? []).map((v) => ({
id: crypto.randomUUID(),
value: v,
})),
);
setDenyList(
(toolsData.deny ?? []).map((v) => ({
id: crypto.randomUUID(),
value: v,
})),
);
}
}, [toolsData]);
@@ -40,8 +55,8 @@ const ToolsPanel: React.FC = () => {
const newConfig: OpenClawToolsConfig = {
...other,
profile: config.profile,
allow: allowList.filter((s) => s.trim()),
deny: denyList.filter((s) => s.trim()),
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"));
@@ -54,20 +69,20 @@ const ToolsPanel: React.FC = () => {
};
const updateListItem = (
list: string[],
setList: React.Dispatch<React.SetStateAction<string[]>>,
setList: React.Dispatch<React.SetStateAction<ListItem[]>>,
index: number,
value: string,
) => {
setList(list.map((item, i) => (i === index ? value : item)));
setList((prev) =>
prev.map((item, i) => (i === index ? { ...item, value } : item)),
);
};
const removeListItem = (
list: string[],
setList: React.Dispatch<React.SetStateAction<string[]>>,
setList: React.Dispatch<React.SetStateAction<ListItem[]>>,
index: number,
) => {
setList(list.filter((_, i) => i !== index));
setList((prev) => prev.filter((_, i) => i !== index));
};
if (isLoading) {
@@ -113,11 +128,11 @@ const ToolsPanel: React.FC = () => {
<Label className="mb-2 block">{t("openclaw.tools.allowList")}</Label>
<div className="space-y-2">
{allowList.map((item, index) => (
<div key={index} className="flex items-center gap-2">
<div key={item.id} className="flex items-center gap-2">
<Input
value={item}
value={item.value}
onChange={(e) =>
updateListItem(allowList, setAllowList, index, e.target.value)
updateListItem(setAllowList, index, e.target.value)
}
placeholder={t("openclaw.tools.patternPlaceholder")}
className="font-mono text-xs"
@@ -126,7 +141,7 @@ const ToolsPanel: React.FC = () => {
variant="ghost"
size="icon"
className="flex-shrink-0 h-9 w-9 text-muted-foreground hover:text-destructive"
onClick={() => removeListItem(allowList, setAllowList, index)}
onClick={() => removeListItem(setAllowList, index)}
>
<Trash2 className="w-4 h-4" />
</Button>
@@ -135,7 +150,12 @@ const ToolsPanel: React.FC = () => {
<Button
variant="outline"
size="sm"
onClick={() => setAllowList((prev) => [...prev, ""])}
onClick={() =>
setAllowList((prev) => [
...prev,
{ id: crypto.randomUUID(), value: "" },
])
}
>
<Plus className="w-4 h-4 mr-1" />
{t("openclaw.tools.addAllow")}
@@ -148,11 +168,11 @@ const ToolsPanel: React.FC = () => {
<Label className="mb-2 block">{t("openclaw.tools.denyList")}</Label>
<div className="space-y-2">
{denyList.map((item, index) => (
<div key={index} className="flex items-center gap-2">
<div key={item.id} className="flex items-center gap-2">
<Input
value={item}
value={item.value}
onChange={(e) =>
updateListItem(denyList, setDenyList, index, e.target.value)
updateListItem(setDenyList, index, e.target.value)
}
placeholder={t("openclaw.tools.patternPlaceholder")}
className="font-mono text-xs"
@@ -161,7 +181,7 @@ const ToolsPanel: React.FC = () => {
variant="ghost"
size="icon"
className="flex-shrink-0 h-9 w-9 text-muted-foreground hover:text-destructive"
onClick={() => removeListItem(denyList, setDenyList, index)}
onClick={() => removeListItem(setDenyList, index)}
>
<Trash2 className="w-4 h-4" />
</Button>
@@ -170,7 +190,12 @@ const ToolsPanel: React.FC = () => {
<Button
variant="outline"
size="sm"
onClick={() => setDenyList((prev) => [...prev, ""])}
onClick={() =>
setDenyList((prev) => [
...prev,
{ id: crypto.randomUUID(), value: "" },
])
}
>
<Plus className="w-4 h-4 mr-1" />
{t("openclaw.tools.addDeny")}
@@ -309,6 +309,7 @@ export function AddProviderDialog({
</TabsContent>
</Tabs>
) : (
// OpenCode/OpenClaw: directly show form without tabs
<ProviderForm
appId={appId}
submitLabel={t("common.add")}
@@ -1,5 +1,5 @@
import { useTranslation } from "react-i18next";
import { useState } from "react";
import { useState, useRef, useCallback } from "react";
import { FormLabel } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
@@ -64,6 +64,21 @@ export function OpenClawFormFields({
{},
);
// Stable key tracking for models list
const modelKeysRef = useRef<string[]>([]);
const getModelKeys = useCallback(() => {
// Grow keys array if models were added externally
while (modelKeysRef.current.length < models.length) {
modelKeysRef.current.push(crypto.randomUUID());
}
// Shrink if models were removed externally
if (modelKeysRef.current.length > models.length) {
modelKeysRef.current.length = models.length;
}
return modelKeysRef.current;
}, [models.length]);
const modelKeys = getModelKeys();
// Toggle advanced section for a model
const toggleModelAdvanced = (index: number) => {
setExpandedModels((prev) => ({ ...prev, [index]: !prev[index] }));
@@ -71,6 +86,7 @@ export function OpenClawFormFields({
// Add a new model entry
const handleAddModel = () => {
modelKeysRef.current.push(crypto.randomUUID());
onModelsChange([
...models,
{
@@ -85,6 +101,7 @@ export function OpenClawFormFields({
// Remove a model entry
const handleRemoveModel = (index: number) => {
modelKeysRef.current.splice(index, 1);
const newModels = [...models];
newModels.splice(index, 1);
onModelsChange(newModels);
@@ -216,7 +233,7 @@ export function OpenClawFormFields({
<div className="space-y-4">
{models.map((model, index) => (
<div
key={index}
key={modelKeys[index]}
className="p-3 border border-border/50 rounded-lg space-y-3"
>
{/* Model ID and Name row */}