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 <noreply@anthropic.com>
1175 lines
26 KiB
Markdown
1175 lines
26 KiB
Markdown
# 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 <noreply@anthropic.com>
|
||
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 <noreply@anthropic.com>
|
||
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 <noreply@anthropic.com>
|
||
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<Message>) => 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<ChatStore>({
|
||
// 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<Message>) => {
|
||
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 <noreply@anthropic.com>
|
||
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 <noreply@anthropic.com>
|
||
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<string, unknown>;
|
||
createdAt: Date;
|
||
}
|
||
|
||
export interface HandsState {
|
||
hands: Hand[];
|
||
runs: Record<string, HandRun>;
|
||
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 <noreply@anthropic.com>
|
||
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 <noreply@anthropic.com>
|
||
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<Hand>) => void;
|
||
addRun: (run: HandRun) => void;
|
||
updateRun: (runId: string, updates: Partial<HandRun>) => 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<HandsStore>({
|
||
// Initial state
|
||
hands: [],
|
||
runs: {},
|
||
triggers: [],
|
||
approvalQueue: [],
|
||
isLoading: false,
|
||
error: null,
|
||
|
||
// Actions
|
||
setHands: (hands: Hand[]) => {
|
||
handsStore.hands = hands;
|
||
},
|
||
|
||
updateHand: (id: string, updates: Partial<Hand>) => {
|
||
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<HandRun>) => {
|
||
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 <noreply@anthropic.com>
|
||
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<T, E = Error> =
|
||
| { ok: true; value: T }
|
||
| { ok: false; error: E };
|
||
|
||
export type AsyncResult<T, E = Error> = Promise<Result<T, E>>;
|
||
|
||
export interface PaginatedResponse<T> {
|
||
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 <noreply@anthropic.com>
|
||
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 <noreply@anthropic.com>
|
||
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
|