"use client"; import { useState, useEffect, useRef } from "react"; import { Card, Button, ModelSelectModal, ManualConfigModal } from "@/shared/components"; import Image from "next/image"; import CliStatusBadge from "./CliStatusBadge"; import { useTranslations } from "next-intl"; const CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL; export default function OpenClawToolCard({ tool, isExpanded = false, onToggle = () => {}, baseUrl, hasActiveProviders, apiKeys, activeProviders, cloudEnabled, batchStatus, lastConfiguredAt, }) { const t = useTranslations("cliTools"); const [openclawStatus, setOpenclawStatus] = useState(null); const [checkingOpenclaw, setCheckingOpenclaw] = useState(false); const [applying, setApplying] = useState(false); const [restoring, setRestoring] = useState(false); const [message, setMessage] = useState(null); const [selectedApiKeyId, setSelectedApiKeyId] = useState(""); const [selectedModel, setSelectedModel] = useState(""); const [modalOpen, setModalOpen] = useState(false); const [modelAliases, setModelAliases] = useState({}); const [showManualConfigModal, setShowManualConfigModal] = useState(false); const [customBaseUrl, setCustomBaseUrl] = useState(""); const hasInitializedModel = useRef(false); // Backups state const [backups, setBackups] = useState([]); const [showBackups, setShowBackups] = useState(false); const [restoringBackup, setRestoringBackup] = useState(null); const cliReady = !!(openclawStatus?.installed && openclawStatus?.runnable); const getConfigStatus = () => { if (!cliReady) return null; const currentProvider = openclawStatus.settings?.models?.providers?.["omniroute"]; if (!currentProvider) return "not_configured"; const localMatch = currentProvider.baseUrl?.includes("localhost") || currentProvider.baseUrl?.includes("127.0.0.1"); const cloudMatch = cloudEnabled && CLOUD_URL && currentProvider.baseUrl?.startsWith(CLOUD_URL); if (localMatch || cloudMatch) return "configured"; return "other"; }; const configStatus = getConfigStatus(); // Use batch status as fallback when card hasn't been expanded yet const effectiveConfigStatus = configStatus || batchStatus?.configStatus || null; // (#523) Store the key *id* (not the masked string) so the backend can // resolve the real secret from DB before writing to config files. useEffect(() => { if (apiKeys?.length > 0 && !selectedApiKeyId) { setSelectedApiKeyId(apiKeys[0].id); } }, [apiKeys, selectedApiKeyId]); useEffect(() => { if (isExpanded && !openclawStatus) { checkOpenclawStatus(); fetchModelAliases(); fetchBackups(); } }, [isExpanded, openclawStatus]); const fetchModelAliases = async () => { try { const res = await fetch("/api/models/alias"); const data = await res.json(); if (res.ok) setModelAliases(data.aliases || {}); } catch (error) { console.log("Error fetching model aliases:", error); } }; useEffect(() => { if (openclawStatus?.installed && !hasInitializedModel.current) { hasInitializedModel.current = true; const provider = openclawStatus.settings?.models?.providers?.["omniroute"]; if (provider) { const primaryModel = openclawStatus.settings?.agents?.defaults?.model?.primary; if (primaryModel) { const modelId = primaryModel.replace("omniroute/", ""); setSelectedModel(modelId); } // (#523) Keys from /api/keys are masked (first 8 + "****" + last 4). // Match by prefix/suffix instead of exact comparison. if (provider.apiKey) { const fileKeyPrefix = provider.apiKey.slice(0, 8); const fileKeySuffix = provider.apiKey.slice(-4); const matchedKey = apiKeys?.find( (k) => k.key && k.key.startsWith(fileKeyPrefix) && k.key.endsWith(fileKeySuffix) ); if (matchedKey) setSelectedApiKeyId(matchedKey.id); } } } }, [openclawStatus, apiKeys]); const checkOpenclawStatus = async () => { setCheckingOpenclaw(true); try { const res = await fetch("/api/cli-tools/openclaw-settings"); const data = await res.json(); setOpenclawStatus(data); } catch (error) { setOpenclawStatus({ installed: false, error: error.message }); } finally { setCheckingOpenclaw(false); } }; const getEffectiveBaseUrl = () => { const url = customBaseUrl || baseUrl; return url.endsWith("/v1") ? url : `${url}/v1`; }; const getDisplayUrl = () => { const url = customBaseUrl || baseUrl; return url.endsWith("/v1") ? url : `${url}/v1`; }; const handleApplySettings = async () => { setApplying(true); setMessage(null); try { // (#523) Prefer keyId lookup so the backend writes the real key to disk. const selectedKeyId = selectedApiKeyId?.trim() || (apiKeys?.length > 0 ? apiKeys[0].id : null); const res = await fetch("/api/cli-tools/openclaw-settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ baseUrl: getEffectiveBaseUrl(), apiKey: !cloudEnabled ? "sk_omniroute" : null, keyId: selectedKeyId, model: selectedModel, }), }); const data = await res.json(); if (res.ok) { setMessage({ type: "success", text: t("settingsApplied") }); checkOpenclawStatus(); } else { setMessage({ type: "error", text: (typeof data.error === "string" ? data.error : data.error?.message) || t("failedApplySettings"), }); } } catch (error) { setMessage({ type: "error", text: error.message }); } finally { setApplying(false); } }; const handleResetSettings = async () => { setRestoring(true); setMessage(null); try { const res = await fetch("/api/cli-tools/openclaw-settings", { method: "DELETE" }); const data = await res.json(); if (res.ok) { setMessage({ type: "success", text: t("settingsReset") }); setSelectedModel(""); setSelectedApiKeyId(""); checkOpenclawStatus(); } else { setMessage({ type: "error", text: (typeof data.error === "string" ? data.error : data.error?.message) || t("failedResetSettings"), }); } } catch (error) { setMessage({ type: "error", text: error.message }); } finally { setRestoring(false); } }; const handleModelSelect = (model) => { setSelectedModel(model.value); setModalOpen(false); }; // ── Backups ── const fetchBackups = async () => { try { const res = await fetch("/api/cli-tools/backups?tool=openclaw"); const data = await res.json(); if (res.ok) setBackups(data.backups || []); } catch (error) { console.log("Error fetching backups:", error); } }; const handleRestoreBackup = async (backupId) => { setRestoringBackup(backupId); setMessage(null); try { const res = await fetch("/api/cli-tools/backups", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ tool: "openclaw", backupId }), }); const data = await res.json(); if (res.ok) { setMessage({ type: "success", text: t("backupRestored") }); checkOpenclawStatus(); fetchBackups(); } else { setMessage({ type: "error", text: (typeof data.error === "string" ? data.error : data.error?.message) || t("failedRestore"), }); } } catch (error) { setMessage({ type: "error", text: error.message }); } finally { setRestoringBackup(null); } }; const getManualConfigs = () => { // (#523) Look up the key object by id to get the masked display value. const selectedKeyObj = apiKeys?.find((k) => k.id === selectedApiKeyId); const keyToDisplay = selectedKeyObj?.key || (!cloudEnabled ? "sk_omniroute" : ""); const settingsContent = { agents: { defaults: { model: { primary: `omniroute/${selectedModel || "provider/model-id"}`, }, }, }, models: { providers: { omniroute: { baseUrl: getEffectiveBaseUrl(), apiKey: keyToDisplay, api: "openai-completions", models: [ { id: selectedModel || "provider/model-id", name: (selectedModel || "provider/model-id").split("/").pop(), }, ], }, }, }, }; return [ { filename: "~/.openclaw/openclaw.json", content: JSON.stringify(settingsContent, null, 2), }, ]; }; return (
{tool.name} { (e.currentTarget as HTMLElement).style.display = "none"; }} />

