Files
zclaw_openfang/desktop/src/domains/chat/store.ts
iven 3ff08faa56 release(v0.2.0): streaming, MCP protocol, Browser Hand, security enhancements
## Major Features

### Streaming Response System
- Implement LlmDriver trait with `stream()` method returning async Stream
- Add SSE parsing for Anthropic and OpenAI API streaming
- Integrate Tauri event system for frontend streaming (`stream:chunk` events)
- Add StreamChunk types: Delta, ToolStart, ToolEnd, Complete, Error

### MCP Protocol Implementation
- Add MCP JSON-RPC 2.0 types (mcp_types.rs)
- Implement stdio-based MCP transport (mcp_transport.rs)
- Support tool discovery, execution, and resource operations

### Browser Hand Implementation
- Complete browser automation with Playwright-style actions
- Support Navigate, Click, Type, Scrape, Screenshot, Wait actions
- Add educational Hands: Whiteboard, Slideshow, Speech, Quiz

### Security Enhancements
- Implement command whitelist/blacklist for shell_exec tool
- Add SSRF protection with private IP blocking
- Create security.toml configuration file

## Test Improvements
- Fix test import paths (security-utils, setup)
- Fix vi.mock hoisting issues with vi.hoisted()
- Update test expectations for validateUrl and sanitizeFilename
- Add getUnsupportedLocalGatewayStatus mock

## Documentation Updates
- Update architecture documentation
- Improve configuration reference
- Add quick-start guide updates

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 03:24:24 +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-4-flash',
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);
});
}