Files
zclaw_openfang/desktop/src/domains/chat/store.ts
iven 7ffd5e1531 feat(domains): create domain-driven architecture for Phase 2
Chat Domain:
- Add types.ts with Message, Conversation, Agent types
- Add store.ts with Valtio-based state management
- Add hooks.ts with useChatState, useMessages, etc.
- Add index.ts for public API export

Hands Domain:
- Add types.ts with Hand, Trigger, Approval types
- Add machine.ts with XState state machine
- Add store.ts with Valtio-based state management
- Add hooks.ts with useHands, useApprovalQueue, etc.

Shared Module:
- Add types.ts with Result, AsyncResult, PaginatedResponse
- Add error-handling.ts with AppError, NetworkError, etc.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 19:47:48 +08:00

223 lines
5.9 KiB
TypeScript

/**
* Chat Domain Store
*
* Valtio-based state management for chat.
* Replaces Zustand for better performance with fine-grained reactivity.
*/
import { proxy, subscribe } from 'valtio';
import type { Message, Conversation, Agent, AgentProfileLike } from './types';
// === Constants ===
const DEFAULT_AGENT: Agent = {
id: '1',
name: 'ZCLAW',
icon: '🦞',
color: 'bg-gradient-to-br from-orange-500 to-red-500',
lastMessage: '发送消息开始对话',
time: '',
};
// === Helper Functions ===
function generateConvId(): string {
return `conv_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
}
function deriveTitle(messages: Message[]): string {
const firstUser = messages.find(m => m.role === 'user');
if (firstUser) {
const text = firstUser.content.trim();
return text.length > 30 ? text.slice(0, 30) + '...' : text;
}
return '新对话';
}
export function toChatAgent(profile: AgentProfileLike): Agent {
return {
id: profile.id,
name: profile.name,
icon: profile.nickname?.slice(0, 1) || profile.name.slice(0, 1) || '🦞',
color: 'bg-gradient-to-br from-orange-500 to-red-500',
lastMessage: profile.role || '新分身',
time: '',
};
}
// === Store Interface ===
export interface ChatStore {
// State
messages: Message[];
conversations: Conversation[];
currentConversationId: string | null;
agents: Agent[];
currentAgent: Agent | null;
isStreaming: boolean;
currentModel: string;
sessionKey: string | null;
// Actions
addMessage: (message: Message) => void;
updateMessage: (id: string, updates: Partial<Message>) => void;
deleteMessage: (id: string) => void;
setCurrentAgent: (agent: Agent) => void;
syncAgents: (profiles: AgentProfileLike[]) => void;
setCurrentModel: (model: string) => void;
setStreaming: (streaming: boolean) => void;
setSessionKey: (key: string | null) => void;
newConversation: () => void;
switchConversation: (id: string) => void;
deleteConversation: (id: string) => void;
clearMessages: () => void;
}
// === Create Proxy State ===
export const chatStore = proxy<ChatStore>({
// Initial state
messages: [],
conversations: [],
currentConversationId: null,
agents: [DEFAULT_AGENT],
currentAgent: DEFAULT_AGENT,
isStreaming: false,
currentModel: 'glm-5',
sessionKey: null,
// === Actions ===
addMessage: (message: Message) => {
chatStore.messages.push(message);
},
updateMessage: (id: string, updates: Partial<Message>) => {
const msg = chatStore.messages.find(m => m.id === id);
if (msg) {
Object.assign(msg, updates);
}
},
deleteMessage: (id: string) => {
const index = chatStore.messages.findIndex(m => m.id === id);
if (index >= 0) {
chatStore.messages.splice(index, 1);
}
},
setCurrentAgent: (agent: Agent) => {
chatStore.currentAgent = agent;
},
syncAgents: (profiles: AgentProfileLike[]) => {
if (profiles.length === 0) {
chatStore.agents = [DEFAULT_AGENT];
} else {
chatStore.agents = profiles.map(toChatAgent);
}
},
setCurrentModel: (model: string) => {
chatStore.currentModel = model;
},
setStreaming: (streaming: boolean) => {
chatStore.isStreaming = streaming;
},
setSessionKey: (key: string | null) => {
chatStore.sessionKey = key;
},
newConversation: () => {
// Save current conversation if has messages
if (chatStore.messages.length > 0) {
const conversation: Conversation = {
id: chatStore.currentConversationId || generateConvId(),
title: deriveTitle(chatStore.messages),
messages: [...chatStore.messages],
sessionKey: chatStore.sessionKey,
agentId: chatStore.currentAgent?.id || null,
createdAt: new Date(),
updatedAt: new Date(),
};
// Check if conversation already exists
const existingIndex = chatStore.conversations.findIndex(
c => c.id === chatStore.currentConversationId
);
if (existingIndex >= 0) {
chatStore.conversations[existingIndex] = conversation;
} else {
chatStore.conversations.unshift(conversation);
}
}
// Reset for new conversation
chatStore.messages = [];
chatStore.sessionKey = null;
chatStore.isStreaming = false;
chatStore.currentConversationId = null;
},
switchConversation: (id: string) => {
const conv = chatStore.conversations.find(c => c.id === id);
if (conv) {
// Save current first
if (chatStore.messages.length > 0) {
const currentConv: Conversation = {
id: chatStore.currentConversationId || generateConvId(),
title: deriveTitle(chatStore.messages),
messages: [...chatStore.messages],
sessionKey: chatStore.sessionKey,
agentId: chatStore.currentAgent?.id || null,
createdAt: new Date(),
updatedAt: new Date(),
};
const existingIndex = chatStore.conversations.findIndex(
c => c.id === chatStore.currentConversationId
);
if (existingIndex >= 0) {
chatStore.conversations[existingIndex] = currentConv;
} else {
chatStore.conversations.unshift(currentConv);
}
}
// Switch to new
chatStore.messages = [...conv.messages];
chatStore.sessionKey = conv.sessionKey;
chatStore.currentConversationId = conv.id;
}
},
deleteConversation: (id: string) => {
const index = chatStore.conversations.findIndex(c => c.id === id);
if (index >= 0) {
chatStore.conversations.splice(index, 1);
// If deleting current, clear messages
if (chatStore.currentConversationId === id) {
chatStore.messages = [];
chatStore.sessionKey = null;
chatStore.currentConversationId = null;
}
}
},
clearMessages: () => {
chatStore.messages = [];
},
});
// === Dev Mode Logging ===
if (import.meta.env.DEV) {
subscribe(chatStore, (ops) => {
console.log('[ChatStore] Changes:', ops);
});
}