## 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>
223 lines
5.9 KiB
TypeScript
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);
|
|
});
|
|
}
|