feat: OpenClaw WebSocket integration with workspace file preview
- Migrate OpenClaw from HTTP (port 8004) to WebSocket (port 18789) - Add workspace file list and content preview handlers - Add OpenClawStatus component with agent/skills view - Add OpenClawView panel in trader interface - Add Zustand store for OpenClaw state management - Fix gateway logging noise (yfinance, websockets) - Fix RunWorkspaceManager.get_agent_asset_dir attribute error - Handle missing workspace files gracefully in preview Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
532
frontend/src/components/OpenClawStatus.jsx
Normal file
532
frontend/src/components/OpenClawStatus.jsx
Normal file
@@ -0,0 +1,532 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useOpenClawStore } from "../store/openclawStore";
|
||||
import { useOpenClawPanel } from "../hooks/useOpenClawPanel";
|
||||
|
||||
// Agent run states matching openclaw-control-center/src/types.ts
|
||||
const AGENT_RUN_STATES = {
|
||||
idle: { label: "空闲", color: "#9CA3AF" },
|
||||
running: { label: "运行中", color: "#10B981" },
|
||||
blocked: { label: "阻塞", color: "#F59E0B" },
|
||||
waiting_approval: { label: "待审批", color: "#8B5CF6" },
|
||||
error: { label: "错误", color: "#EF4444" },
|
||||
};
|
||||
|
||||
// Agent accent colors for avatar borders
|
||||
const AGENT_COLORS = [
|
||||
{ accent: "#3B82F6" },
|
||||
{ accent: "#8B5CF6" },
|
||||
{ accent: "#EC4899" },
|
||||
{ accent: "#F59E0B" },
|
||||
{ accent: "#10B981" },
|
||||
{ accent: "#EF4444" },
|
||||
{ accent: "#06B6D4" },
|
||||
{ accent: "#84CC16" },
|
||||
];
|
||||
|
||||
function getAgentColor(agentId) {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < (agentId || "").length; i++) {
|
||||
hash = ((hash << 5) - hash) + agentId.charCodeAt(i);
|
||||
hash = hash & hash;
|
||||
}
|
||||
return AGENT_COLORS[Math.abs(hash) % AGENT_COLORS.length].accent;
|
||||
}
|
||||
|
||||
function agentStateFromPresence(presence, agentId) {
|
||||
const p = presence?.[agentId];
|
||||
if (!p) return "idle";
|
||||
if (p.status === "active") return "running";
|
||||
if (p.sessions?.some(s => s.state === "blocked")) return "blocked";
|
||||
if (p.sessions?.some(s => s.state === "waiting_approval")) return "waiting_approval";
|
||||
if (p.sessions?.some(s => s.state === "error" || s.state === "failed")) return "error";
|
||||
if (p.activeSessions > 0) return "running";
|
||||
return "idle";
|
||||
}
|
||||
|
||||
function AvatarIcon({ agentId, size = 56, borderRadius = 14 }) {
|
||||
const color = getAgentColor(agentId);
|
||||
return (
|
||||
<div style={{
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius,
|
||||
background: `${color}18`,
|
||||
border: `1px solid ${color}33`,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
fontSize: size * 0.36,
|
||||
fontWeight: 800,
|
||||
color,
|
||||
}}>
|
||||
{agentId?.slice(0, 2).toUpperCase() || "??"}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SkillBadge({ skill, color }) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
display: "grid",
|
||||
gap: 7,
|
||||
paddingBottom: 10,
|
||||
borderBottom: "1px dashed #D7DEE7",
|
||||
}}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", gap: 10, alignItems: "flex-start" }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
style={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
border: "none",
|
||||
background: "transparent",
|
||||
padding: 0,
|
||||
textAlign: "left",
|
||||
cursor: "pointer",
|
||||
display: "grid",
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<span style={{ fontSize: 12, color: "#6B7280", fontWeight: 700 }}>
|
||||
{expanded ? "▾" : "▸"}
|
||||
</span>
|
||||
{skill.emoji && <span style={{ fontSize: 12 }}>{skill.emoji}</span>}
|
||||
<span style={{ fontSize: 12, fontWeight: 700, color: "#111111" }}>
|
||||
{skill.name || "未命名技能"}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{expanded && skill.description && (
|
||||
<div style={{
|
||||
marginLeft: 20,
|
||||
borderRadius: 8,
|
||||
border: "1px solid #E5E7EB",
|
||||
background: "#FFFFFF",
|
||||
padding: "10px 12px",
|
||||
display: "grid",
|
||||
gap: 8,
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: 11,
|
||||
color: "#1F2937",
|
||||
lineHeight: 1.6,
|
||||
whiteSpace: "pre-wrap",
|
||||
fontFamily: '"Courier New", monospace',
|
||||
}}>
|
||||
{skill.description}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AgentDetail({ agent, presence, skills }) {
|
||||
const { workspaceFiles, workspaceFilesLoading, workspaceFilesError, workspaceFileContent } = useOpenClawStore();
|
||||
const { requestWorkspaceFiles, requestWorkspaceFile } = useOpenClawPanel();
|
||||
const [selectedDoc, setSelectedDoc] = useState(null);
|
||||
|
||||
// Always use "main" as the workspace key since that's the only valid OpenClaw agent ID
|
||||
const workspace = agent?.id || "main";
|
||||
const rawFiles = workspaceFiles[workspace]?.files || [];
|
||||
// Normalize file props: API returns uppercase (Name, Size, Path, Preview, PreviewTruncated)
|
||||
const files = rawFiles.map(f => ({
|
||||
name: f.Name || f.name,
|
||||
size: f.Size || f.size,
|
||||
path: f.Path || f.path,
|
||||
preview: f.Preview || f.preview,
|
||||
previewTruncated: f.PreviewTruncated || f.previewTruncated,
|
||||
}));
|
||||
const isLoadingFiles = workspaceFilesLoading && !workspaceFiles[workspace];
|
||||
|
||||
// Fetch workspace files when agent changes
|
||||
useEffect(() => {
|
||||
if (workspace && !workspaceFiles[workspace]) {
|
||||
requestWorkspaceFiles(workspace);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [workspace]);
|
||||
|
||||
const agentId = agent.id || agent.name || "?";
|
||||
const state = agentStateFromPresence(presence, agentId);
|
||||
const stateInfo = AGENT_RUN_STATES[state] || AGENT_RUN_STATES.idle;
|
||||
const color = getAgentColor(agentId);
|
||||
|
||||
// Skills are global in OpenClaw — show all skills (not filtered per-agent)
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
display: "grid",
|
||||
gap: 16,
|
||||
minHeight: 0,
|
||||
overflowY: "auto",
|
||||
alignContent: "start",
|
||||
}}>
|
||||
{/* Header */}
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: 16, flexWrap: "wrap" }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
||||
<AvatarIcon agentId={agentId} size={58} borderRadius={12} />
|
||||
<div style={{ display: "grid", gap: 4 }}>
|
||||
<div style={{ fontSize: 15, fontWeight: 800, color: "#111111" }}>{agent.name || agentId}</div>
|
||||
<div style={{ fontSize: 11, color: "#6B7280" }}>{agentId}</div>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
|
||||
<div style={{ width: 8, height: 8, borderRadius: "50%", background: stateInfo.color }} />
|
||||
<span style={{ fontSize: 11, color: stateInfo.color, fontWeight: 700 }}>{stateInfo.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{
|
||||
border: `1px solid ${color}2e`,
|
||||
background: `${color}0e`,
|
||||
borderRadius: 12,
|
||||
padding: "10px 12px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
}}>
|
||||
<div style={{ display: "grid", gap: 2 }}>
|
||||
<div style={{ fontSize: 11, color: "#4B5563", fontWeight: 700 }}>模型</div>
|
||||
<div style={{ fontSize: 12, color: "#111111", fontWeight: 800, fontFamily: '"Courier New", monospace' }}>
|
||||
{agent.model || "—"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Skills + Documents: left-right layout */}
|
||||
<div style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "minmax(240px, 2fr) minmax(0, 3fr)",
|
||||
gap: 16,
|
||||
alignItems: "start",
|
||||
minHeight: 0,
|
||||
}}>
|
||||
{/* Left: Skills */}
|
||||
<div style={{ display: "grid", gap: 10 }}>
|
||||
{(() => {
|
||||
const available = skills.filter(s => {
|
||||
const hasMissing = s.missing && (s.missing.bins?.length || s.missing.env?.length || s.missing.config?.length);
|
||||
return s.eligible !== false && s.disabled !== true && !hasMissing;
|
||||
});
|
||||
return available.length > 0 && (
|
||||
<div style={{
|
||||
border: "1px solid #E5EAF1",
|
||||
borderRadius: 12,
|
||||
background: "#FCFDFE",
|
||||
padding: 14,
|
||||
display: "grid",
|
||||
gap: 10,
|
||||
}}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", gap: 10, alignItems: "center" }}>
|
||||
<div style={{ display: "grid", gap: 2 }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 800, color: "#111111" }}>可用技能</div>
|
||||
<div style={{ fontSize: 10, color: "#6B7280", fontFamily: '"Courier New", monospace' }}>
|
||||
已就绪: {available.length}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{
|
||||
border: "1px solid #E5E7EB",
|
||||
background: "#F8FAFC",
|
||||
borderRadius: 8,
|
||||
padding: "10px 12px",
|
||||
display: "grid",
|
||||
gap: 8,
|
||||
maxHeight: 420,
|
||||
overflowY: "auto",
|
||||
}}>
|
||||
{available.map((skill, i) => (
|
||||
<SkillBadge key={skill.name || i} skill={skill} color={color} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{/* Right: Documents */}
|
||||
<div style={{ display: "grid", gap: 10 }}>
|
||||
{workspace && (
|
||||
<div style={{
|
||||
border: "1px solid #E5EAF1",
|
||||
borderRadius: 12,
|
||||
background: "#FCFDFE",
|
||||
padding: 14,
|
||||
display: "grid",
|
||||
gap: 10,
|
||||
}}>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", gap: 10, alignItems: "center" }}>
|
||||
<div style={{ display: "grid", gap: 2 }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 800, color: "#111111" }}>工作区文档</div>
|
||||
<div style={{ fontSize: 10, color: "#6B7280", fontFamily: '"Courier New", monospace' }}>
|
||||
{files.length} 个文件
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{
|
||||
border: "1px solid #E5E7EB",
|
||||
background: "#F8FAFC",
|
||||
borderRadius: 8,
|
||||
padding: "10px 12px",
|
||||
display: "grid",
|
||||
gap: 8,
|
||||
maxHeight: 420,
|
||||
overflowY: "auto",
|
||||
}}>
|
||||
{isLoadingFiles ? (
|
||||
<div style={{ fontSize: 11, color: "#6B7280", fontStyle: "italic" }}>加载中…</div>
|
||||
) : workspaceFilesError ? (
|
||||
<div style={{ fontSize: 11, color: "#EF4444" }}>加载失败</div>
|
||||
) : files.length === 0 ? (
|
||||
<div style={{ fontSize: 11, color: "#9CA3AF" }}>暂无文档</div>
|
||||
) : (
|
||||
files.map((f) => (
|
||||
<button
|
||||
key={f.name}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const clickedFile = selectedDoc?.name === f.name ? null : f;
|
||||
setSelectedDoc(clickedFile);
|
||||
if (clickedFile && !workspaceFileContent[`${workspace}:${f.name}`]) {
|
||||
requestWorkspaceFile(workspace, f.name);
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
textAlign: "left",
|
||||
background: selectedDoc?.name === f.name ? `${color}14` : "transparent",
|
||||
border: `1px solid ${selectedDoc?.name === f.name ? color + "40" : "#E5EAF1"}`,
|
||||
borderRadius: 6,
|
||||
padding: "8px 10px",
|
||||
cursor: "pointer",
|
||||
display: "grid",
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<div style={{ fontSize: 11, fontWeight: 700, color: "#111111" }}>{f.name}</div>
|
||||
<div style={{ fontSize: 10, color: "#9CA3AF" }}>{f.size} B</div>
|
||||
</div>
|
||||
{selectedDoc?.name === f.name && (
|
||||
<div style={{
|
||||
fontSize: 10,
|
||||
color: "#4B5563",
|
||||
fontFamily: '"Courier New", monospace',
|
||||
lineHeight: 1.5,
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-word",
|
||||
maxHeight: 120,
|
||||
overflowY: "auto",
|
||||
background: "#FFFFFF",
|
||||
borderRadius: 4,
|
||||
padding: "6px 8px",
|
||||
marginTop: 4,
|
||||
}}>
|
||||
{workspaceFileContent[`${workspace}:${f.name}`] || f.preview || "(内容加载中...)"}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function OpenClawStatus() {
|
||||
const store = useOpenClawStore();
|
||||
const {
|
||||
requestStatus,
|
||||
requestAgents,
|
||||
requestAgentsPresence,
|
||||
requestSkills,
|
||||
} = useOpenClawPanel();
|
||||
|
||||
const [selectedAgentId, setSelectedAgentId] = useState(
|
||||
() => store.agents[0]?.id || store.agents[0]?.name || null
|
||||
);
|
||||
|
||||
// Fetch data only if store is empty (on mount / page refresh)
|
||||
useEffect(() => {
|
||||
if (!store.agents.length) requestAgents();
|
||||
if (!store.skills.length) requestSkills();
|
||||
if (!store.openclawStatus) requestStatus();
|
||||
requestAgentsPresence();
|
||||
}, []);
|
||||
|
||||
const status = store.openclawStatus;
|
||||
const agents = store.agents;
|
||||
const presence = store.agentsPresence?.agents || {};
|
||||
const skills = store.skills || [];
|
||||
|
||||
const selectedAgent = agents.find(a => (a.id || a.name) === selectedAgentId) || agents[0] || null;
|
||||
|
||||
// Auto-select first agent when agents load
|
||||
useEffect(() => {
|
||||
if (!selectedAgentId && agents.length > 0) {
|
||||
setSelectedAgentId(agents[0].id || agents[0].name);
|
||||
}
|
||||
}, [agents, selectedAgentId]);
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
height: "100%",
|
||||
overflow: "hidden",
|
||||
padding: "18px",
|
||||
background: "linear-gradient(180deg, #ffffff 0%, #f4f7fb 100%)",
|
||||
display: "grid",
|
||||
gridTemplateRows: "auto 1fr",
|
||||
gap: 18,
|
||||
}}>
|
||||
{/* Header */}
|
||||
<div style={{ display: "grid", gap: 4 }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 800, letterSpacing: "0.5px", color: "#111111" }}>
|
||||
OpenClaw Agent 状态
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: "#6B7280" }}>
|
||||
监控 OpenClaw 多 Agent 运行时状态
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main content: left agent list + right detail */}
|
||||
<div style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: agents.length > 0 ? "120px minmax(0, 1fr)" : "1fr",
|
||||
gap: 16,
|
||||
alignItems: "stretch",
|
||||
minHeight: 0,
|
||||
overflow: "hidden",
|
||||
}}>
|
||||
{/* Left: agent avatar list */}
|
||||
{agents.length > 0 && (
|
||||
<div style={{
|
||||
border: "1px solid #D9E0E7",
|
||||
borderRadius: 14,
|
||||
background: "#FFFFFF",
|
||||
boxShadow: "0 10px 24px rgba(15, 23, 42, 0.06)",
|
||||
padding: 12,
|
||||
display: "grid",
|
||||
gap: 10,
|
||||
minHeight: 0,
|
||||
overflowY: "auto",
|
||||
alignContent: "start",
|
||||
}}>
|
||||
{agents.map((agent) => {
|
||||
const agentId = agent.id || agent.name;
|
||||
const isSelected = (agent.id || agent.name) === (selectedAgent?.id || selectedAgent?.name);
|
||||
const color = getAgentColor(agentId);
|
||||
const state = agentStateFromPresence(presence, agentId);
|
||||
const stateInfo = AGENT_RUN_STATES[state] || AGENT_RUN_STATES.idle;
|
||||
return (
|
||||
<button
|
||||
key={agentId}
|
||||
type="button"
|
||||
onClick={() => setSelectedAgentId(agentId)}
|
||||
title={agent.name || agentId}
|
||||
style={{
|
||||
border: isSelected ? `2px solid ${color}` : "1px solid #D9E0E7",
|
||||
borderRadius: 16,
|
||||
background: isSelected ? `${color}10` : "#FFFFFF",
|
||||
boxShadow: isSelected ? `0 10px 20px ${color}18` : "none",
|
||||
padding: 8,
|
||||
display: "grid",
|
||||
gap: 4,
|
||||
justifyItems: "center",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<div style={{ position: "relative" }}>
|
||||
<AvatarIcon agentId={agentId} size={48} borderRadius={12} />
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
bottom: -2,
|
||||
right: -2,
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: "50%",
|
||||
background: stateInfo.color,
|
||||
border: "2px solid #FFFFFF",
|
||||
}} />
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: 9,
|
||||
fontWeight: 800,
|
||||
color: isSelected ? color : "#374151",
|
||||
textAlign: "center",
|
||||
lineHeight: 1.4,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
maxWidth: "100%",
|
||||
}}>
|
||||
{agent.name || agentId}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Right: agent detail */}
|
||||
<div style={{
|
||||
display: "grid",
|
||||
minHeight: 0,
|
||||
overflow: "hidden",
|
||||
}}>
|
||||
{/* Agent detail */}
|
||||
<div style={{
|
||||
border: "1px solid #D9E0E7",
|
||||
borderRadius: 14,
|
||||
background: "#FFFFFF",
|
||||
boxShadow: "0 10px 24px rgba(15, 23, 42, 0.06)",
|
||||
padding: 18,
|
||||
display: "grid",
|
||||
minHeight: 0,
|
||||
overflow: "hidden",
|
||||
}}>
|
||||
{agents.length === 0 ? (
|
||||
<div style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "100%",
|
||||
gap: 12,
|
||||
}}>
|
||||
<AvatarIcon agentId="??" size={64} borderRadius={16} />
|
||||
<div style={{ fontSize: 13, color: "#9CA3AF" }}>
|
||||
{store.agentsLoading ? "加载中..." : (store.agentsError ? `错误: ${String(store.agentsError).slice(0, 60)}` : "暂无 Agent")}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => { requestAgents(); requestAgentsPresence(); }}
|
||||
style={{
|
||||
padding: "8px 14px",
|
||||
border: "1px solid #000000",
|
||||
background: "#FFFFFF",
|
||||
color: "#000000",
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
borderRadius: 8,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
刷新
|
||||
</button>
|
||||
</div>
|
||||
) : selectedAgent ? (
|
||||
<AgentDetail agent={selectedAgent} presence={presence} skills={skills} />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
14
frontend/src/components/OpenClawView.jsx
Normal file
14
frontend/src/components/OpenClawView.jsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { OpenClawStatus } from './OpenClawStatus';
|
||||
|
||||
export default function OpenClawView() {
|
||||
return (
|
||||
<div style={{
|
||||
height: '100%',
|
||||
overflow: 'auto',
|
||||
padding: '16px',
|
||||
background: '#F3F4F6',
|
||||
}}>
|
||||
<OpenClawStatus />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
263
frontend/src/hooks/useOpenClawPanel.js
Normal file
263
frontend/src/hooks/useOpenClawPanel.js
Normal file
@@ -0,0 +1,263 @@
|
||||
import { useCallback } from "react";
|
||||
import { useOpenClawStore } from "../store/openclawStore";
|
||||
|
||||
const RETRY_DELAY_MS = 250;
|
||||
|
||||
function sendWithRetry(clientRef, payload, retries = 3) {
|
||||
const attemptSend = (remaining) => {
|
||||
const client = clientRef.current;
|
||||
if (!client) return false;
|
||||
const sent = client.send(typeof payload === "string" ? payload : JSON.stringify(payload));
|
||||
if (sent || remaining <= 0) return sent;
|
||||
window.setTimeout(() => attemptSend(remaining - 1), RETRY_DELAY_MS);
|
||||
return false;
|
||||
};
|
||||
return attemptSend(retries);
|
||||
}
|
||||
|
||||
export function useOpenClawPanel() {
|
||||
// Access store state directly — do NOT destructure store as a useCallback dep
|
||||
// or every store update will recreate all callbacks and trigger infinite loops.
|
||||
const getStore = () => useOpenClawStore.getState();
|
||||
|
||||
const requestStatus = useCallback(() => {
|
||||
const store = getStore();
|
||||
const client = store.clientRef?.current;
|
||||
if (!client) return;
|
||||
store.setStatusLoading(true);
|
||||
store.setStatusError(null);
|
||||
sendWithRetry({ current: client }, { type: "get_openclaw_status" });
|
||||
}, []);
|
||||
|
||||
const requestSessions = useCallback(() => {
|
||||
const store = getStore();
|
||||
const client = store.clientRef?.current;
|
||||
if (!client) return;
|
||||
store.setSessionsLoading(true);
|
||||
store.setSessionsError(null);
|
||||
sendWithRetry({ current: client }, { type: "get_openclaw_sessions" });
|
||||
}, []);
|
||||
|
||||
const requestSessionDetail = useCallback((sessionKey) => {
|
||||
const store = getStore();
|
||||
const client = store.clientRef?.current;
|
||||
if (!client) return;
|
||||
store.setSelectedSessionKey(sessionKey);
|
||||
store.setSessionDetailLoading(true);
|
||||
store.setSessionDetailError(null);
|
||||
sendWithRetry({ current: client }, { type: "get_openclaw_session_detail", session_key: sessionKey });
|
||||
}, []);
|
||||
|
||||
const requestSessionHistory = useCallback((sessionKey, limit = 20) => {
|
||||
const client = getStore().clientRef?.current;
|
||||
if (!client) return;
|
||||
sendWithRetry({ current: client }, {
|
||||
type: "get_openclaw_session_history",
|
||||
session_key: sessionKey,
|
||||
limit,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const requestCron = useCallback(() => {
|
||||
const store = getStore();
|
||||
const client = store.clientRef?.current;
|
||||
if (!client) return;
|
||||
store.setCronLoading(true);
|
||||
store.setCronError(null);
|
||||
sendWithRetry({ current: client }, { type: "get_openclaw_cron" });
|
||||
}, []);
|
||||
|
||||
const requestApprovals = useCallback(() => {
|
||||
const store = getStore();
|
||||
const client = store.clientRef?.current;
|
||||
if (!client) return;
|
||||
store.setApprovalsLoading(true);
|
||||
store.setApprovalsError(null);
|
||||
sendWithRetry({ current: client }, { type: "get_openclaw_approvals" });
|
||||
}, []);
|
||||
|
||||
const requestAgents = useCallback(() => {
|
||||
const store = getStore();
|
||||
const client = store.clientRef?.current;
|
||||
if (!client) return;
|
||||
store.setAgentsLoading(true);
|
||||
store.setAgentsError(null);
|
||||
sendWithRetry({ current: client }, { type: "get_openclaw_agents" });
|
||||
}, []);
|
||||
|
||||
const requestAgentsPresence = useCallback(() => {
|
||||
const client = getStore().clientRef?.current;
|
||||
if (!client) return;
|
||||
sendWithRetry({ current: client }, { type: "get_openclaw_agents_presence" });
|
||||
}, []);
|
||||
|
||||
const requestSkills = useCallback(() => {
|
||||
const store = getStore();
|
||||
const client = store.clientRef?.current;
|
||||
if (!client) return;
|
||||
store.setSkillsLoading(true);
|
||||
store.setSkillsError(null);
|
||||
sendWithRetry({ current: client }, { type: "get_openclaw_skills" });
|
||||
}, []);
|
||||
|
||||
const requestModels = useCallback(() => {
|
||||
const store = getStore();
|
||||
const client = store.clientRef?.current;
|
||||
if (!client) return;
|
||||
store.setModelsLoading(true);
|
||||
store.setModelsError(null);
|
||||
sendWithRetry({ current: client }, { type: "get_openclaw_models" });
|
||||
}, []);
|
||||
|
||||
const requestHooks = useCallback(() => {
|
||||
const store = getStore();
|
||||
const client = store.clientRef?.current;
|
||||
if (!client) return;
|
||||
store.setHooksLoading(true);
|
||||
store.setHooksError(null);
|
||||
sendWithRetry({ current: client }, { type: "get_openclaw_hooks" });
|
||||
}, []);
|
||||
|
||||
const requestPlugins = useCallback(() => {
|
||||
const store = getStore();
|
||||
const client = store.clientRef?.current;
|
||||
if (!client) return;
|
||||
store.setPluginsLoading(true);
|
||||
store.setPluginsError(null);
|
||||
sendWithRetry({ current: client }, { type: "get_openclaw_plugins" });
|
||||
}, []);
|
||||
|
||||
const requestSecretsAudit = useCallback(() => {
|
||||
const store = getStore();
|
||||
const client = store.clientRef?.current;
|
||||
if (!client) return;
|
||||
store.setSecretsAuditLoading(true);
|
||||
store.setSecretsAuditError(null);
|
||||
sendWithRetry({ current: client }, { type: "get_openclaw_secrets_audit" });
|
||||
}, []);
|
||||
|
||||
const requestSecurityAudit = useCallback(() => {
|
||||
const store = getStore();
|
||||
const client = store.clientRef?.current;
|
||||
if (!client) return;
|
||||
store.setSecurityAuditLoading(true);
|
||||
store.setSecurityAuditError(null);
|
||||
sendWithRetry({ current: client }, { type: "get_openclaw_security_audit" });
|
||||
}, []);
|
||||
|
||||
const requestDaemonStatus = useCallback(() => {
|
||||
const store = getStore();
|
||||
const client = store.clientRef?.current;
|
||||
if (!client) return;
|
||||
store.setDaemonStatusLoading(true);
|
||||
store.setDaemonStatusError(null);
|
||||
sendWithRetry({ current: client }, { type: "get_openclaw_daemon_status" });
|
||||
}, []);
|
||||
|
||||
const requestPairing = useCallback(() => {
|
||||
const store = getStore();
|
||||
const client = store.clientRef?.current;
|
||||
if (!client) return;
|
||||
store.setPairingLoading(true);
|
||||
store.setPairingError(null);
|
||||
sendWithRetry({ current: client }, { type: "get_openclaw_pairing" });
|
||||
}, []);
|
||||
|
||||
const requestQrCode = useCallback(() => {
|
||||
const store = getStore();
|
||||
const client = store.clientRef?.current;
|
||||
if (!client) return;
|
||||
store.setQrCodeLoading(true);
|
||||
store.setQrCodeError(null);
|
||||
sendWithRetry({ current: client }, { type: "get_openclaw_qr" });
|
||||
}, []);
|
||||
|
||||
const requestUpdateStatus = useCallback(() => {
|
||||
const store = getStore();
|
||||
const client = store.clientRef?.current;
|
||||
if (!client) return;
|
||||
store.setUpdateStatusLoading(true);
|
||||
store.setUpdateStatusError(null);
|
||||
sendWithRetry({ current: client }, { type: "get_openclaw_update_status" });
|
||||
}, []);
|
||||
|
||||
const requestModelsAliases = useCallback(() => {
|
||||
const store = getStore();
|
||||
const client = store.clientRef?.current;
|
||||
if (!client) return;
|
||||
store.setModelsAliasesLoading(true);
|
||||
store.setModelsAliasesError(null);
|
||||
sendWithRetry({ current: client }, { type: "get_openclaw_models_aliases" });
|
||||
}, []);
|
||||
|
||||
const requestModelsFallbacks = useCallback(() => {
|
||||
const store = getStore();
|
||||
const client = store.clientRef?.current;
|
||||
if (!client) return;
|
||||
store.setModelsFallbacksLoading(true);
|
||||
store.setModelsFallbacksError(null);
|
||||
sendWithRetry({ current: client }, { type: "get_openclaw_models_fallbacks" });
|
||||
}, []);
|
||||
|
||||
const requestModelsImageFallbacks = useCallback(() => {
|
||||
const store = getStore();
|
||||
const client = store.clientRef?.current;
|
||||
if (!client) return;
|
||||
store.setModelsImageFallbacksLoading(true);
|
||||
store.setModelsImageFallbacksError(null);
|
||||
sendWithRetry({ current: client }, { type: "get_openclaw_models_image_fallbacks" });
|
||||
}, []);
|
||||
|
||||
const requestSkillUpdate = useCallback((slug = null, all = false) => {
|
||||
const store = getStore();
|
||||
const client = store.clientRef?.current;
|
||||
if (!client) return;
|
||||
store.setSkillUpdateLoading(true);
|
||||
store.setSkillUpdateError(null);
|
||||
sendWithRetry({ current: client }, { type: "get_openclaw_skill_update", slug, all });
|
||||
}, []);
|
||||
|
||||
const requestWorkspaceFiles = useCallback((workspace) => {
|
||||
const store = getStore();
|
||||
const client = store.clientRef?.current;
|
||||
if (!client || !workspace) return;
|
||||
store.setWorkspaceFilesLoading(true);
|
||||
store.setWorkspaceFilesError(null);
|
||||
sendWithRetry({ current: client }, { type: "get_openclaw_workspace_files", workspace });
|
||||
}, []);
|
||||
|
||||
const requestWorkspaceFile = useCallback((agent_id, file_name) => {
|
||||
const store = getStore();
|
||||
const client = store.clientRef?.current;
|
||||
if (!client || !agent_id || !file_name) return;
|
||||
console.log("[DEBUG] requestWorkspaceFile:", { type: "get_openclaw_workspace_file", agent_id, file_name });
|
||||
sendWithRetry({ current: client }, { type: "get_openclaw_workspace_file", agent_id, file_name });
|
||||
}, []);
|
||||
|
||||
return {
|
||||
requestStatus,
|
||||
requestSessions,
|
||||
requestSessionDetail,
|
||||
requestSessionHistory,
|
||||
requestCron,
|
||||
requestApprovals,
|
||||
requestAgents,
|
||||
requestAgentsPresence,
|
||||
requestSkills,
|
||||
requestModels,
|
||||
requestHooks,
|
||||
requestPlugins,
|
||||
requestSecretsAudit,
|
||||
requestSecurityAudit,
|
||||
requestDaemonStatus,
|
||||
requestPairing,
|
||||
requestQrCode,
|
||||
requestUpdateStatus,
|
||||
requestModelsAliases,
|
||||
requestModelsFallbacks,
|
||||
requestModelsImageFallbacks,
|
||||
requestSkillUpdate,
|
||||
requestWorkspaceFiles,
|
||||
requestWorkspaceFile,
|
||||
};
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useRef, useCallback } from 'react';
|
||||
import { AGENTS } from '../config/constants';
|
||||
import { ReadOnlyClient } from '../services/websocket';
|
||||
import { useRuntimeStore } from '../store/runtimeStore';
|
||||
import { useOpenClawStore } from '../store/openclawStore';
|
||||
import { useMarketStore } from '../store/marketStore';
|
||||
import { usePortfolioStore } from '../store/portfolioStore';
|
||||
import { useAgentStore } from '../store/agentStore';
|
||||
@@ -797,7 +798,198 @@ export function useWebSocketConnection({
|
||||
|
||||
fast_forward_success: (e) => {
|
||||
console.log(`✅ ${e.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
openclaw_status_loaded: (e) => {
|
||||
useOpenClawStore.getState().setOpenclawStatus(e.data || e);
|
||||
useOpenClawStore.getState().setStatusLoading(false);
|
||||
},
|
||||
openclaw_sessions_loaded: (e) => {
|
||||
useOpenClawStore.getState().setOpenclawSessions(e.data || e);
|
||||
useOpenClawStore.getState().setSessionsLoading(false);
|
||||
},
|
||||
openclaw_session_detail_loaded: (e) => {
|
||||
useOpenClawStore.getState().setOpenclawSessionDetail(e.data || e);
|
||||
useOpenClawStore.getState().setSessionDetailLoading(false);
|
||||
},
|
||||
openclaw_session_history_loaded: (e) => {
|
||||
useOpenClawStore.getState().setOpenclawSessionHistory(e.data || e);
|
||||
},
|
||||
openclaw_cron_loaded: (e) => {
|
||||
useOpenClawStore.getState().setOpenclawCronJobs(e.data || e);
|
||||
useOpenClawStore.getState().setCronLoading(false);
|
||||
},
|
||||
openclaw_approvals_loaded: (e) => {
|
||||
useOpenClawStore.getState().setOpenclawApprovals(e.data || e);
|
||||
useOpenClawStore.getState().setApprovalsLoading(false);
|
||||
},
|
||||
openclaw_agents_loaded: (e) => {
|
||||
useOpenClawStore.getState().setAgentsLoading(false);
|
||||
const d = e.data?.data ?? e.data;
|
||||
if (d?.error) {
|
||||
useOpenClawStore.getState().setAgentsError(d.error);
|
||||
} else {
|
||||
useOpenClawStore.getState().setAgents(d?.agents || []);
|
||||
useOpenClawStore.getState().setAgentsError(null);
|
||||
}
|
||||
},
|
||||
openclaw_agents_presence_loaded: (e) => {
|
||||
useOpenClawStore.getState().setAgentsPresence((e.data?.data ?? e.data) || {});
|
||||
},
|
||||
openclaw_skills_loaded: (e) => {
|
||||
useOpenClawStore.getState().setSkillsLoading(false);
|
||||
const d = e.data?.data ?? e.data;
|
||||
if (d?.error) {
|
||||
useOpenClawStore.getState().setSkillsError(d.error);
|
||||
} else {
|
||||
useOpenClawStore.getState().setSkills(d?.skills || []);
|
||||
useOpenClawStore.getState().setSkillsError(null);
|
||||
}
|
||||
},
|
||||
openclaw_models_loaded: (e) => {
|
||||
useOpenClawStore.getState().setModelsLoading(false);
|
||||
const d = e.data?.data ?? e.data;
|
||||
if (d?.error) {
|
||||
useOpenClawStore.getState().setModelsError(d.error);
|
||||
} else {
|
||||
useOpenClawStore.getState().setModels(d?.models || []);
|
||||
useOpenClawStore.getState().setModelsError(null);
|
||||
}
|
||||
},
|
||||
openclaw_workspace_files_loaded: (e) => {
|
||||
useOpenClawStore.getState().setWorkspaceFilesLoading(false);
|
||||
const d = e.data?.data ?? e.data;
|
||||
const workspace = d?.workspace || "";
|
||||
if (d?.error) {
|
||||
useOpenClawStore.getState().setWorkspaceFilesError(d.error);
|
||||
} else {
|
||||
useOpenClawStore.getState().setWorkspaceFiles(workspace, d);
|
||||
useOpenClawStore.getState().setWorkspaceFilesError(null);
|
||||
}
|
||||
},
|
||||
openclaw_workspace_file_loaded: (e) => {
|
||||
const d = e.data?.data ?? e.data;
|
||||
console.log("[DEBUG] workspace_file_loaded:", { d });
|
||||
if (d?.error) return;
|
||||
const agentId = d?.agentId || "main";
|
||||
const fileName = d?.file?.Name || d?.file?.name || "";
|
||||
const key = `${agentId}:${fileName}`;
|
||||
if (d?.file?.missing) {
|
||||
useOpenClawStore.getState().setWorkspaceFileContent(key, "(文件不存在)");
|
||||
} else if (d?.file?.content) {
|
||||
useOpenClawStore.getState().setWorkspaceFileContent(key, d.file.content);
|
||||
}
|
||||
},
|
||||
openclaw_hooks_loaded: (e) => {
|
||||
useOpenClawStore.getState().setHooksLoading(false);
|
||||
const d = e.data?.data ?? e.data;
|
||||
if (d?.error) {
|
||||
useOpenClawStore.getState().setHooksError(d.error);
|
||||
} else {
|
||||
useOpenClawStore.getState().setHooks(d?.hooks || []);
|
||||
useOpenClawStore.getState().setHooksError(null);
|
||||
}
|
||||
},
|
||||
openclaw_plugins_loaded: (e) => {
|
||||
useOpenClawStore.getState().setPluginsLoading(false);
|
||||
const d = e.data?.data ?? e.data;
|
||||
if (d?.error) {
|
||||
useOpenClawStore.getState().setPluginsError(d.error);
|
||||
} else {
|
||||
useOpenClawStore.getState().setPlugins(d?.plugins || []);
|
||||
useOpenClawStore.getState().setPluginsError(null);
|
||||
}
|
||||
},
|
||||
openclaw_secrets_audit_loaded: (e) => {
|
||||
useOpenClawStore.getState().setSecretsAuditLoading(false);
|
||||
if (e.data?.data?.error) {
|
||||
useOpenClawStore.getState().setSecretsAuditError(e.data.data.error);
|
||||
} else {
|
||||
useOpenClawStore.getState().setSecretsAudit(e.data?.data || null);
|
||||
useOpenClawStore.getState().setSecretsAuditError(null);
|
||||
}
|
||||
},
|
||||
openclaw_security_audit_loaded: (e) => {
|
||||
useOpenClawStore.getState().setSecurityAuditLoading(false);
|
||||
if (e.data?.data?.error) {
|
||||
useOpenClawStore.getState().setSecurityAuditError(e.data.data.error);
|
||||
} else {
|
||||
useOpenClawStore.getState().setSecurityAudit(e.data?.data || null);
|
||||
useOpenClawStore.getState().setSecurityAuditError(null);
|
||||
}
|
||||
},
|
||||
openclaw_daemon_status_loaded: (e) => {
|
||||
useOpenClawStore.getState().setDaemonStatusLoading(false);
|
||||
if (e.data?.data?.error) {
|
||||
useOpenClawStore.getState().setDaemonStatusError(e.data.data.error);
|
||||
} else {
|
||||
useOpenClawStore.getState().setDaemonStatus(e.data?.data || null);
|
||||
useOpenClawStore.getState().setDaemonStatusError(null);
|
||||
}
|
||||
},
|
||||
openclaw_pairing_loaded: (e) => {
|
||||
useOpenClawStore.getState().setPairingLoading(false);
|
||||
if (e.data?.data?.error) {
|
||||
useOpenClawStore.getState().setPairingError(e.data.data.error);
|
||||
} else {
|
||||
useOpenClawStore.getState().setPairing(e.data?.data || null);
|
||||
useOpenClawStore.getState().setPairingError(null);
|
||||
}
|
||||
},
|
||||
openclaw_qr_loaded: (e) => {
|
||||
useOpenClawStore.getState().setQrCodeLoading(false);
|
||||
if (e.data?.data?.error) {
|
||||
useOpenClawStore.getState().setQrCodeError(e.data.data.error);
|
||||
} else {
|
||||
useOpenClawStore.getState().setQrCode(e.data?.data || null);
|
||||
useOpenClawStore.getState().setQrCodeError(null);
|
||||
}
|
||||
},
|
||||
openclaw_update_status_loaded: (e) => {
|
||||
useOpenClawStore.getState().setUpdateStatusLoading(false);
|
||||
if (e.data?.data?.error) {
|
||||
useOpenClawStore.getState().setUpdateStatusError(e.data.data.error);
|
||||
} else {
|
||||
useOpenClawStore.getState().setUpdateStatus(e.data?.data || null);
|
||||
useOpenClawStore.getState().setUpdateStatusError(null);
|
||||
}
|
||||
},
|
||||
openclaw_models_aliases_loaded: (e) => {
|
||||
useOpenClawStore.getState().setModelsAliasesLoading(false);
|
||||
if (e.data?.data?.error) {
|
||||
useOpenClawStore.getState().setModelsAliasesError(e.data.data.error);
|
||||
} else {
|
||||
useOpenClawStore.getState().setModelsAliases(e.data?.data || null);
|
||||
useOpenClawStore.getState().setModelsAliasesError(null);
|
||||
}
|
||||
},
|
||||
openclaw_models_fallbacks_loaded: (e) => {
|
||||
useOpenClawStore.getState().setModelsFallbacksLoading(false);
|
||||
if (e.data?.data?.error) {
|
||||
useOpenClawStore.getState().setModelsFallbacksError(e.data.data.error);
|
||||
} else {
|
||||
useOpenClawStore.getState().setModelsFallbacks(e.data?.data?.items || []);
|
||||
useOpenClawStore.getState().setModelsFallbacksError(null);
|
||||
}
|
||||
},
|
||||
openclaw_models_image_fallbacks_loaded: (e) => {
|
||||
useOpenClawStore.getState().setModelsImageFallbacksLoading(false);
|
||||
if (e.data?.data?.error) {
|
||||
useOpenClawStore.getState().setModelsImageFallbacksError(e.data.data.error);
|
||||
} else {
|
||||
useOpenClawStore.getState().setModelsImageFallbacks(e.data?.data?.items || []);
|
||||
useOpenClawStore.getState().setModelsImageFallbacksError(null);
|
||||
}
|
||||
},
|
||||
openclaw_skill_update_loaded: (e) => {
|
||||
useOpenClawStore.getState().setSkillUpdateLoading(false);
|
||||
if (e.data?.data?.error) {
|
||||
useOpenClawStore.getState().setSkillUpdateError(e.data.data.error);
|
||||
} else {
|
||||
useOpenClawStore.getState().setSkillUpdate(e.data?.data || null);
|
||||
useOpenClawStore.getState().setSkillUpdateError(null);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
227
frontend/src/store/openclawStore.js
Normal file
227
frontend/src/store/openclawStore.js
Normal file
@@ -0,0 +1,227 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
|
||||
export const useOpenClawStore = create(
|
||||
persist(
|
||||
(set) => ({
|
||||
// Raw data
|
||||
openclawStatus: null,
|
||||
openclawSessions: [],
|
||||
openclawSessionDetail: null,
|
||||
openclawSessionHistory: [],
|
||||
openclawCronJobs: [],
|
||||
openclawApprovals: [],
|
||||
|
||||
// Loading states
|
||||
isStatusLoading: false,
|
||||
isSessionsLoading: false,
|
||||
isSessionDetailLoading: false,
|
||||
isCronLoading: false,
|
||||
isApprovalsLoading: false,
|
||||
|
||||
// Error states
|
||||
statusError: null,
|
||||
sessionsError: null,
|
||||
sessionDetailError: null,
|
||||
cronError: null,
|
||||
approvalsError: null,
|
||||
|
||||
// Agents state
|
||||
agents: [],
|
||||
agentsLoading: false,
|
||||
agentsError: null,
|
||||
agentsPresence: {},
|
||||
|
||||
// Skills state
|
||||
skills: [],
|
||||
skillsLoading: false,
|
||||
skillsError: null,
|
||||
|
||||
// Models state
|
||||
models: [],
|
||||
modelsLoading: false,
|
||||
modelsError: null,
|
||||
|
||||
// Hooks state
|
||||
hooks: [],
|
||||
hooksLoading: false,
|
||||
hooksError: null,
|
||||
|
||||
// Plugins state
|
||||
plugins: [],
|
||||
pluginsLoading: false,
|
||||
pluginsError: null,
|
||||
|
||||
// Secrets audit state
|
||||
secretsAudit: null,
|
||||
secretsAuditLoading: false,
|
||||
secretsAuditError: null,
|
||||
|
||||
// Security audit state
|
||||
securityAudit: null,
|
||||
securityAuditLoading: false,
|
||||
securityAuditError: null,
|
||||
|
||||
// Daemon status state
|
||||
daemonStatus: null,
|
||||
daemonStatusLoading: false,
|
||||
daemonStatusError: null,
|
||||
|
||||
// Pairing state
|
||||
pairing: null,
|
||||
pairingLoading: false,
|
||||
pairingError: null,
|
||||
|
||||
// QR code state
|
||||
qrCode: null,
|
||||
qrCodeLoading: false,
|
||||
qrCodeError: null,
|
||||
|
||||
// Update status state
|
||||
updateStatus: null,
|
||||
updateStatusLoading: false,
|
||||
updateStatusError: null,
|
||||
|
||||
// Models aliases state
|
||||
modelsAliases: null,
|
||||
modelsAliasesLoading: false,
|
||||
modelsAliasesError: null,
|
||||
|
||||
// Models fallbacks state
|
||||
modelsFallbacks: [],
|
||||
modelsFallbacksLoading: false,
|
||||
modelsFallbacksError: null,
|
||||
|
||||
// Models image fallbacks state
|
||||
modelsImageFallbacks: [],
|
||||
modelsImageFallbacksLoading: false,
|
||||
modelsImageFallbacksError: null,
|
||||
|
||||
// Skill update state
|
||||
skillUpdate: null,
|
||||
skillUpdateLoading: false,
|
||||
skillUpdateError: null,
|
||||
|
||||
// Workspace files state (per agent, keyed by workspace path)
|
||||
workspaceFiles: {},
|
||||
workspaceFilesLoading: false,
|
||||
workspaceFilesError: null,
|
||||
|
||||
// Workspace file content (keyed by "agentId:filename")
|
||||
workspaceFileContent: {},
|
||||
|
||||
// Selected session key for detail/history drill-down
|
||||
selectedSessionKey: null,
|
||||
|
||||
// WebSocket client ref (set by App.jsx on connection)
|
||||
clientRef: null,
|
||||
setClientRef: (ref) => set({ clientRef: ref }),
|
||||
|
||||
// Setters
|
||||
setOpenclawStatus: (data) => set({ openclawStatus: data, statusError: null }),
|
||||
setOpenclawSessions: (data) => set({ openclawSessions: data?.sessions || [], sessionsError: null }),
|
||||
setOpenclawSessionDetail: (data) => set({ openclawSessionDetail: data?.session || null, sessionDetailError: null }),
|
||||
setOpenclawSessionHistory: (data) => set({ openclawSessionHistory: data?.history || [], sessionDetailError: null }),
|
||||
setOpenclawCronJobs: (data) => set({ openclawCronJobs: data?.cron || [], cronError: null }),
|
||||
setOpenclawApprovals: (data) => set({ openclawApprovals: data?.approvals || [], approvalsError: null }),
|
||||
|
||||
setSelectedSessionKey: (key) => set({ selectedSessionKey: key }),
|
||||
|
||||
setStatusLoading: (v) => set({ isStatusLoading: v }),
|
||||
setSessionsLoading: (v) => set({ isSessionsLoading: v }),
|
||||
setSessionDetailLoading: (v) => set({ isSessionDetailLoading: v }),
|
||||
setCronLoading: (v) => set({ isCronLoading: v }),
|
||||
setApprovalsLoading: (v) => set({ isApprovalsLoading: v }),
|
||||
|
||||
setStatusError: (e) => set({ statusError: e }),
|
||||
setSessionsError: (e) => set({ sessionsError: e }),
|
||||
setSessionDetailError: (e) => set({ sessionDetailError: e }),
|
||||
setCronError: (e) => set({ cronError: e }),
|
||||
setApprovalsError: (e) => set({ approvalsError: e }),
|
||||
|
||||
setAgents: (agents) => set({ agents }),
|
||||
setAgentsLoading: (loading) => set({ agentsLoading: loading }),
|
||||
setAgentsError: (error) => set({ agentsError: error }),
|
||||
setAgentsPresence: (presence) => set({ agentsPresence: presence }),
|
||||
setSkills: (skills) => set({ skills }),
|
||||
setSkillsLoading: (loading) => set({ skillsLoading: loading }),
|
||||
setSkillsError: (error) => set({ skillsError: error }),
|
||||
setModels: (models) => set({ models }),
|
||||
setModelsLoading: (loading) => set({ modelsLoading: loading }),
|
||||
setModelsError: (error) => set({ modelsError: error }),
|
||||
|
||||
setHooks: (hooks) => set({ hooks }),
|
||||
setHooksLoading: (loading) => set({ hooksLoading: loading }),
|
||||
setHooksError: (error) => set({ hooksError: error }),
|
||||
setPlugins: (plugins) => set({ plugins }),
|
||||
setPluginsLoading: (loading) => set({ pluginsLoading: loading }),
|
||||
setPluginsError: (error) => set({ pluginsError: error }),
|
||||
setSecretsAudit: (data) => set({ secretsAudit: data }),
|
||||
setSecretsAuditLoading: (loading) => set({ secretsAuditLoading: loading }),
|
||||
setSecretsAuditError: (error) => set({ secretsAuditError: error }),
|
||||
setSecurityAudit: (data) => set({ securityAudit: data }),
|
||||
setSecurityAuditLoading: (loading) => set({ securityAuditLoading: loading }),
|
||||
setSecurityAuditError: (error) => set({ securityAuditError: error }),
|
||||
setDaemonStatus: (data) => set({ daemonStatus: data }),
|
||||
setDaemonStatusLoading: (loading) => set({ daemonStatusLoading: loading }),
|
||||
setDaemonStatusError: (error) => set({ daemonStatusError: error }),
|
||||
setPairing: (data) => set({ pairing: data }),
|
||||
setPairingLoading: (loading) => set({ pairingLoading: loading }),
|
||||
setPairingError: (error) => set({ pairingError: error }),
|
||||
setQrCode: (data) => set({ qrCode: data }),
|
||||
setQrCodeLoading: (loading) => set({ qrCodeLoading: loading }),
|
||||
setQrCodeError: (error) => set({ qrCodeError: error }),
|
||||
setUpdateStatus: (data) => set({ updateStatus: data }),
|
||||
setUpdateStatusLoading: (loading) => set({ updateStatusLoading: loading }),
|
||||
setUpdateStatusError: (error) => set({ updateStatusError: error }),
|
||||
setModelsAliases: (data) => set({ modelsAliases: data }),
|
||||
setModelsAliasesLoading: (loading) => set({ modelsAliasesLoading: loading }),
|
||||
setModelsAliasesError: (error) => set({ modelsAliasesError: error }),
|
||||
setModelsFallbacks: (data) => set({ modelsFallbacks: data }),
|
||||
setModelsFallbacksLoading: (loading) => set({ modelsFallbacksLoading: loading }),
|
||||
setModelsFallbacksError: (error) => set({ modelsFallbacksError: error }),
|
||||
setModelsImageFallbacks: (data) => set({ modelsImageFallbacks: data }),
|
||||
setModelsImageFallbacksLoading: (loading) => set({ modelsImageFallbacksLoading: loading }),
|
||||
setModelsImageFallbacksError: (error) => set({ modelsImageFallbacksError: error }),
|
||||
setSkillUpdate: (data) => set({ skillUpdate: data }),
|
||||
setSkillUpdateLoading: (loading) => set({ skillUpdateLoading: loading }),
|
||||
setSkillUpdateError: (error) => set({ skillUpdateError: error }),
|
||||
|
||||
setWorkspaceFiles: (workspace, data) => set((state) => ({
|
||||
workspaceFiles: { ...state.workspaceFiles, [workspace]: data },
|
||||
})),
|
||||
setWorkspaceFilesLoading: (loading) => set({ workspaceFilesLoading: loading }),
|
||||
setWorkspaceFilesError: (error) => set({ workspaceFilesError: error }),
|
||||
setWorkspaceFileContent: (key, content) => set((state) => ({
|
||||
workspaceFileContent: { ...state.workspaceFileContent, [key]: content },
|
||||
})),
|
||||
}),
|
||||
{
|
||||
name: "openclaw-store",
|
||||
// Skip persisting ephemeral UI state
|
||||
partialize: (state) => ({
|
||||
// Persist only data, not loading/error/UI states
|
||||
openclawStatus: state.openclawStatus,
|
||||
openclawSessions: state.openclawSessions,
|
||||
openclawCronJobs: state.openclawCronJobs,
|
||||
openclawApprovals: state.openclawApprovals,
|
||||
agents: state.agents,
|
||||
agentsPresence: state.agentsPresence,
|
||||
skills: state.skills,
|
||||
models: state.models,
|
||||
hooks: state.hooks,
|
||||
plugins: state.plugins,
|
||||
secretsAudit: state.secretsAudit,
|
||||
securityAudit: state.securityAudit,
|
||||
daemonStatus: state.daemonStatus,
|
||||
pairing: state.pairing,
|
||||
qrCode: state.qrCode,
|
||||
updateStatus: state.updateStatus,
|
||||
modelsAliases: state.modelsAliases,
|
||||
modelsFallbacks: state.modelsFallbacks,
|
||||
modelsImageFallbacks: state.modelsImageFallbacks,
|
||||
skillUpdate: state.skillUpdate,
|
||||
}),
|
||||
}
|
||||
)
|
||||
);
|
||||
Reference in New Issue
Block a user