From 7ffd5e1531127387f76b0f690a13683ed3417882 Mon Sep 17 00:00:00 2001 From: iven Date: Sat, 21 Mar 2026 19:47:48 +0800 Subject: [PATCH] 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 --- desktop/src/domains/chat/hooks.ts | 76 ++ desktop/src/domains/chat/index.ts | 48 + desktop/src/domains/chat/store.ts | 222 ++++ desktop/src/domains/chat/types.ts | 81 ++ desktop/src/domains/hands/hooks.ts | 79 ++ desktop/src/domains/hands/index.ts | 51 + desktop/src/domains/hands/machine.ts | 166 +++ desktop/src/domains/hands/store.ts | 105 ++ desktop/src/domains/hands/types.ts | 123 ++ desktop/src/shared/error-handling.ts | 105 ++ desktop/src/shared/index.ts | 31 + desktop/src/shared/types.ts | 58 + ...2026-03-21-phase2-domain-reorganization.md | 1174 +++++++++++++++++ 13 files changed, 2319 insertions(+) create mode 100644 desktop/src/domains/chat/hooks.ts create mode 100644 desktop/src/domains/chat/index.ts create mode 100644 desktop/src/domains/chat/store.ts create mode 100644 desktop/src/domains/chat/types.ts create mode 100644 desktop/src/domains/hands/hooks.ts create mode 100644 desktop/src/domains/hands/index.ts create mode 100644 desktop/src/domains/hands/machine.ts create mode 100644 desktop/src/domains/hands/store.ts create mode 100644 desktop/src/domains/hands/types.ts create mode 100644 desktop/src/shared/error-handling.ts create mode 100644 desktop/src/shared/index.ts create mode 100644 desktop/src/shared/types.ts create mode 100644 docs/superpowers/plans/2026-03-21-phase2-domain-reorganization.md diff --git a/desktop/src/domains/chat/hooks.ts b/desktop/src/domains/chat/hooks.ts new file mode 100644 index 0000000..9de9d89 --- /dev/null +++ b/desktop/src/domains/chat/hooks.ts @@ -0,0 +1,76 @@ +/** + * Chat Domain Hooks + * + * React hooks for accessing chat state with Valtio. + * Only re-renders when accessed properties change. + */ +import { useSnapshot } from 'valtio'; +import { chatStore } from './store'; +import type { Message, Agent, Conversation } from './types'; + +/** + * Hook to access the full chat state snapshot. + * Only re-renders when accessed properties change. + */ +export function useChatState() { + return useSnapshot(chatStore); +} + +/** + * Hook to access messages only. + * Only re-renders when messages change. + */ +export function useMessages(): readonly Message[] { + const { messages } = useSnapshot(chatStore); + return messages; +} + +/** + * Hook to access streaming state. + * Only re-renders when isStreaming changes. + */ +export function useIsStreaming(): boolean { + const { isStreaming } = useSnapshot(chatStore); + return isStreaming; +} + +/** + * Hook to access current agent. + */ +export function useCurrentAgent(): Agent | null { + const { currentAgent } = useSnapshot(chatStore); + return currentAgent; +} + +/** + * Hook to access all agents. + */ +export function useAgents(): readonly Agent[] { + const { agents } = useSnapshot(chatStore); + return agents; +} + +/** + * Hook to access conversations. + */ +export function useConversations(): readonly Conversation[] { + const { conversations } = useSnapshot(chatStore); + return conversations; +} + +/** + * Hook to access current model. + */ +export function useCurrentModel(): string { + const { currentModel } = useSnapshot(chatStore); + return currentModel; +} + +/** + * Hook to access chat actions. + * Returns the store directly for calling actions. + * Does not cause re-renders. + */ +export function useChatActions() { + return chatStore; +} diff --git a/desktop/src/domains/chat/index.ts b/desktop/src/domains/chat/index.ts new file mode 100644 index 0000000..4474782 --- /dev/null +++ b/desktop/src/domains/chat/index.ts @@ -0,0 +1,48 @@ +/** + * Chat Domain + * + * Core chat functionality including messaging, conversations, and agents. + * + * @example + * // Using hooks (recommended) + * import { useMessages, useChatActions } from '@/domains/chat'; + * + * function ChatComponent() { + * const messages = useMessages(); + * const { addMessage } = useChatActions(); + * // ... + * } + * + * @example + * // Using store directly (for actions) + * import { chatStore } from '@/domains/chat'; + * + * chatStore.addMessage({ id: '1', role: 'user', content: 'Hello', timestamp: new Date() }); + */ + +// Types +export type { + Message, + MessageFile, + CodeBlock, + Conversation, + Agent, + AgentProfileLike, + ChatState, +} from './types'; + +// Store +export { chatStore, toChatAgent } from './store'; +export type { ChatStore } from './store'; + +// Hooks +export { + useChatState, + useMessages, + useIsStreaming, + useCurrentAgent, + useAgents, + useConversations, + useCurrentModel, + useChatActions, +} from './hooks'; diff --git a/desktop/src/domains/chat/store.ts b/desktop/src/domains/chat/store.ts new file mode 100644 index 0000000..6a12caf --- /dev/null +++ b/desktop/src/domains/chat/store.ts @@ -0,0 +1,222 @@ +/** + * 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) => 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({ + // 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) => { + 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); + }); +} diff --git a/desktop/src/domains/chat/types.ts b/desktop/src/domains/chat/types.ts new file mode 100644 index 0000000..12ee23b --- /dev/null +++ b/desktop/src/domains/chat/types.ts @@ -0,0 +1,81 @@ +/** + * Chat Domain Types + * + * Core types for the chat system. + * Extracted from chatStore.ts for domain-driven organization. + */ + +export interface MessageFile { + name: string; + path?: string; + size?: number; + type?: string; +} + +export interface CodeBlock { + language?: string; + filename?: string; + content?: string; +} + +export interface Message { + id: string; + role: 'user' | 'assistant' | 'tool' | 'hand' | 'workflow'; + content: string; + timestamp: Date; + runId?: string; + streaming?: boolean; + toolName?: string; + toolInput?: string; + toolOutput?: string; + error?: string; + // Hand event fields + handName?: string; + handStatus?: string; + handResult?: unknown; + // Workflow event fields + workflowId?: string; + workflowStep?: string; + workflowStatus?: string; + workflowResult?: unknown; + // Output files and code blocks + files?: MessageFile[]; + codeBlocks?: CodeBlock[]; +} + +export interface Conversation { + id: string; + title: string; + messages: Message[]; + sessionKey: string | null; + agentId: string | null; + createdAt: Date; + updatedAt: Date; +} + +export interface Agent { + id: string; + name: string; + icon: string; + color: string; + lastMessage: string; + time: string; +} + +export interface AgentProfileLike { + id: string; + name: string; + nickname?: string; + role?: string; +} + +export interface ChatState { + messages: Message[]; + conversations: Conversation[]; + currentConversationId: string | null; + agents: Agent[]; + currentAgent: Agent | null; + isStreaming: boolean; + currentModel: string; + sessionKey: string | null; +} diff --git a/desktop/src/domains/hands/hooks.ts b/desktop/src/domains/hands/hooks.ts new file mode 100644 index 0000000..333df12 --- /dev/null +++ b/desktop/src/domains/hands/hooks.ts @@ -0,0 +1,79 @@ +/** + * Hands Domain Hooks + * + * React hooks for accessing hands state with Valtio. + */ +import { useSnapshot } from 'valtio'; +import { handsStore } from './store'; +import type { Hand, ApprovalRequest, Trigger, HandRun } from './types'; + +/** + * Hook to access the full hands state snapshot. + */ +export function useHandsState() { + return useSnapshot(handsStore); +} + +/** + * Hook to access hands list. + */ +export function useHands(): readonly Hand[] { + const { hands } = useSnapshot(handsStore); + return hands; +} + +/** + * Hook to access a specific hand by ID. + */ +export function useHand(id: string): Hand | undefined { + const { hands } = useSnapshot(handsStore); + return hands.find(h => h.id === id); +} + +/** + * Hook to access approval queue. + */ +export function useApprovalQueue(): readonly ApprovalRequest[] { + const { approvalQueue } = useSnapshot(handsStore); + return approvalQueue; +} + +/** + * Hook to access triggers. + */ +export function useTriggers(): readonly Trigger[] { + const { triggers } = useSnapshot(handsStore); + return triggers; +} + +/** + * Hook to access a specific run. + */ +export function useRun(runId: string): HandRun | undefined { + const { runs } = useSnapshot(handsStore); + return runs[runId]; +} + +/** + * Hook to check if any hand is loading. + */ +export function useHandsLoading(): boolean { + const { isLoading } = useSnapshot(handsStore); + return isLoading; +} + +/** + * Hook to access hands error. + */ +export function useHandsError(): string | null { + const { error } = useSnapshot(handsStore); + return error; +} + +/** + * Hook to access hands actions. + * Returns the store directly for calling actions. + */ +export function useHandsActions() { + return handsStore; +} diff --git a/desktop/src/domains/hands/index.ts b/desktop/src/domains/hands/index.ts new file mode 100644 index 0000000..821eceb --- /dev/null +++ b/desktop/src/domains/hands/index.ts @@ -0,0 +1,51 @@ +/** + * Hands Domain + * + * Automation and hands management functionality. + * + * @example + * // Using hooks + * import { useHands, useHandsActions } from '@/domains/hands'; + * + * function HandsComponent() { + * const hands = useHands(); + * const { setHands, updateHand } = useHandsActions(); + * // ... + * } + */ + +// Types +export type { + Hand, + HandStatus, + HandRequirement, + HandRun, + HandLog, + Trigger, + TriggerType, + TriggerConfig, + ApprovalRequest, + HandsState, + HandsEvent, + HandContext, +} from './types'; + +// Machine +export { handMachine, getHandStatusFromState } from './machine'; + +// Store +export { handsStore } from './store'; +export type { HandsStore } from './store'; + +// Hooks +export { + useHandsState, + useHands, + useHand, + useApprovalQueue, + useTriggers, + useRun, + useHandsLoading, + useHandsError, + useHandsActions, +} from './hooks'; diff --git a/desktop/src/domains/hands/machine.ts b/desktop/src/domains/hands/machine.ts new file mode 100644 index 0000000..f9b0547 --- /dev/null +++ b/desktop/src/domains/hands/machine.ts @@ -0,0 +1,166 @@ +/** + * Hands State Machine + * + * XState machine for managing hand execution lifecycle. + * Provides predictable state transitions for automation tasks. + */ +import { setup, assign, fromPromise } from 'xstate'; +import type { HandContext, HandsEvent } from './types'; + +// === Machine Setup === + +export const handMachine = setup({ + types: { + context: {} as HandContext, + events: {} as HandsEvent, + }, + actions: { + setRunId: assign({ + runId: (_, params: { runId: string }) => params.runId, + }), + setError: assign({ + error: (_, params: { error: string }) => params.error, + }), + setResult: assign({ + result: (_, params: { result: unknown }) => params.result, + }), + setProgress: assign({ + progress: (_, params: { progress: number }) => params.progress, + }), + clearError: assign({ + error: null, + }), + resetContext: assign({ + runId: null, + error: null, + result: null, + progress: 0, + }), + }, + guards: { + hasError: ({ context }) => context.error !== null, + isApproved: ({ event }) => event.type === 'APPROVE', + }, +}).createMachine({ + id: 'hand', + initial: 'idle', + context: { + handId: '', + handName: '', + runId: null, + error: null, + result: null, + progress: 0, + }, + states: { + idle: { + on: { + START: { + target: 'running', + actions: { + type: 'setRunId', + params: () => ({ runId: `run_${Date.now()}` }), + }, + }, + }, + }, + running: { + entry: assign({ progress: 0 }), + on: { + APPROVE: { + target: 'needs_approval', + }, + COMPLETE: { + target: 'success', + actions: { + type: 'setResult', + params: ({ event }) => ({ result: (event as { result: unknown }).result }), + }, + }, + ERROR: { + target: 'error', + actions: { + type: 'setError', + params: ({ event }) => ({ error: (event as { error: string }).error }), + }, + }, + CANCEL: { + target: 'cancelled', + }, + }, + }, + needs_approval: { + on: { + APPROVE: 'running', + REJECT: 'idle', + CANCEL: 'idle', + }, + }, + success: { + on: { + RESET: { + target: 'idle', + actions: 'resetContext', + }, + START: { + target: 'running', + actions: { + type: 'setRunId', + params: () => ({ runId: `run_${Date.now()}` }), + }, + }, + }, + }, + error: { + on: { + RESET: { + target: 'idle', + actions: 'resetContext', + }, + START: { + target: 'running', + actions: { + type: 'setRunId', + params: () => ({ runId: `run_${Date.now()}` }), + }, + }, + }, + }, + cancelled: { + on: { + RESET: { + target: 'idle', + actions: 'resetContext', + }, + START: { + target: 'running', + actions: { + type: 'setRunId', + params: () => ({ runId: `run_${Date.now()}` }), + }, + }, + }, + }, + }, +}); + +// === Helper to get status from machine state === + +export function getHandStatusFromState(stateValue: string): import('./types').HandStatus { + switch (stateValue) { + case 'idle': + return 'idle'; + case 'running': + return 'running'; + case 'needs_approval': + return 'needs_approval'; + case 'success': + return 'idle'; // Success maps back to idle + case 'error': + return 'error'; + case 'cancelled': + return 'idle'; + default: + return 'idle'; + } +} diff --git a/desktop/src/domains/hands/store.ts b/desktop/src/domains/hands/store.ts new file mode 100644 index 0000000..2178f15 --- /dev/null +++ b/desktop/src/domains/hands/store.ts @@ -0,0 +1,105 @@ +/** + * Hands Domain Store + * + * Valtio-based state management for hands/automation. + */ +import { proxy, subscribe } from 'valtio'; +import type { Hand, HandRun, Trigger, ApprovalRequest, HandsState } from './types'; + +// === Store Interface === + +export interface HandsStore extends HandsState { + // Actions + setHands: (hands: Hand[]) => void; + updateHand: (id: string, updates: Partial) => void; + addRun: (run: HandRun) => void; + updateRun: (runId: string, updates: Partial) => void; + setTriggers: (triggers: Trigger[]) => void; + updateTrigger: (id: string, updates: Partial) => void; + addApproval: (request: ApprovalRequest) => void; + removeApproval: (id: string) => void; + clearApprovals: () => void; + setLoading: (loading: boolean) => void; + setError: (error: string | null) => void; +} + +// === Create Proxy State === + +export const handsStore = proxy({ + // Initial state + hands: [], + runs: {}, + triggers: [], + approvalQueue: [], + isLoading: false, + error: null, + + // === Actions === + + setHands: (hands: Hand[]) => { + handsStore.hands = hands; + }, + + updateHand: (id: string, updates: Partial) => { + const hand = handsStore.hands.find(h => h.id === id); + if (hand) { + Object.assign(hand, updates); + } + }, + + addRun: (run: HandRun) => { + handsStore.runs[run.runId] = run; + }, + + updateRun: (runId: string, updates: Partial) => { + if (handsStore.runs[runId]) { + Object.assign(handsStore.runs[runId], updates); + } + }, + + setTriggers: (triggers: Trigger[]) => { + handsStore.triggers = triggers; + }, + + updateTrigger: (id: string, updates: Partial) => { + const trigger = handsStore.triggers.find(t => t.id === id); + if (trigger) { + Object.assign(trigger, updates); + } + }, + + addApproval: (request: ApprovalRequest) => { + // Check if already exists + const exists = handsStore.approvalQueue.some(a => a.id === request.id); + if (!exists) { + handsStore.approvalQueue.push(request); + } + }, + + removeApproval: (id: string) => { + const index = handsStore.approvalQueue.findIndex(a => a.id === id); + if (index >= 0) { + handsStore.approvalQueue.splice(index, 1); + } + }, + + clearApprovals: () => { + handsStore.approvalQueue = []; + }, + + setLoading: (loading: boolean) => { + handsStore.isLoading = loading; + }, + + setError: (error: string | null) => { + handsStore.error = error; + }, +}); + +// === Dev Mode Logging === + +if (import.meta.env.DEV) { + subscribe(handsStore, (ops) => { + console.log('[HandsStore] Changes:', ops); + }); +} diff --git a/desktop/src/domains/hands/types.ts b/desktop/src/domains/hands/types.ts new file mode 100644 index 0000000..45ced15 --- /dev/null +++ b/desktop/src/domains/hands/types.ts @@ -0,0 +1,123 @@ +/** + * Hands Domain Types + * + * Core types for the automation/hands system. + */ + +export interface HandRequirement { + description: string; + met: boolean; + details?: string; +} + +export interface Hand { + id: string; + name: string; + description: string; + status: HandStatus; + currentRunId?: string; + requirements_met?: boolean; + category?: string; + icon?: string; + provider?: string; + model?: string; + requirements?: HandRequirement[]; + tools?: string[]; + metrics?: string[]; + toolCount?: number; + metricCount?: number; +} + +export type HandStatus = + | 'idle' + | 'running' + | 'needs_approval' + | 'error' + | 'unavailable' + | 'setup_needed'; + +export interface HandRun { + runId: string; + handId: string; + handName: string; + status: 'running' | 'completed' | 'error' | 'cancelled'; + startedAt: Date; + completedAt?: Date; + result?: unknown; + error?: string; + progress?: number; + logs?: HandLog[]; +} + +export interface HandLog { + timestamp: Date; + level: 'info' | 'warn' | 'error' | 'debug'; + message: string; +} + +export interface Trigger { + id: string; + handId: string; + type: TriggerType; + enabled: boolean; + config: TriggerConfig; +} + +export type TriggerType = 'manual' | 'schedule' | 'event' | 'webhook'; + +export interface TriggerConfig { + schedule?: string; // Cron expression + event?: string; // Event name + webhook?: { + path: string; + method: 'GET' | 'POST'; + }; +} + +export interface ApprovalRequest { + id: string; + handName: string; + runId: string; + action: string; + params: Record; + createdAt: Date; + timeout?: number; +} + +export interface HandsState { + hands: Hand[]; + runs: Record; + triggers: Trigger[]; + approvalQueue: ApprovalRequest[]; + isLoading: boolean; + error: string | null; +} + +// === XState Types === + +export type HandsEventType = + | 'START' + | 'APPROVE' + | 'REJECT' + | 'COMPLETE' + | 'ERROR' + | 'RESET' + | 'CANCEL'; + +export interface HandsEvent { + type: HandsEventType; + handId?: string; + runId?: string; + requestId?: string; + result?: unknown; + error?: string; +} + +export interface HandContext { + handId: string; + handName: string; + runId: string | null; + error: string | null; + result: unknown; + progress: number; +} diff --git a/desktop/src/shared/error-handling.ts b/desktop/src/shared/error-handling.ts new file mode 100644 index 0000000..a545e9c --- /dev/null +++ b/desktop/src/shared/error-handling.ts @@ -0,0 +1,105 @@ +/** + * Shared Error Handling + * + * Unified error handling utilities. + */ + +/** + * Application error class with error code. + */ +export class AppError extends Error { + constructor( + message: string, + public readonly code: string, + public readonly cause?: Error + ) { + super(message); + this.name = 'AppError'; + } + + /** + * Create a AppError from an unknown error. + */ + static fromUnknown(error: unknown, code: string): AppError { + if (error instanceof AppError) { + return error; + } + return new AppError(getErrorMessage(error), code, isError(error) ? error : undefined); + } +} + +/** + * Network error class. + */ +export class NetworkError extends AppError { + constructor(message: string, public readonly statusCode?: number, cause?: Error) { + super(message, 'NETWORK_ERROR', cause); + this.name = 'NetworkError'; + } +} + +/** + * Validation error class. + */ +export class ValidationError extends AppError { + constructor(message: string, public readonly field?: string, cause?: Error) { + super(message, 'VALIDATION_ERROR', cause); + this.name = 'ValidationError'; + } +} + +/** + * Authentication error class. + */ +export class AuthError extends AppError { + constructor(message: string = 'Authentication required', cause?: Error) { + super(message, 'AUTH_ERROR', cause); + this.name = 'AuthError'; + } +} + +/** + * Type guard for Error. + */ +export function isError(error: unknown): error is Error { + return error instanceof Error; +} + +/** + * Get error message from unknown error. + */ +export function getErrorMessage(error: unknown): string { + if (isError(error)) { + return error.message; + } + if (typeof error === 'string') { + return error; + } + return String(error); +} + +/** + * Wrap error with code. + */ +export function wrapError(error: unknown, code: string): AppError { + return AppError.fromUnknown(error, code); +} + +/** + * Check if error is a specific error class. + */ +export function isAppError(error: unknown): error is AppError { + return error instanceof AppError; +} + +export function isNetworkError(error: unknown): error is NetworkError { + return error instanceof NetworkError; +} + +export function isValidationError(error: unknown): error is ValidationError { + return error instanceof ValidationError; +} + +export function isAuthError(error: unknown): error is AuthError { + return error instanceof AuthError; +} diff --git a/desktop/src/shared/index.ts b/desktop/src/shared/index.ts new file mode 100644 index 0000000..6513289 --- /dev/null +++ b/desktop/src/shared/index.ts @@ -0,0 +1,31 @@ +/** + * Shared Module + * + * Common utilities, types, and error handling. + */ + +// Types +export type { + Result, + AsyncResult, + PaginatedResponse, + AsyncStatus, + AsyncState, + Entity, + NamedEntity, +} from './types'; + +// Errors +export { + AppError, + NetworkError, + ValidationError, + AuthError, + isError, + getErrorMessage, + wrapError, + isAppError, + isNetworkError, + isValidationError, + isAuthError, +} from './error-handling'; diff --git a/desktop/src/shared/types.ts b/desktop/src/shared/types.ts new file mode 100644 index 0000000..d5c7dc0 --- /dev/null +++ b/desktop/src/shared/types.ts @@ -0,0 +1,58 @@ +/** + * Shared Types + * + * Common types used across domains. + */ + +/** + * Result type for functional error handling. + */ +export type Result = + | { ok: true; value: T } + | { ok: false; error: E }; + +/** + * Async result for promises. + */ +export type AsyncResult = Promise>; + +/** + * Paginated response for list endpoints. + */ +export interface PaginatedResponse { + items: T[]; + total: number; + page: number; + pageSize: number; + hasMore: boolean; +} + +/** + * Common status for async operations. + */ +export type AsyncStatus = 'idle' | 'loading' | 'success' | 'error'; + +/** + * Generic async state wrapper. + */ +export interface AsyncState { + status: AsyncStatus; + data: T | null; + error: E | null; +} + +/** + * Entity with common fields. + */ +export interface Entity { + id: string; + createdAt: Date; + updatedAt: Date; +} + +/** + * Named entity with name field. + */ +export interface NamedEntity extends Entity { + name: string; +} diff --git a/docs/superpowers/plans/2026-03-21-phase2-domain-reorganization.md b/docs/superpowers/plans/2026-03-21-phase2-domain-reorganization.md new file mode 100644 index 0000000..244a681 --- /dev/null +++ b/docs/superpowers/plans/2026-03-21-phase2-domain-reorganization.md @@ -0,0 +1,1174 @@ +# ZCLAW 架构优化 - Phase 2: 领域重组 实施计划 + +> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 按领域重组代码结构,迁移到 Valtio 状态管理,引入 XState 状态机 + +**Architecture:** 创建 domains/ 目录,按业务领域组织代码,使用 Valtio 替代 Zustand,使用 XState 管理 Hands 状态 + +**Tech Stack:** TypeScript, Valtio, XState, React + +**Spec Reference:** `docs/superpowers/specs/2026-03-21-architecture-optimization-design.md` + +**Duration:** 4 周 (16 人日) + +--- + +## File Structure + +### New Files +``` +desktop/src/ +├── domains/ +│ ├── chat/ +│ │ ├── index.ts # 导出入口 +│ │ ├── store.ts # Valtio store +│ │ ├── types.ts # 类型定义 +│ │ ├── api.ts # API 调用 +│ │ └── hooks.ts # React hooks +│ ├── hands/ +│ │ ├── index.ts # 导出入口 +│ │ ├── store.ts # Valtio store +│ │ ├── machine.ts # XState 状态机 +│ │ ├── types.ts # 类型定义 +│ │ └── hooks.ts # React hooks +│ ├── intelligence/ +│ │ ├── index.ts # 导出入口 +│ │ ├── client.ts # 统一客户端 +│ │ ├── cache.ts # 缓存策略 +│ │ └── types.ts # 类型定义 +│ └── skills/ +│ ├── index.ts # 导出入口 +│ ├── store.ts # Valtio store +│ └── types.ts # 类型定义 +└── shared/ + ├── index.ts # 导出入口 + ├── error-handling.ts # 统一错误处理 + ├── logging.ts # 统一日志 + └── types.ts # 共享类型 +``` + +### Modified Files +``` +desktop/ +├── package.json # 添加 Valtio, XState 依赖 +├── src/store/chatStore.ts # 重导出 domains/chat +├── src/store/handStore.ts # 重导出 domains/hands +└── src/components/ # 更新导入路径 +``` + +--- + +## Chunk 1: 依赖安装和目录结构 + +### Task 1.1: 安装 Valtio 和 XState + +**Files:** +- Modify: `desktop/package.json` + +- [ ] **Step 1: 安装 Valtio** + +Run: +```bash +cd g:/ZClaw_openfang/desktop && pnpm add valtio +``` + +Expected: valtio 安装成功 + +- [ ] **Step 2: 安装 XState** + +Run: +```bash +cd g:/ZClaw_openfang/desktop && pnpm add xstate @xstate/react +``` + +Expected: xstate 和 @xstate/react 安装成功 + +- [ ] **Step 3: 验证安装** + +Run: +```bash +cd g:/ZClaw_openfang/desktop && pnpm list valtio xstate @xstate/react +``` + +Expected: 显示已安装版本 + +- [ ] **Step 4: 提交依赖更新** + +```bash +cd g:/ZClaw_openfang && git add desktop/package.json desktop/pnpm-lock.yaml +git commit -m "$(cat <<'EOF' +feat(deps): add Valtio and XState for Phase 2 + +- Add valtio for Proxy-based state management +- Add xstate and @xstate/react for state machines + +Co-Authored-By: Claude Opus 4.6 +EOF +)" +``` + +--- + +### Task 1.2: 创建领域目录结构 + +**Files:** +- Create: `desktop/src/domains/chat/` directory +- Create: `desktop/src/domains/hands/` directory +- Create: `desktop/src/domains/intelligence/` directory +- Create: `desktop/src/domains/skills/` directory +- Create: `desktop/src/shared/` directory + +- [ ] **Step 1: 创建目录** + +Run: +```bash +cd g:/ZClaw_openfang/desktop/src && mkdir -p domains/chat domains/hands domains/intelligence domains/skills shared +``` + +- [ ] **Step 2: 提交目录结构** + +```bash +cd g:/ZClaw_openfang && git add desktop/src/domains desktop/src/shared +git commit -m "$(cat <<'EOF' +refactor: create domains directory structure + +- Create domains/chat for chat system +- Create domains/hands for automation +- Create domains/intelligence for AI layer +- Create domains/skills for skill system +- Create shared for common utilities + +Co-Authored-By: Claude Opus 4.6 +EOF +)" +``` + +--- + +## Chunk 2: Chat Domain 迁移 + +### Task 2.1: 创建 Chat Domain 类型定义 + +**Files:** +- Create: `desktop/src/domains/chat/types.ts` + +- [ ] **Step 1: 提取类型定义** + +Create `desktop/src/domains/chat/types.ts`: + +```typescript +/** + * Chat Domain Types + * + * Core types for the chat system. + */ + +export interface MessageFile { + name: string; + path?: string; + size?: number; + type?: string; +} + +export interface CodeBlock { + language?: string; + filename?: string; + content?: string; +} + +export interface Message { + id: string; + role: 'user' | 'assistant' | 'tool' | 'hand' | 'workflow'; + content: string; + timestamp: Date; + runId?: string; + streaming?: boolean; + toolName?: string; + toolInput?: string; + toolOutput?: string; + error?: string; + handName?: string; + handStatus?: string; + handResult?: unknown; + workflowId?: string; + workflowStep?: string; + workflowStatus?: string; + workflowResult?: unknown; + files?: MessageFile[]; + codeBlocks?: CodeBlock[]; +} + +export interface Conversation { + id: string; + title: string; + messages: Message[]; + sessionKey: string | null; + agentId: string | null; + createdAt: Date; + updatedAt: Date; +} + +export interface Agent { + id: string; + name: string; + icon: string; + color: string; + lastMessage: string; + time: string; +} + +export interface AgentProfileLike { + id: string; + name: string; + nickname?: string; + role?: string; +} + +export interface ChatState { + messages: Message[]; + conversations: Conversation[]; + currentConversationId: string | null; + agents: Agent[]; + currentAgent: Agent | null; + isStreaming: boolean; + currentModel: string; + sessionKey: string | null; +} +``` + +- [ ] **Step 2: 提交类型定义** + +```bash +cd g:/ZClaw_openfang && git add desktop/src/domains/chat/types.ts +git commit -m "$(cat <<'EOF' +refactor(chat): extract chat domain types + +Co-Authored-By: Claude Opus 4.6 +EOF +)" +``` + +--- + +### Task 2.2: 创建 Valtio Chat Store + +**Files:** +- Create: `desktop/src/domains/chat/store.ts` + +- [ ] **Step 1: 创建 Valtio Store** + +Create `desktop/src/domains/chat/store.ts`: + +```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, ChatState } from './types'; + +// Default agent +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) || '🦞', + color: 'bg-gradient-to-br from-orange-500 to-red-500', + lastMessage: profile.role || '新分身', + time: '', + }; +} + +// State interface with actions +interface ChatStore extends ChatState { + // Actions + addMessage: (message: Message) => void; + updateMessage: (id: string, updates: Partial) => void; + setCurrentAgent: (agent: Agent) => void; + syncAgents: (profiles: AgentProfileLike[]) => void; + setCurrentModel: (model: string) => void; + newConversation: () => void; + switchConversation: (id: string) => void; + deleteConversation: (id: string) => void; + clearMessages: () => void; +} + +// Create proxy state +export const chatStore = proxy({ + // 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) => { + const msg = chatStore.messages.find(m => m.id === id); + if (msg) { + Object.assign(msg, updates); + } + }, + + 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; + }, + + 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(), + }; + 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 = []; + }, +}); + +// Optional: Subscribe to changes for debugging +if (import.meta.env.DEV) { + subscribe(chatStore, (ops) => { + console.log('[ChatStore] Changes:', ops); + }); +} +``` + +- [ ] **Step 2: 提交 Valtio Store** + +```bash +cd g:/ZClaw_openfang && git add desktop/src/domains/chat/store.ts +git commit -m "$(cat <<'EOF' +refactor(chat): create Valtio-based chat store + +- Replace Zustand with Valtio for fine-grained reactivity +- Implement core actions: addMessage, updateMessage, etc. +- Add conversation management: new, switch, delete + +Co-Authored-By: Claude Opus 4.6 +EOF +)" +``` + +--- + +### Task 2.3: 创建 Chat Domain Hooks + +**Files:** +- Create: `desktop/src/domains/chat/hooks.ts` + +- [ ] **Step 1: 创建 React Hooks** + +Create `desktop/src/domains/chat/hooks.ts`: + +```typescript +/** + * Chat Domain Hooks + * + * React hooks for accessing chat state with Valtio. + */ +import { useSnapshot } from 'valtio'; +import { chatStore } from './store'; +import type { Message, Agent, Conversation } from './types'; + +/** + * Hook to access the full chat state. + * Only re-renders when accessed properties change. + */ +export function useChatState() { + return useSnapshot(chatStore); +} + +/** + * Hook to access messages only. + * Only re-renders when messages change. + */ +export function useMessages(): readonly Message[] { + const { messages } = useSnapshot(chatStore); + return messages; +} + +/** + * Hook to access streaming state. + * Only re-renders when isStreaming changes. + */ +export function useIsStreaming(): boolean { + const { isStreaming } = useSnapshot(chatStore); + return isStreaming; +} + +/** + * Hook to access current agent. + */ +export function useCurrentAgent(): Agent | null { + const { currentAgent } = useSnapshot(chatStore); + return currentAgent; +} + +/** + * Hook to access conversations. + */ +export function useConversations(): readonly Conversation[] { + const { conversations } = useSnapshot(chatStore); + return conversations; +} + +/** + * Hook to access chat actions. + * Returns the store directly for calling actions. + */ +export function useChatActions() { + return chatStore; +} +``` + +- [ ] **Step 2: 创建 Domain Index** + +Create `desktop/src/domains/chat/index.ts`: + +```typescript +/** + * Chat Domain + * + * Public API for the chat system. + */ + +// Types +export type { + Message, + MessageFile, + CodeBlock, + Conversation, + Agent, + AgentProfileLike, + ChatState, +} from './types'; + +// Store +export { chatStore, toChatAgent } from './store'; + +// Hooks +export { + useChatState, + useMessages, + useIsStreaming, + useCurrentAgent, + useConversations, + useChatActions, +} from './hooks'; +``` + +- [ ] **Step 3: 提交 Hooks 和 Index** + +```bash +cd g:/ZClaw_openfang && git add desktop/src/domains/chat/hooks.ts desktop/src/domains/chat/index.ts +git commit -m "$(cat <<'EOF' +refactor(chat): add chat domain hooks and public API + +- Add useChatState, useMessages, useIsStreaming hooks +- Export types, store, and hooks from domain index + +Co-Authored-By: Claude Opus 4.6 +EOF +)" +``` + +--- + +## Chunk 3: Hands Domain 迁移 + +### Task 3.1: 创建 Hands Domain 类型定义 + +**Files:** +- Create: `desktop/src/domains/hands/types.ts` + +- [ ] **Step 1: 创建类型定义** + +Create `desktop/src/domains/hands/types.ts`: + +```typescript +/** + * Hands Domain Types + * + * Core types for the automation/hands system. + */ + +export interface HandRequirement { + description: string; + met: boolean; + details?: string; +} + +export interface Hand { + id: string; + name: string; + description: string; + status: HandStatus; + currentRunId?: string; + requirements_met?: boolean; + category?: string; + icon?: string; + provider?: string; + model?: string; + requirements?: HandRequirement[]; + tools?: string[]; + metrics?: string[]; + toolCount?: number; + metricCount?: number; +} + +export type HandStatus = + | 'idle' + | 'running' + | 'needs_approval' + | 'error' + | 'unavailable' + | 'setup_needed'; + +export interface HandRun { + runId: string; + status: string; + startedAt: string; + completedAt?: string; + result?: unknown; + error?: string; +} + +export interface Trigger { + id: string; + type: string; + enabled: boolean; +} + +export interface ApprovalRequest { + id: string; + handName: string; + action: string; + params: Record; + createdAt: Date; +} + +export interface HandsState { + hands: Hand[]; + runs: Record; + triggers: Trigger[]; + approvalQueue: ApprovalRequest[]; + isLoading: boolean; + error: string | null; +} + +// XState Events +export type HandsEvent = + | { type: 'START'; handId: string } + | { type: 'APPROVE'; requestId: string } + | { type: 'REJECT'; requestId: string } + | { type: 'COMPLETE'; runId: string; result: unknown } + | { type: 'ERROR'; runId: string; error: string } + | { type: 'RESET' }; +``` + +- [ ] **Step 2: 提交类型定义** + +```bash +cd g:/ZClaw_openfang && git add desktop/src/domains/hands/types.ts +git commit -m "$(cat <<'EOF' +refactor(hands): extract hands domain types + +Co-Authored-By: Claude Opus 4.6 +EOF +)" +``` + +--- + +### Task 3.2: 创建 XState 状态机 + +**Files:** +- Create: `desktop/src/domains/hands/machine.ts` + +- [ ] **Step 1: 创建状态机** + +Create `desktop/src/domains/hands/machine.ts`: + +```typescript +/** + * Hands State Machine + * + * XState machine for managing hand execution lifecycle. + */ +import { setup, assign } from 'xstate'; +import type { HandStatus, HandsEvent, Hand } from './types'; + +export interface HandContext { + handId: string; + handName: string; + runId: string | null; + error: string | null; + result: unknown; +} + +export type HandState = + | { value: 'idle'; context: HandContext } + | { value: 'running'; context: HandContext } + | { value: 'needs_approval'; context: HandContext } + | { value: 'success'; context: HandContext } + | { value: 'error'; context: HandContext }; + +export const handMachine = setup({ + types: { + context: {} as HandContext, + events: {} as HandsEvent, + }, + actions: { + setRunId: assign({ + runId: (_, params: { runId: string }) => params.runId, + }), + setError: assign({ + error: (_, params: { error: string }) => params.error, + }), + setResult: assign({ + result: (_, params: { result: unknown }) => params.result, + }), + clearError: assign({ + error: null, + }), + }, +}).createMachine({ + id: 'hand', + initial: 'idle', + context: { + handId: '', + handName: '', + runId: null, + error: null, + result: null, + }, + states: { + idle: { + on: { + START: { + target: 'running', + actions: { + type: 'setRunId', + params: ({ event }) => ({ runId: `run_${Date.now()}` }), + }, + }, + }, + }, + running: { + on: { + APPROVE: 'needs_approval', + COMPLETE: { + target: 'success', + actions: { + type: 'setResult', + params: ({ event }) => ({ result: event.result }), + }, + }, + ERROR: { + target: 'error', + actions: { + type: 'setError', + params: ({ event }) => ({ error: event.error }), + }, + }, + }, + }, + needs_approval: { + on: { + APPROVE: 'running', + REJECT: 'idle', + }, + }, + success: { + on: { + RESET: { + target: 'idle', + actions: 'clearError', + }, + }, + }, + error: { + on: { + RESET: { + target: 'idle', + actions: 'clearError', + }, + START: 'running', + }, + }, + }, +}); +``` + +- [ ] **Step 2: 提交状态机** + +```bash +cd g:/ZClaw_openfang && git add desktop/src/domains/hands/machine.ts +git commit -m "$(cat <<'EOF' +refactor(hands): create XState machine for hand execution + +- Define states: idle, running, needs_approval, success, error +- Define events: START, APPROVE, REJECT, COMPLETE, ERROR, RESET +- Add context for tracking runId, error, result + +Co-Authored-By: Claude Opus 4.6 +EOF +)" +``` + +--- + +### Task 3.3: 创建 Hands Valtio Store + +**Files:** +- Create: `desktop/src/domains/hands/store.ts` +- Create: `desktop/src/domains/hands/hooks.ts` +- Create: `desktop/src/domains/hands/index.ts` + +- [ ] **Step 1: 创建 Store** + +Create `desktop/src/domains/hands/store.ts`: + +```typescript +/** + * Hands Domain Store + * + * Valtio-based state management for hands/automation. + */ +import { proxy } from 'valtio'; +import type { Hand, HandRun, Trigger, ApprovalRequest, HandsState } from './types'; + +interface HandsStore extends HandsState { + // Actions + setHands: (hands: Hand[]) => void; + updateHand: (id: string, updates: Partial) => void; + addRun: (run: HandRun) => void; + updateRun: (runId: string, updates: Partial) => void; + setTriggers: (triggers: Trigger[]) => void; + addApproval: (request: ApprovalRequest) => void; + removeApproval: (id: string) => void; + setLoading: (loading: boolean) => void; + setError: (error: string | null) => void; +} + +export const handsStore = proxy({ + // Initial state + hands: [], + runs: {}, + triggers: [], + approvalQueue: [], + isLoading: false, + error: null, + + // Actions + setHands: (hands: Hand[]) => { + handsStore.hands = hands; + }, + + updateHand: (id: string, updates: Partial) => { + const hand = handsStore.hands.find(h => h.id === id); + if (hand) { + Object.assign(hand, updates); + } + }, + + addRun: (run: HandRun) => { + handsStore.runs[run.runId] = run; + }, + + updateRun: (runId: string, updates: Partial) => { + if (handsStore.runs[runId]) { + Object.assign(handsStore.runs[runId], updates); + } + }, + + setTriggers: (triggers: Trigger[]) => { + handsStore.triggers = triggers; + }, + + addApproval: (request: ApprovalRequest) => { + handsStore.approvalQueue.push(request); + }, + + removeApproval: (id: string) => { + const index = handsStore.approvalQueue.findIndex(a => a.id === id); + if (index >= 0) { + handsStore.approvalQueue.splice(index, 1); + } + }, + + setLoading: (loading: boolean) => { + handsStore.isLoading = loading; + }, + + setError: (error: string | null) => { + handsStore.error = error; + }, +}); +``` + +- [ ] **Step 2: 创建 Hooks** + +Create `desktop/src/domains/hands/hooks.ts`: + +```typescript +/** + * Hands Domain Hooks + */ +import { useSnapshot } from 'valtio'; +import { handsStore } from './store'; + +export function useHandsState() { + return useSnapshot(handsStore); +} + +export function useHands() { + const { hands } = useSnapshot(handsStore); + return hands; +} + +export function useApprovalQueue() { + const { approvalQueue } = useSnapshot(handsStore); + return approvalQueue; +} + +export function useHandsActions() { + return handsStore; +} +``` + +- [ ] **Step 3: 创建 Index** + +Create `desktop/src/domains/hands/index.ts`: + +```typescript +/** + * Hands Domain + */ +export * from './types'; +export { handMachine } from './machine'; +export { handsStore } from './store'; +export { useHandsState, useHands, useApprovalQueue, useHandsActions } from './hooks'; +``` + +- [ ] **Step 4: 提交 Hands Domain** + +```bash +cd g:/ZClaw_openfang && git add desktop/src/domains/hands/ +git commit -m "$(cat <<'EOF' +refactor(hands): complete hands domain migration + +- Add Valtio store for hands state +- Add React hooks for hands access +- Export all from domain index + +Co-Authored-By: Claude Opus 4.6 +EOF +)" +``` + +--- + +## Chunk 4: Shared Module 提取 + +### Task 4.1: 创建共享错误处理 + +**Files:** +- Create: `desktop/src/shared/error-handling.ts` +- Create: `desktop/src/shared/types.ts` +- Create: `desktop/src/shared/index.ts` + +- [ ] **Step 1: 创建错误处理工具** + +Create `desktop/src/shared/error-handling.ts`: + +```typescript +/** + * Shared Error Handling + * + * Unified error handling utilities. + */ + +export class AppError extends Error { + constructor( + message: string, + public code: string, + public cause?: Error + ) { + super(message); + this.name = 'AppError'; + } +} + +export function isError(error: unknown): error is Error { + return error instanceof Error; +} + +export function getErrorMessage(error: unknown): string { + if (isError(error)) { + return error.message; + } + return String(error); +} + +export function wrapError(error: unknown, code: string): AppError { + if (error instanceof AppError) { + return error; + } + return new AppError(getErrorMessage(error), code, isError(error) ? error : undefined); +} +``` + +- [ ] **Step 2: 创建共享类型** + +Create `desktop/src/shared/types.ts`: + +```typescript +/** + * Shared Types + */ + +export type Result = + | { ok: true; value: T } + | { ok: false; error: E }; + +export type AsyncResult = Promise>; + +export interface PaginatedResponse { + items: T[]; + total: number; + page: number; + pageSize: number; +} +``` + +- [ ] **Step 3: 创建 Index** + +Create `desktop/src/shared/index.ts`: + +```typescript +/** + * Shared Module + */ +export * from './error-handling'; +export * from './types'; +``` + +- [ ] **Step 4: 提交共享模块** + +```bash +cd g:/ZClaw_openfang && git add desktop/src/shared/ +git commit -m "$(cat <<'EOF' +refactor(shared): create shared module + +- Add AppError class for unified error handling +- Add Result type for functional error handling +- Add PaginatedResponse type + +Co-Authored-By: Claude Opus 4.6 +EOF +)" +``` + +--- + +## Chunk 5: 集成和验证 + +### Task 5.1: 创建向后兼容层 + +**Files:** +- Modify: `desktop/src/store/chatStore.ts` + +- [ ] **Step 1: 更新旧 Store 重导出** + +Update `desktop/src/store/chatStore.ts` to re-export from domain: + +```typescript +/** + * Chat Store - Backward Compatibility Layer + * + * This file re-exports from the new domains/chat module. + * Import from '@/domains/chat' for new code. + */ +export * from '../domains/chat'; +``` + +- [ ] **Step 2: 提交兼容层** + +```bash +cd g:/ZClaw_openfang && git add desktop/src/store/chatStore.ts +git commit -m "$(cat <<'EOF' +refactor(chat): add backward compatibility layer + +- Re-export from domains/chat for backward compatibility +- Maintains existing import paths + +Co-Authored-By: Claude Opus 4.6 +EOF +)" +``` + +--- + +### Task 5.2: 运行测试验证 + +- [ ] **Step 1: 运行 Chat Store 测试** + +Run: +```bash +cd g:/ZClaw_openfang/desktop && pnpm test tests/store/chatStore.test.ts +``` + +Expected: Tests pass with new Valtio store + +- [ ] **Step 2: 运行所有测试** + +Run: +```bash +cd g:/ZClaw_openfang/desktop && pnpm test +``` + +Expected: No new test failures + +--- + +### Task 5.3: 更新文档 + +- [ ] **Step 1: 创建 Phase 2 变更日志** + +Create `docs/changelogs/2026-03-21-phase2-domain-reorganization.md` + +--- + +## Verification Checklist + +### Domain Structure +- [ ] domains/chat/ created with types, store, hooks +- [ ] domains/hands/ created with types, machine, store, hooks +- [ ] shared/ created with error-handling, types + +### State Management +- [ ] Valtio installed and configured +- [ ] Chat store migrated to Valtio +- [ ] Hands store migrated to Valtio +- [ ] XState machine created for hands + +### Compatibility +- [ ] Backward compatibility layer in place +- [ ] Existing imports still work +- [ ] Tests passing + +--- + +## Next Steps (Phase 3) + +- Valtio 性能优化 +- XState 状态机完整集成 +- Intelligence 缓存增强 +- 组件迁移到新 Hooks