chore: 提交所有工作进度 — SaaS 后端增强、Admin UI、桌面端集成
包含大量 SaaS 平台改进、Admin 管理后台更新、桌面端集成完善、 文档同步、测试文件重构等内容。为 QA 测试准备干净工作树。
This commit is contained in:
@@ -8,6 +8,7 @@ import { getSkillDiscovery } from '../lib/skill-discovery';
|
||||
import { useOfflineStore, isOffline } from './offlineStore';
|
||||
import { useConnectionStore } from './connectionStore';
|
||||
import { createLogger } from '../lib/logger';
|
||||
import { generateRandomString } from '../lib/crypto-utils';
|
||||
|
||||
const log = createLogger('ChatStore');
|
||||
|
||||
@@ -106,7 +107,7 @@ interface ChatState {
|
||||
}
|
||||
|
||||
function generateConvId(): string {
|
||||
return `conv_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
||||
return `conv_${Date.now()}_${generateRandomString(4)}`;
|
||||
}
|
||||
|
||||
function deriveTitle(messages: Message[]): string {
|
||||
@@ -224,7 +225,12 @@ export const useChatStore = create<ChatState>()(
|
||||
const conversations = upsertActiveConversation([...state.conversations], state);
|
||||
|
||||
// Try to find existing conversation for this agent
|
||||
const agentConversation = conversations.find(c => c.agentId === agent.id);
|
||||
// DEFAULT_AGENT conversations are stored with agentId: null (via resolveConversationAgentId),
|
||||
// so we need to match both the agent's ID and null for default agent lookups.
|
||||
const agentConversation = conversations.find(c =>
|
||||
c.agentId === agent.id ||
|
||||
(agent.id === DEFAULT_AGENT.id && c.agentId === null)
|
||||
);
|
||||
|
||||
if (agentConversation) {
|
||||
// Restore the agent's previous conversation
|
||||
@@ -251,7 +257,11 @@ export const useChatStore = create<ChatState>()(
|
||||
|
||||
syncAgents: (profiles) =>
|
||||
set((state) => {
|
||||
const agents = profiles.length > 0 ? profiles.map(toChatAgent) : [DEFAULT_AGENT];
|
||||
const cloneAgents = profiles.length > 0 ? profiles.map(toChatAgent) : [];
|
||||
// Always include DEFAULT_AGENT so users can switch back to default conversations
|
||||
const agents = cloneAgents.length > 0
|
||||
? [DEFAULT_AGENT, ...cloneAgents]
|
||||
: [DEFAULT_AGENT];
|
||||
const currentAgent = state.currentConversationId
|
||||
? resolveAgentForConversation(
|
||||
state.conversations.find((conversation) => conversation.id === state.currentConversationId)?.agentId || null,
|
||||
@@ -260,7 +270,20 @@ export const useChatStore = create<ChatState>()(
|
||||
: state.currentAgent
|
||||
? agents.find((agent) => agent.id === state.currentAgent?.id) || agents[0]
|
||||
: agents[0];
|
||||
return { agents, currentAgent };
|
||||
|
||||
// Safety net: if rehydration failed to restore messages (onRehydrateStorage
|
||||
// direct mutation doesn't trigger re-renders), restore them here via set().
|
||||
let messages = state.messages;
|
||||
let sessionKey = state.sessionKey;
|
||||
if (messages.length === 0 && state.currentConversationId && state.conversations.length > 0) {
|
||||
const conv = state.conversations.find(c => c.id === state.currentConversationId);
|
||||
if (conv && conv.messages.length > 0) {
|
||||
messages = conv.messages.map(m => ({ ...m }));
|
||||
sessionKey = conv.sessionKey;
|
||||
}
|
||||
}
|
||||
|
||||
return { agents, currentAgent, messages, sessionKey };
|
||||
}),
|
||||
|
||||
setCurrentModel: (model) => set({ currentModel: model }),
|
||||
@@ -307,7 +330,7 @@ export const useChatStore = create<ChatState>()(
|
||||
|
||||
sendMessage: async (content: string) => {
|
||||
const { addMessage, currentAgent, sessionKey } = get();
|
||||
const effectiveSessionKey = sessionKey || `session_${Date.now()}`;
|
||||
const effectiveSessionKey = sessionKey || crypto.randomUUID();
|
||||
const effectiveAgentId = resolveGatewayAgentId(currentAgent);
|
||||
const agentId = currentAgent?.id || 'zclaw-main';
|
||||
|
||||
@@ -413,7 +436,7 @@ export const useChatStore = create<ChatState>()(
|
||||
},
|
||||
onTool: (tool: string, input: string, output: string) => {
|
||||
const toolMsg: Message = {
|
||||
id: `tool_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
||||
id: `tool_${Date.now()}_${generateRandomString(4)}`,
|
||||
role: 'tool',
|
||||
content: output || input,
|
||||
timestamp: new Date(),
|
||||
@@ -426,7 +449,7 @@ export const useChatStore = create<ChatState>()(
|
||||
},
|
||||
onHand: (name: string, status: string, result?: unknown) => {
|
||||
const handMsg: Message = {
|
||||
id: `hand_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
||||
id: `hand_${Date.now()}_${generateRandomString(4)}`,
|
||||
role: 'hand',
|
||||
content: result
|
||||
? (typeof result === 'string' ? result : JSON.stringify(result, null, 2))
|
||||
@@ -588,7 +611,7 @@ export const useChatStore = create<ChatState>()(
|
||||
}));
|
||||
} else if (delta.stream === 'tool') {
|
||||
const toolMsg: Message = {
|
||||
id: `tool_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
||||
id: `tool_${Date.now()}_${generateRandomString(4)}`,
|
||||
role: 'tool',
|
||||
content: delta.toolOutput || '',
|
||||
timestamp: new Date(),
|
||||
@@ -616,7 +639,7 @@ export const useChatStore = create<ChatState>()(
|
||||
} else if (delta.stream === 'hand') {
|
||||
// Handle Hand trigger events from ZCLAW
|
||||
const handMsg: Message = {
|
||||
id: `hand_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
||||
id: `hand_${Date.now()}_${generateRandomString(4)}`,
|
||||
role: 'hand',
|
||||
content: delta.handResult
|
||||
? (typeof delta.handResult === 'string' ? delta.handResult : JSON.stringify(delta.handResult, null, 2))
|
||||
@@ -631,7 +654,7 @@ export const useChatStore = create<ChatState>()(
|
||||
} else if (delta.stream === 'workflow') {
|
||||
// Handle Workflow execution events from ZCLAW
|
||||
const workflowMsg: Message = {
|
||||
id: `workflow_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
||||
id: `workflow_${Date.now()}_${generateRandomString(4)}`,
|
||||
role: 'workflow',
|
||||
content: delta.workflowResult
|
||||
? (typeof delta.workflowResult === 'string' ? delta.workflowResult : JSON.stringify(delta.workflowResult, null, 2))
|
||||
@@ -671,12 +694,18 @@ export const useChatStore = create<ChatState>()(
|
||||
}
|
||||
}
|
||||
|
||||
// Restore messages from current conversation if exists
|
||||
// Restore messages from current conversation via setState() to properly
|
||||
// trigger subscriber re-renders. Direct mutation (state.messages = ...)
|
||||
// does NOT notify zustand subscribers, leaving the UI stuck on [].
|
||||
// Safe to reference useChatStore here because zustand persist runs
|
||||
// hydration via setTimeout(1ms), so the store is fully created by
|
||||
// the time this callback executes.
|
||||
if (state?.currentConversationId && state.conversations) {
|
||||
const currentConv = state.conversations.find(c => c.id === state.currentConversationId);
|
||||
if (currentConv) {
|
||||
state.messages = [...currentConv.messages];
|
||||
state.sessionKey = currentConv.sessionKey;
|
||||
if (currentConv && currentConv.messages.length > 0) {
|
||||
const messages = currentConv.messages.map(m => ({ ...m }));
|
||||
const sessionKey = currentConv.sessionKey;
|
||||
useChatStore.setState({ messages, sessionKey });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user