chore: 提交所有工作进度 — SaaS 后端增强、Admin UI、桌面端集成

包含大量 SaaS 平台改进、Admin 管理后台更新、桌面端集成完善、
文档同步、测试文件重构等内容。为 QA 测试准备干净工作树。
This commit is contained in:
iven
2026-03-29 10:46:26 +08:00
parent 9a5fad2b59
commit 5fdf96c3f5
268 changed files with 22011 additions and 3886 deletions

View File

@@ -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 });
}
}
},