{tool.name}

{t("toolDescriptions.openclaw")}

expand_more
{isExpanded && (
{checkingOpenclaw && (
progress_activity {t("checkingCli", { tool: "Open Claw" })}
)} {!checkingOpenclaw && openclawStatus && !cliReady && (
warning

{openclawStatus.installed ? t("cliNotRunnable", { tool: "Open Claw" }) : t("cliNotInstalled", { tool: "Open Claw" })}

{openclawStatus.installed ? t("cliFoundFailedHealthcheck", { tool: "Open Claw", reason: openclawStatus.reason ? ` (${openclawStatus.reason})` : "", }) : t("installCliPrompt", { tool: "Open Claw" })}

)} {!checkingOpenclaw && cliReady && ( <>
{/* Current Base URL */} {openclawStatus?.settings?.models?.providers?.["omniroute"]?.baseUrl && (
{t("current")} arrow_forward {openclawStatus.settings.models.providers["omniroute"].baseUrl}
)} {/* Base URL */}
{t("baseUrl")} arrow_forward setCustomBaseUrl(e.target.value)} placeholder={t("baseUrlPlaceholder")} className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50" /> {customBaseUrl && customBaseUrl !== baseUrl && ( )}
{/* API Key */}
{t("apiKey")} arrow_forward {apiKeys.length > 0 ? ( ) : ( {cloudEnabled ? t("noApiKeysCreateOne") : t("defaultOmnirouteKey")} )}
{/* Model */}
{t("model")} arrow_forward setSelectedModel(e.target.value)} placeholder={t("providerModelPlaceholder")} className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50" /> {selectedModel && ( )}
{message && (
{message.type === "success" ? "check_circle" : "error"} {message.text}
)}
{showBackups && (

history {t("configBackups")}

{backups.length === 0 ? (

{t("noBackupsYet")}

) : (
{backups.map((b) => (
description {b.id} {new Date(b.createdAt).toLocaleString()}
))}
)}
)} )}
)} setModalOpen(false)} onSelect={handleModelSelect} selectedModel={selectedModel} activeProviders={activeProviders} modelAliases={modelAliases} title={t("selectModelForTool", { tool: "Open Claw" })} /> setShowManualConfigModal(false)} title={t("openClawManualConfiguration")} configs={getManualConfigs()} /> ); }