feat: update openclaw workspace integration

This commit is contained in:
2026-03-27 22:27:16 +08:00
parent 5c08c1865c
commit 4aa69650e8
26 changed files with 2103 additions and 310 deletions

View File

@@ -13,6 +13,9 @@
"preview:host": "vite preview --host"
},
"dependencies": {
"@dicebear/collection": "^9.4.2",
"@dicebear/core": "^9.4.2",
"@lobehub/icons": "^5.0.1",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.7",

View File

@@ -234,6 +234,26 @@ export default function LiveTradingApp() {
workspaceFilesByAgent,
]);
useEffect(() => {
if (!isSocketReady || !clientRef.current) {
return;
}
AGENTS.forEach((agent) => {
if (!agent?.id) {
return;
}
if (!agentProfilesByAgent[agent.id]) {
requestAgentProfile(agent.id);
}
});
}, [
agentProfilesByAgent,
clientRef,
isSocketReady,
requestAgentProfile,
]);
useEffect(() => {
const symbols = runtimeControls.displayTickers
.map((ticker) => ticker.symbol)

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { ASSETS } from '../config/constants';
import { getModelIcon, getShortModelName } from '../utils/modelIcons';
import LobeModelLogo from './LobeModelLogo.jsx';
/**
* Get rank medal/trophy
@@ -207,14 +208,18 @@ export default function AgentCard({ agent, onClose, isClosing }) {
justifyContent: 'center',
marginBottom: 4
}}>
{modelInfo.logoPath ? (
<img
src={modelInfo.logoPath}
{agent.modelName || modelInfo.logoPath ? (
<LobeModelLogo
model={agent.modelName}
provider={agent.modelProvider}
fallbackSrc={modelInfo.logoPath}
alt={modelInfo.provider}
size={36}
type="color"
shape="square"
style={{
maxHeight: '100%',
maxWidth: '100%',
objectFit: 'contain'
}}
/>
) : (

View File

@@ -3,6 +3,7 @@ import { formatTime } from '../utils/formatters';
import { MESSAGE_COLORS, getAgentColors, AGENTS, ASSETS } from '../config/constants';
import { getModelIcon } from '../utils/modelIcons';
import MarkdownModal from './MarkdownModal';
import LobeModelLogo from './LobeModelLogo.jsx';
const isAnalyst = (agentId, agentName) => {
if (agentId && agentId.includes('analyst')) return true;
@@ -167,11 +168,11 @@ const AgentFeed = forwardRef(({ feed, leaderboard, agentProfilesByAgent }, ref)
// Get current selection display info
const getCurrentSelectionInfo = () => {
if (selectedAgent === 'all') {
return { label: '全部角色', modelInfo: null };
return { label: '全部角色', modelInfo: null, agentInfo: null };
}
const agentInfo = getAgentInfoByName(selectedAgent);
const modelInfo = agentInfo ? getModelIcon(agentInfo.modelName, agentInfo.modelProvider) : null;
return { label: selectedAgent, modelInfo };
return { label: selectedAgent, modelInfo, agentInfo };
};
const currentSelection = getCurrentSelectionInfo();
@@ -189,11 +190,16 @@ const AgentFeed = forwardRef(({ feed, leaderboard, agentProfilesByAgent }, ref)
onBlur={() => setTimeout(() => setDropdownOpen(false), 200)}
>
<div className="custom-select-value">
{currentSelection.modelInfo?.logoPath && (
<img
src={currentSelection.modelInfo.logoPath}
alt={currentSelection.modelInfo.provider}
{(currentSelection.agentInfo?.modelName || currentSelection.modelInfo?.logoPath) && (
<LobeModelLogo
model={currentSelection.agentInfo?.modelName}
provider={currentSelection.agentInfo?.modelProvider}
fallbackSrc={currentSelection.modelInfo?.logoPath}
alt={currentSelection.modelInfo?.provider}
size={18}
className="select-model-icon"
shape="square"
type="color"
/>
)}
<span>{currentSelection.label}</span>
@@ -223,11 +229,16 @@ const AgentFeed = forwardRef(({ feed, leaderboard, agentProfilesByAgent }, ref)
setDropdownOpen(false);
}}
>
{modelInfo?.logoPath && (
<img
src={modelInfo.logoPath}
alt={modelInfo.provider}
{(agentInfo?.modelName || modelInfo?.logoPath) && (
<LobeModelLogo
model={agentInfo?.modelName}
provider={agentInfo?.modelProvider}
fallbackSrc={modelInfo?.logoPath}
alt={modelInfo?.provider}
size={18}
className="select-model-icon"
shape="square"
type="color"
/>
)}
<span>{agent}</span>
@@ -363,16 +374,16 @@ function ConferenceMessage({ message, getAgentModelInfo }) {
return (
<div className="conf-message-item">
<div className="conf-agent-name" style={{ color: agentColors.text, display: 'flex', alignItems: 'center', gap: '6px', fontSize: '12px' }}>
{modelInfo.logoPath && (
<img
src={modelInfo.logoPath}
{(agentModelData.modelName || modelInfo.logoPath) && (
<LobeModelLogo
model={agentModelData.modelName}
provider={agentModelData.modelProvider}
fallbackSrc={modelInfo.logoPath}
alt={modelInfo.provider}
style={{
width: '20px',
height: '20px',
borderRadius: '50%',
objectFit: 'contain'
}}
size={20}
shape="circle"
type="color"
style={{ borderRadius: '50%' }}
/>
)}
{message.agent}
@@ -591,16 +602,16 @@ function MessageItem({ message, itemId, isHighlighted, getAgentModelInfo }) {
>
<div className="feed-item-header">
<span className="feed-item-title" style={{ color: colors.text, display: 'flex', alignItems: 'center', gap: '6px', fontSize: '12px' }}>
{modelInfo.logoPath && message.agent !== 'Memory' && (
<img
src={modelInfo.logoPath}
{message.agent !== 'Memory' && (agentModelData.modelName || modelInfo.logoPath) && (
<LobeModelLogo
model={agentModelData.modelName}
provider={agentModelData.modelProvider}
fallbackSrc={modelInfo.logoPath}
alt={modelInfo.provider}
style={{
width: '20px',
height: '20px',
borderRadius: '50%',
objectFit: 'contain'
}}
size={20}
shape="circle"
type="color"
style={{ borderRadius: '50%' }}
/>
)}
{title}

View File

@@ -0,0 +1,78 @@
import React from 'react';
import ModelIcon from '@lobehub/icons/es/features/ModelIcon';
import ProviderIcon from '@lobehub/icons/es/features/ProviderIcon';
export default function LobeModelLogo({
model,
provider,
fallbackSrc = null,
alt = '',
size = 28,
shape = 'square',
type = 'color',
style = {},
className = '',
}) {
const hasModel = typeof model === 'string' && model.trim().length > 0;
const hasProvider = typeof provider === 'string' && provider.trim().length > 0;
try {
if (hasModel) {
return (
<ModelIcon
model={model}
size={size}
shape={shape}
type={type}
className={className}
style={style}
/>
);
}
if (hasProvider) {
return (
<ProviderIcon
provider={provider.toLowerCase()}
size={size}
shape={shape}
type={type}
className={className}
style={style}
/>
);
}
} catch {
// Fall through to local fallback asset.
}
if (fallbackSrc) {
return (
<img
src={fallbackSrc}
alt={alt}
className={className}
style={{
width: size,
height: size,
objectFit: 'contain',
...style,
}}
/>
);
}
return (
<div
className={className}
style={{
width: size,
height: size,
borderRadius: shape === 'circle' ? '50%' : 8,
background: '#F3F4F6',
border: '1px solid #D1D5DB',
...style,
}}
/>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,5 @@
import { OpenClawStatus } from './OpenClawStatus';
export default function OpenClawView() {
return (
<div style={{
height: '100%',
overflow: 'auto',
padding: '16px',
background: '#F3F4F6',
}}>
<OpenClawStatus />
</div>
);
return <OpenClawStatus />;
}

View File

@@ -2,6 +2,7 @@ import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react'
import { ASSETS, SCENE_NATIVE, AGENT_SEATS, AGENTS } from '../config/constants';
import AgentCard from './AgentCard';
import { getModelIcon } from '../utils/modelIcons';
import LobeModelLogo from './LobeModelLogo.jsx';
/**
* Custom hook to load an image
@@ -518,21 +519,23 @@ export default function RoomView({ bubbles, bubbleFor, leaderboard, agentProfile
{medal}
</span>
)}
{modelInfo.logoPath && (
<img
src={modelInfo.logoPath}
{(agentData?.modelName || modelInfo.logoPath) && (
<LobeModelLogo
model={agentData?.modelName}
provider={agentData?.modelProvider}
fallbackSrc={modelInfo.logoPath}
alt={modelInfo.provider}
size={25}
shape="circle"
type="color"
className="agent-model-badge"
style={{
position: 'absolute',
top: -12,
right: -12,
width: 25,
height: 25,
borderRadius: '50%',
border: '2px solid #ffffff',
background: '#ffffff',
objectFit: 'contain',
padding: 2,
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
pointerEvents: 'none'
@@ -642,10 +645,15 @@ export default function RoomView({ bubbles, bubbleFor, leaderboard, agentProfile
{/* Agent header with model icon */}
<div className="room-bubble-header">
{modelInfo.logoPath && (
<img
src={modelInfo.logoPath}
{(agentData?.modelName || modelInfo.logoPath) && (
<LobeModelLogo
model={agentData?.modelName}
provider={agentData?.modelProvider}
fallbackSrc={modelInfo.logoPath}
alt={modelInfo.provider}
size={18}
shape="circle"
type="color"
className="bubble-model-icon"
/>
)}

View File

@@ -2,6 +2,7 @@ import React, { useEffect, useMemo, useState } from 'react';
import { createPortal } from 'react-dom';
import JSZip from 'jszip';
import { getModelIcon, getShortModelName } from '../utils/modelIcons';
import LobeModelLogo from './LobeModelLogo.jsx';
export default function TraderView({
agents,
@@ -249,13 +250,16 @@ export default function TraderView({
alignItems: 'center',
gap: 10
}}>
{modelInfo.logoPath && (
<img
src={modelInfo.logoPath}
alt={modelInfo.provider}
style={{ width: 26, height: 26, borderRadius: 999 }}
/>
)}
<LobeModelLogo
model={profile.model_name}
provider={profile.model_provider}
fallbackSrc={modelInfo.logoPath}
alt={modelInfo.provider}
size={26}
shape="circle"
type="color"
style={{ borderRadius: 999 }}
/>
<div style={{ display: 'grid', gap: 2 }}>
<div style={{ fontSize: 11, color: '#4B5563', fontWeight: 700 }}>模型</div>
<div style={{ fontSize: 12, color: '#111111', fontWeight: 800 }}>

View File

@@ -58,6 +58,92 @@ export function useOpenClawPanel() {
});
}, []);
const resolveSession = useCallback(({ agentId, label = null, channel = null, includeGlobal = true }) => {
const store = getStore();
const client = store.clientRef?.current;
if (!client) return;
store.setChatError?.(null);
sendWithRetry({ current: client }, {
type: "openclaw_resolve_session",
agent_id: agentId,
label,
channel,
include_global: includeGlobal,
});
}, []);
const createSession = useCallback(({ agentId, label = null, model = null, initialMessage = null }) => {
const store = getStore();
const client = store.clientRef?.current;
if (!client || !agentId) return;
store.setChatError?.(null);
sendWithRetry({ current: client }, {
type: "openclaw_create_session",
agent_id: agentId,
label,
model,
initial_message: initialMessage,
});
}, []);
const subscribeSession = useCallback((sessionKey) => {
const client = getStore().clientRef?.current;
if (!client || !sessionKey) return;
sendWithRetry({ current: client }, {
type: "openclaw_subscribe_session",
session_key: sessionKey,
});
}, []);
const unsubscribeSession = useCallback((sessionKey) => {
const client = getStore().clientRef?.current;
if (!client || !sessionKey) return;
sendWithRetry({ current: client }, {
type: "openclaw_unsubscribe_session",
session_key: sessionKey,
});
}, []);
const resetSession = useCallback((sessionKey) => {
const store = getStore();
const client = store.clientRef?.current;
if (!client || !sessionKey) return;
store.setChatError?.(null);
sendWithRetry({ current: client }, {
type: "openclaw_reset_session",
session_key: sessionKey,
});
}, []);
const deleteSession = useCallback((sessionKey) => {
const store = getStore();
const client = store.clientRef?.current;
if (!client || !sessionKey) return;
store.setChatError?.(null);
sendWithRetry({ current: client }, {
type: "openclaw_delete_session",
session_key: sessionKey,
});
}, []);
const sendSessionMessage = useCallback((sessionKey, message, thinking = null) => {
const store = getStore();
const client = store.clientRef?.current;
if (!client || !sessionKey || !message?.trim()) return;
sendWithRetry({ current: client }, {
type: "openclaw_subscribe_session",
session_key: sessionKey,
});
store.setOpenclawChatSendingForSession?.(sessionKey, true);
store.setChatError?.(null);
sendWithRetry({ current: client }, {
type: "openclaw_send_message",
session_key: sessionKey,
message: message.trim(),
thinking,
});
}, []);
const requestCron = useCallback(() => {
const store = getStore();
const client = store.clientRef?.current;
@@ -91,13 +177,13 @@ export function useOpenClawPanel() {
sendWithRetry({ current: client }, { type: "get_openclaw_agents_presence" });
}, []);
const requestSkills = useCallback(() => {
const requestSkills = useCallback((agentId = null) => {
const store = getStore();
const client = store.clientRef?.current;
if (!client) return;
store.setSkillsLoading(true);
store.setSkillsError(null);
sendWithRetry({ current: client }, { type: "get_openclaw_skills" });
sendWithRetry({ current: client }, { type: "get_openclaw_skills", agent_id: agentId });
}, []);
const requestModels = useCallback(() => {
@@ -239,6 +325,13 @@ export function useOpenClawPanel() {
requestSessions,
requestSessionDetail,
requestSessionHistory,
resolveSession,
createSession,
subscribeSession,
unsubscribeSession,
resetSession,
deleteSession,
sendSessionMessage,
requestCron,
requestApprovals,
requestAgents,

View File

@@ -66,6 +66,306 @@ function buildTickersFromSymbols(symbols, previousTickers = []) {
});
}
function normalizeOpenClawHistoryItems(history) {
if (!Array.isArray(history)) {
return [];
}
return history
.map((item, index) => {
const role = item?.role || item?.senderRole || item?.kind || item?.type || 'event';
const isFinal = hasOpenClawFinalTag(item);
const text = extractOpenClawText(item);
if (!shouldKeepOpenClawMessage(item)) {
return null;
}
const timestamp = item?.timestamp || item?.ts || item?.createdAt || item?.time || null;
const nestedMeta = item?.message?.__openclaw || item?.__openclaw || null;
const seq = item?.messageSeq ?? item?.seq ?? nestedMeta?.seq ?? null;
const messageId = item?.messageId ?? item?.id ?? nestedMeta?.id ?? null;
return {
id: messageId || (seq !== null ? `seq:${seq}` : `${timestamp || 'history'}:${index}`),
role,
text: String(text || ''),
timestamp,
seq,
messageId,
isFinal,
raw: item,
};
})
.filter(Boolean);
}
function unwrapOpenClawFinal(value) {
if (typeof value !== 'string') {
return null;
}
const match = value.match(/<final>([\s\S]*?)<\/final>/i);
if (!match) {
return null;
}
return match[1].trim();
}
function stripOpenClawFinalTags(value) {
if (typeof value !== 'string') {
return value ? String(value) : '';
}
return value.replace(/<\/?final>/gi, '').trim();
}
function shouldHideOpenClawMessage({ role, text }) {
const normalizedRole = String(role || '').toLowerCase();
const normalizedText = String(text || '').trim();
if (normalizedRole === 'system') {
return true;
}
if (normalizedRole === 'user') {
if (normalizedText.startsWith('Sender (untrusted metadata):')) {
return true;
}
if (normalizedText.startsWith('[Fri ') || normalizedText.startsWith('[Sat ') || normalizedText.startsWith('[Sun ')
|| normalizedText.startsWith('[Mon ') || normalizedText.startsWith('[Tue ') || normalizedText.startsWith('[Wed ')
|| normalizedText.startsWith('[Thu ')) {
return true;
}
}
return false;
}
function shouldKeepOpenClawMessage(item) {
const role = item?.role || item?.senderRole || item?.kind || item?.type || 'event';
const text = extractOpenClawText(item);
const isFinal = hasOpenClawFinalTag(item);
if (shouldHideOpenClawMessage({ role, text })) {
return false;
}
const normalizedRole = String(role || '').toLowerCase();
if (normalizedRole === 'assistant') {
return isFinal;
}
if (!normalizedRole || normalizedRole === 'event') {
return isFinal;
}
return true;
}
function hasOpenClawFinalTag(item) {
if (typeof item === 'string') {
return /<final>[\s\S]*?<\/final>/i.test(item);
}
if (!item || typeof item !== 'object') {
return false;
}
const candidates = [];
if (typeof item.text === 'string') candidates.push(item.text);
if (typeof item.message === 'string') candidates.push(item.message);
if (typeof item.content === 'string') candidates.push(item.content);
const nestedMessage = item.message && typeof item.message === 'object' ? item.message : null;
if (nestedMessage) {
if (typeof nestedMessage.content === 'string') candidates.push(nestedMessage.content);
if (Array.isArray(nestedMessage.content)) {
nestedMessage.content.forEach((entry) => {
if (typeof entry === 'string') candidates.push(entry);
if (entry?.type === 'text' && typeof entry?.text === 'string') candidates.push(entry.text);
});
}
}
if (Array.isArray(item.content)) {
item.content.forEach((entry) => {
if (typeof entry === 'string') candidates.push(entry);
if (entry?.type === 'text' && typeof entry?.text === 'string') candidates.push(entry.text);
});
}
return candidates.some((value) => /<final>[\s\S]*?<\/final>/i.test(value));
}
function extractOpenClawText(item) {
if (typeof item === 'string') {
return unwrapOpenClawFinal(item) || stripOpenClawFinalTags(item);
}
if (!item || typeof item !== 'object') {
return item ? String(item) : '';
}
if (typeof item.text === 'string' && item.text.trim()) {
return unwrapOpenClawFinal(item.text) || stripOpenClawFinalTags(item.text);
}
if (typeof item.message === 'string' && item.message.trim()) {
return unwrapOpenClawFinal(item.message) || stripOpenClawFinalTags(item.message);
}
if (typeof item.content === 'string' && item.content.trim()) {
return unwrapOpenClawFinal(item.content) || stripOpenClawFinalTags(item.content);
}
const nestedMessage = item.message && typeof item.message === 'object' ? item.message : null;
if (nestedMessage) {
if (typeof nestedMessage.content === 'string' && nestedMessage.content.trim()) {
return unwrapOpenClawFinal(nestedMessage.content) || stripOpenClawFinalTags(nestedMessage.content);
}
if (Array.isArray(nestedMessage.content)) {
const textBlock = nestedMessage.content.find((entry) => entry?.type === 'text' && typeof entry?.text === 'string');
if (textBlock?.text) {
return unwrapOpenClawFinal(textBlock.text) || stripOpenClawFinalTags(textBlock.text);
}
}
}
if (Array.isArray(item.content)) {
const textParts = item.content
.map((entry) => {
if (typeof entry === 'string') return entry;
if (entry?.type === 'text' && typeof entry?.text === 'string') return entry.text;
return '';
})
.filter(Boolean);
if (textParts.length > 0) {
const merged = textParts.join('\n');
return unwrapOpenClawFinal(merged) || stripOpenClawFinalTags(merged);
}
}
if (typeof item.summary === 'string' && item.summary.trim()) {
return item.summary;
}
if (typeof item.value === 'string' && item.value.trim()) {
return item.value;
}
return JSON.stringify(item);
}
function normalizeOpenClawLiveEvent(evt) {
const payload = evt?.payload || {};
const nestedMessage = payload?.message && typeof payload.message === 'object' ? payload.message : null;
const nestedMeta = nestedMessage?.__openclaw || payload?.__openclaw || null;
const isFinal = hasOpenClawFinalTag(payload);
const text = extractOpenClawText(payload) || evt?.event || '';
const role =
payload.role
|| nestedMessage?.role
|| payload.senderRole
|| payload.kind
|| evt?.event
|| 'event';
const seq = payload.messageSeq ?? payload.seq ?? nestedMeta?.seq ?? null;
const messageId = payload.messageId ?? payload.id ?? nestedMeta?.id ?? null;
return {
id: messageId || (seq !== null ? `seq:${seq}` : `${evt?.event || 'event'}:${Date.now()}`),
role,
text: String(text),
timestamp: payload.timestamp || payload.ts || new Date().toISOString(),
seq,
messageId,
isFinal,
raw: payload,
};
}
function shouldAppendOpenClawLiveEvent(evt) {
const name = String(evt?.event || '');
const payload = evt?.payload || {};
if (name === 'session.message') {
return shouldKeepOpenClawMessage(payload);
}
return Boolean(payload.text || payload.message || payload.content);
}
function requestOpenClawSessionHistory(clientRef, sessionKey, limit = 30) {
const client = clientRef?.current;
if (!client || !sessionKey) {
return false;
}
return client.send(JSON.stringify({
type: 'get_openclaw_session_history',
session_key: sessionKey,
limit,
}));
}
function normalizeOpenClawAgents(agents, presence, sessionsPayload = null) {
const normalizedAgents = Array.isArray(agents) ? agents : [];
const presenceAgents = presence?.agents || presence || {};
const sessionDefaults = sessionsPayload?.defaults || {};
const sessions = Array.isArray(sessionsPayload?.sessions) ? sessionsPayload.sessions : [];
const sessionModelByAgent = new Map();
sessions.forEach((session) => {
if (!session || typeof session !== 'object') return;
let agentId = String(session.agentId || session.agent_id || '').trim();
if (!agentId) {
const key = String(session.key || session.sessionKey || '').trim();
const parts = key.split(':');
if (parts.length >= 3 && parts[0] === 'agent') {
agentId = parts[1];
}
}
const modelValue =
session.model ||
session.modelName ||
session.model_name ||
session.resolvedModel ||
session.resolved_model ||
null;
if (agentId && modelValue && !sessionModelByAgent.has(agentId)) {
sessionModelByAgent.set(agentId, modelValue);
}
});
return normalizedAgents.map((agent) => {
if (!agent || typeof agent !== 'object') {
return agent;
}
const agentId = String(agent.id || agent.agentId || '').trim();
const presenceEntry = agentId ? presenceAgents?.[agentId] : null;
const presenceSessions = Array.isArray(presenceEntry?.sessions) ? presenceEntry.sessions : [];
const firstPresenceSession = presenceSessions.find((session) => {
const value = session?.model || session?.modelName || session?.model_name || session?.resolvedModel;
return typeof value === 'string' && value.trim();
});
const model =
agent.model ||
agent.modelName ||
agent.model_name ||
agent.resolvedModel ||
agent.resolved_model ||
agent.defaultModel ||
agent.default_model ||
sessionModelByAgent.get(agentId) ||
sessionDefaults.model ||
sessionDefaults.modelName ||
sessionDefaults.model_name ||
firstPresenceSession?.model ||
firstPresenceSession?.modelName ||
firstPresenceSession?.model_name ||
firstPresenceSession?.resolvedModel ||
null;
return {
...agent,
model: typeof model === 'string' && model.trim() ? model.trim() : null,
};
});
}
/**
* Custom hook for WebSocket connection lifecycle and event handling.
* Manages clientRef, connection, and ALL event handlers.
@@ -805,7 +1105,15 @@ export function useWebSocketConnection({
useOpenClawStore.getState().setStatusLoading(false);
},
openclaw_sessions_loaded: (e) => {
useOpenClawStore.getState().setOpenclawSessions(e.data || e);
const payload = e.data || e;
useOpenClawStore.getState().setOpenclawSessions(payload);
const currentAgents = useOpenClawStore.getState().agents || [];
const presence = useOpenClawStore.getState().agentsPresence;
if (currentAgents.length > 0) {
useOpenClawStore.getState().setAgents(
normalizeOpenClawAgents(currentAgents, presence, payload),
);
}
useOpenClawStore.getState().setSessionsLoading(false);
},
openclaw_session_detail_loaded: (e) => {
@@ -813,7 +1121,120 @@ export function useWebSocketConnection({
useOpenClawStore.getState().setSessionDetailLoading(false);
},
openclaw_session_history_loaded: (e) => {
useOpenClawStore.getState().setOpenclawSessionHistory(e.data || e);
const data = e.data || e;
const sessionKey = e.session_key || data?.session_key || useOpenClawStore.getState().selectedSessionKey;
useOpenClawStore.getState().setOpenclawSessionHistory(data);
if (sessionKey) {
useOpenClawStore.getState().replaceOpenclawChatHistory(
sessionKey,
normalizeOpenClawHistoryItems(data?.history || []),
);
}
},
openclaw_session_resolved: (e) => {
const d = e.data || {};
useOpenClawStore.getState().setOpenclawResolvedSessionKey(d.key || null);
if (d?.error) {
useOpenClawStore.getState().setChatError(d.error);
} else {
useOpenClawStore.getState().setChatError(null);
}
},
openclaw_session_created: (e) => {
const d = e.data || {};
if (d?.error) {
useOpenClawStore.getState().setChatError(d.error);
return;
}
if (d?.entry || d?.key) {
const createdKey = d?.key || d?.entry?.key || d?.entry?.sessionKey || '';
useOpenClawStore.getState().appendOpenclawSession(
d.entry || {
key: createdKey,
sessionKey: createdKey,
agentId: String(createdKey).split(':')[1] || '',
}
);
}
if (d?.key) {
useOpenClawStore.getState().setSelectedSessionKey(d.key);
useOpenClawStore.getState().setOpenclawResolvedSessionKey(d.key);
useOpenClawStore.getState().setChatError(null);
}
},
openclaw_session_subscribed: (e) => {
const sessionKey = e.session_key || e.data?.key || null;
if (sessionKey) {
useOpenClawStore.getState().setOpenclawSessionSubscribed(sessionKey, true);
}
if (e.data?.error) {
useOpenClawStore.getState().setChatError(e.data.error);
}
},
openclaw_session_unsubscribed: (e) => {
const sessionKey = e.session_key || e.data?.key || null;
if (sessionKey) {
useOpenClawStore.getState().setOpenclawSessionSubscribed(sessionKey, false);
}
},
openclaw_session_reset: (e) => {
const sessionKey = e.session_key || e.data?.key || null;
if (e.data?.error) {
useOpenClawStore.getState().setChatError(e.data.error);
return;
}
if (sessionKey) {
useOpenClawStore.getState().replaceOpenclawChatHistory(sessionKey, []);
useOpenClawStore.getState().setChatError(null);
}
},
openclaw_session_deleted: (e) => {
const sessionKey = e.session_key || e.data?.key || null;
if (e.data?.error) {
useOpenClawStore.getState().setChatError(e.data.error);
return;
}
if (sessionKey) {
useOpenClawStore.getState().removeOpenclawSession(sessionKey);
useOpenClawStore.getState().setChatError(null);
}
},
openclaw_message_sent: (e) => {
const sessionKey = e.session_key || e.data?.key || useOpenClawStore.getState().selectedSessionKey;
if (sessionKey) {
useOpenClawStore.getState().setOpenclawChatSendingForSession(sessionKey, false);
}
if (e.data?.error) {
useOpenClawStore.getState().setChatError(e.data.error);
} else {
useOpenClawStore.getState().setChatError(null);
if (sessionKey && (e.data?.status || e.data?.runId || e.data?.messageSeq !== undefined)) {
const statusBits = [
e.data?.status ? `status=${e.data.status}` : null,
e.data?.runId ? `runId=${e.data.runId}` : null,
e.data?.messageSeq !== undefined ? `seq=${e.data.messageSeq}` : null,
].filter(Boolean);
useOpenClawStore.getState().appendOpenclawChatMessage(sessionKey, {
id: `send-meta:${e.data?.runId || Date.now()}`,
role: 'system',
text: `消息已提交到 OpenClaw${statusBits.length ? ` (${statusBits.join(', ')})` : ''}`,
timestamp: new Date().toISOString(),
});
}
if (sessionKey) {
window.setTimeout(() => requestOpenClawSessionHistory(clientRef, sessionKey, 30), 600);
}
}
},
openclaw_session_event: (e) => {
const sessionKey = e.session_key || e.payload?.sessionKey || e.payload?.key;
if (!sessionKey || !shouldAppendOpenClawLiveEvent(e)) {
return;
}
useOpenClawStore.getState().appendOpenclawChatMessage(
sessionKey,
normalizeOpenClawLiveEvent(e),
);
},
openclaw_cron_loaded: (e) => {
useOpenClawStore.getState().setOpenclawCronJobs(e.data || e);
@@ -829,12 +1250,30 @@ export function useWebSocketConnection({
if (d?.error) {
useOpenClawStore.getState().setAgentsError(d.error);
} else {
useOpenClawStore.getState().setAgents(d?.agents || []);
const presence = useOpenClawStore.getState().agentsPresence;
const sessionsPayload = {
sessions: useOpenClawStore.getState().openclawSessions || [],
defaults: useOpenClawStore.getState().openclawSessionsDefaults || null,
};
useOpenClawStore.getState().setAgents(
normalizeOpenClawAgents(d?.agents || [], presence, sessionsPayload),
);
useOpenClawStore.getState().setAgentsError(null);
}
},
openclaw_agents_presence_loaded: (e) => {
useOpenClawStore.getState().setAgentsPresence((e.data?.data ?? e.data) || {});
const presencePayload = (e.data?.data ?? e.data) || {};
useOpenClawStore.getState().setAgentsPresence(presencePayload);
const currentAgents = useOpenClawStore.getState().agents || [];
if (currentAgents.length > 0) {
const sessionsPayload = {
sessions: useOpenClawStore.getState().openclawSessions || [],
defaults: useOpenClawStore.getState().openclawSessionsDefaults || null,
};
useOpenClawStore.getState().setAgents(
normalizeOpenClawAgents(currentAgents, presencePayload, sessionsPayload),
);
}
},
openclaw_skills_loaded: (e) => {
useOpenClawStore.getState().setSkillsLoading(false);

View File

@@ -7,10 +7,16 @@ export const useOpenClawStore = create(
// Raw data
openclawStatus: null,
openclawSessions: [],
openclawSessionsDefaults: null,
openclawSessionDetail: null,
openclawSessionHistory: [],
openclawCronJobs: [],
openclawApprovals: [],
openclawResolvedSessionKey: null,
openclawChatMessagesBySession: {},
openclawChatDraftBySession: {},
openclawChatSendingBySession: {},
openclawSessionSubscriptions: {},
// Loading states
isStatusLoading: false,
@@ -18,6 +24,7 @@ export const useOpenClawStore = create(
isSessionDetailLoading: false,
isCronLoading: false,
isApprovalsLoading: false,
isChatSending: false,
// Error states
statusError: null,
@@ -25,6 +32,7 @@ export const useOpenClawStore = create(
sessionDetailError: null,
cronError: null,
approvalsError: null,
chatError: null,
// Agents state
agents: [],
@@ -119,11 +127,129 @@ export const useOpenClawStore = create(
// 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 }),
setOpenclawSessions: (data) => set({
openclawSessions: data?.sessions || [],
openclawSessionsDefaults: data?.defaults || null,
sessionsError: null,
}),
appendOpenclawSession: (session) => set((state) => {
const key = session?.key || session?.sessionKey;
if (!key) {
return {};
}
const existing = state.openclawSessions || [];
const deduped = existing.filter((item) => (item?.key || item?.sessionKey) !== key);
return { openclawSessions: [session, ...deduped] };
}),
removeOpenclawSession: (sessionKey) => set((state) => ({
openclawSessions: (state.openclawSessions || []).filter(
(item) => (item?.key || item?.sessionKey) !== sessionKey
),
selectedSessionKey:
state.selectedSessionKey === sessionKey ? null : state.selectedSessionKey,
})),
setOpenclawSessionDetail: (data) => set({ openclawSessionDetail: data?.session || null, sessionDetailError: data?.error || null }),
setOpenclawSessionHistory: (data) => set({ openclawSessionHistory: data?.history || [], sessionDetailError: data?.error || null }),
setOpenclawCronJobs: (data) => set({ openclawCronJobs: data?.cron || [], cronError: null }),
setOpenclawApprovals: (data) => set({ openclawApprovals: data?.approvals || [], approvalsError: null }),
setOpenclawResolvedSessionKey: (key) => set({ openclawResolvedSessionKey: key || null }),
setOpenclawChatDraft: (sessionKey, value) => set((state) => ({
openclawChatDraftBySession: { ...state.openclawChatDraftBySession, [sessionKey]: value },
})),
appendOpenclawChatMessage: (sessionKey, message) => set((state) => {
const current = state.openclawChatMessagesBySession[sessionKey] || [];
const sameMessageIndex = current.findIndex((item) => {
const sameId = Boolean(message?.id && item?.id && message.id === item.id);
const sameMessageId = Boolean(
message?.messageId &&
item?.messageId &&
message.messageId === item.messageId
);
const sameSeq = Boolean(
message?.seq !== undefined &&
message?.seq !== null &&
item?.seq !== undefined &&
item?.seq !== null &&
message.seq === item.seq &&
message?.role === item?.role
);
const incomingText = String(message?.text || '').trim();
const existingText = String(item?.text || '').trim();
const incomingTs = Date.parse(message?.timestamp || '');
const existingTs = Date.parse(item?.timestamp || '');
const nearInTime =
Number.isFinite(incomingTs) &&
Number.isFinite(existingTs) &&
Math.abs(incomingTs - existingTs) < 1500;
const sameAssistantText =
message?.role === 'assistant' &&
item?.role === 'assistant' &&
incomingText &&
existingText &&
(
incomingText === existingText ||
incomingText.startsWith(existingText) ||
existingText.startsWith(incomingText)
) &&
nearInTime;
return sameId || sameMessageId || sameSeq || sameAssistantText;
});
if (sameMessageIndex >= 0) {
const next = [...current];
next[sameMessageIndex] = { ...next[sameMessageIndex], ...message };
return {
openclawChatMessagesBySession: {
...state.openclawChatMessagesBySession,
[sessionKey]: next,
},
};
}
return {
openclawChatMessagesBySession: {
...state.openclawChatMessagesBySession,
[sessionKey]: [...current, message],
},
};
}),
replaceOpenclawChatHistory: (sessionKey, messages) => set((state) => {
const incoming = Array.isArray(messages) ? messages : [];
const existing = state.openclawChatMessagesBySession[sessionKey] || [];
const merged = [];
const seen = new Set();
const signatureFor = (message) => {
if (!message) return "";
if (message.id) return `id:${message.id}`;
if (message.messageId) return `mid:${message.messageId}`;
if (message.seq !== undefined && message.seq !== null) return `seq:${message.seq}`;
return `txt:${message.role || ""}:${String(message.text || "").trim()}`;
};
for (const message of [...incoming, ...existing]) {
const signature = signatureFor(message);
if (!signature || seen.has(signature)) {
continue;
}
seen.add(signature);
merged.push(message);
}
return {
openclawChatMessagesBySession: {
...state.openclawChatMessagesBySession,
[sessionKey]: merged,
},
};
}),
setOpenclawChatSendingForSession: (sessionKey, value) => set((state) => ({
openclawChatSendingBySession: { ...state.openclawChatSendingBySession, [sessionKey]: Boolean(value) },
isChatSending: Boolean(value),
})),
setOpenclawSessionSubscribed: (sessionKey, value) => set((state) => ({
openclawSessionSubscriptions: { ...state.openclawSessionSubscriptions, [sessionKey]: Boolean(value) },
})),
setSelectedSessionKey: (key) => set({ selectedSessionKey: key }),
@@ -138,6 +264,7 @@ export const useOpenClawStore = create(
setSessionDetailError: (e) => set({ sessionDetailError: e }),
setCronError: (e) => set({ cronError: e }),
setApprovalsError: (e) => set({ approvalsError: e }),
setChatError: (e) => set({ chatError: e }),
setAgents: (agents) => set({ agents }),
setAgentsLoading: (loading) => set({ agentsLoading: loading }),