docs(guide): rewrite CLAUDE.md with ZCLAW-first perspective

Major changes:
- Shift from "OpenFang desktop client" to "independent AI Agent desktop app"
- Add decision principle: "Is this useful for ZCLAW? Does it affect ZCLAW?"
- Simplify project structure and tech stack sections
- Replace OpenClaw vs OpenFang comparison with unified backend approach
- Consolidate troubleshooting from scattered sections into organized FAQ
- Update Hands system documentation with 8 capabilities and status
- Stream
This commit is contained in:
iven
2026-03-20 19:30:09 +08:00
parent 3518fc8ece
commit 6f72442531
63 changed files with 8920 additions and 857 deletions

View File

@@ -0,0 +1,635 @@
/**
* Store 状态检查工具
* 用于验证 Zustand Store 的状态变化
*
* 实际 localStorage keys:
* - chatStore: zclaw-chat-storage (持久化)
* - teamStore: zclaw-teams (持久化)
* - gatewayStore: zclaw-gateway-url, zclaw-gateway-token, zclaw-device-id (单独键)
* - handStore: 不持久化
* - agentStore: 不持久化
*/
import { Page } from '@playwright/test';
import { expect } from '@playwright/test';
/**
* localStorage key 映射
*/
export const STORAGE_KEYS = {
// 标准持久化 Store (使用 zustand persist)
CHAT: 'zclaw-chat-storage',
TEAMS: 'zclaw-teams',
// Gateway Store 使用单独的键
GATEWAY: 'zclaw-gateway-url', // 主键
GATEWAY_URL: 'zclaw-gateway-url',
GATEWAY_TOKEN: 'zclaw-gateway-token',
DEVICE_ID: 'zclaw-device-id',
// 非持久化 Store (运行时状态,不在 localStorage)
HAND: null,
AGENT: null,
WORKFLOW: null,
CONNECTION: null,
CONFIG: null,
} as const;
/**
* Store 名称类型
*/
export type StoreName = keyof typeof STORAGE_KEYS;
/**
* 向后兼容: STORE_NAMES 枚举
* @deprecated 使用 STORAGE_KEYS 代替
*/
export const STORE_NAMES = {
CHAT: 'CHAT' as StoreName,
GATEWAY: 'GATEWAY' as StoreName,
AGENT: 'AGENT' as StoreName,
HAND: 'HAND' as StoreName,
WORKFLOW: 'WORKFLOW' as StoreName,
CONFIG: 'CONFIG' as StoreName,
TEAM: 'TEAMS' as StoreName,
CONNECTION: 'CONNECTION' as StoreName,
} as const;
/**
* Store 状态检查工具
*/
export const storeInspectors = {
/**
* 获取 localStorage key
*/
getStorageKey(storeName: StoreName): string | null {
return STORAGE_KEYS[storeName];
},
/**
* 获取持久化的 Chat Store 状态
*/
async getChatState<T = unknown>(page: Page): Promise<T | null> {
return page.evaluate((key) => {
const stored = localStorage.getItem(key);
if (!stored) return null;
try {
const parsed = JSON.parse(stored);
return parsed.state as T;
} catch {
return null;
}
}, STORAGE_KEYS.CHAT);
},
/**
* 获取持久化的 Teams Store 状态
*/
async getTeamsState<T = unknown>(page: Page): Promise<T | null> {
return page.evaluate((key) => {
const stored = localStorage.getItem(key);
if (!stored) return null;
try {
// teams store 可能直接存储数组或对象
const parsed = JSON.parse(stored);
return parsed.state ? parsed.state : parsed;
} catch {
return null;
}
}, STORAGE_KEYS.TEAMS);
},
/**
* 获取 Gateway 配置
*/
async getGatewayConfig(page: Page): Promise<{
url: string | null;
token: string | null;
deviceId: string | null;
}> {
return page.evaluate((keys) => {
return {
url: localStorage.getItem(keys.url),
token: localStorage.getItem(keys.token),
deviceId: localStorage.getItem(keys.deviceId),
};
}, {
url: STORAGE_KEYS.GATEWAY_URL,
token: STORAGE_KEYS.GATEWAY_TOKEN,
deviceId: STORAGE_KEYS.DEVICE_ID,
});
},
/**
* 获取持久化的 Store 状态 (通用方法)
*/
async getPersistedState<T = unknown>(page: Page, storeName: StoreName): Promise<T | null> {
const key = STORAGE_KEYS[storeName];
if (!key) {
// 非持久化 Store尝试从运行时获取
return this.getRuntimeState<T>(page, storeName);
}
return page.evaluate((storageKey) => {
const stored = localStorage.getItem(storageKey);
if (!stored) return null;
try {
const parsed = JSON.parse(stored);
return parsed.state ? (parsed.state as T) : (parsed as T);
} catch {
return null;
}
}, key);
},
/**
* 获取运行时 Store 状态 (通过 window 全局变量)
* 注意:需要在应用中暴露 store 到 window 对象
*/
async getRuntimeState<T = unknown>(page: Page, storeName: string): Promise<T | null> {
return page.evaluate((name) => {
// 尝试从 window.__ZCLAW_STORES__ 获取
const stores = (window as any).__ZCLAW_STORES__;
if (stores && stores[name]) {
return stores[name].getState() as T;
}
return null;
}, storeName);
},
/**
* 获取整个持久化对象(包含 state 和 version
*/
async getFullStorage<T = unknown>(
page: Page,
storeName: StoreName
): Promise<{ state: T; version: number } | null> {
const key = STORAGE_KEYS[storeName];
if (!key) return null;
return page.evaluate((storageKey) => {
const stored = localStorage.getItem(storageKey);
if (!stored) return null;
try {
return JSON.parse(stored);
} catch {
return null;
}
}, key);
},
/**
* 获取 Store 中的特定字段
*/
async getStateField<T = unknown>(
page: Page,
storeName: StoreName,
fieldPath: string
): Promise<T | null> {
const key = STORAGE_KEYS[storeName];
if (!key) return null;
return page.evaluate(
({ storageKey, path }) => {
const stored = localStorage.getItem(storageKey);
if (!stored) return null;
try {
const parsed = JSON.parse(stored);
const state = parsed.state ? parsed.state : parsed;
const value = path.split('.').reduce((obj: any, key) => obj?.[key], state);
return value as T;
} catch {
return null;
}
},
{ storageKey: key, path: fieldPath }
);
},
/**
* 等待 Store 状态变化
*/
async waitForStateChange<T = unknown>(
page: Page,
storeName: StoreName,
fieldPath: string,
expectedValue: T,
options?: { timeout?: number }
): Promise<void> {
const key = STORAGE_KEYS[storeName];
if (!key) {
throw new Error(`Store ${storeName} is not persisted`);
}
await page.waitForFunction(
({ storageKey, path, expected }) => {
const stored = localStorage.getItem(storageKey);
if (!stored) return false;
try {
const parsed = JSON.parse(stored);
const state = parsed.state ? parsed.state : parsed;
const value = path.split('.').reduce((obj: any, key) => obj?.[key], state);
return JSON.stringify(value) === JSON.stringify(expected);
} catch {
return false;
}
},
{
storageKey: key,
path: fieldPath,
expected: expectedValue,
},
{ timeout: options?.timeout ?? 5000 }
);
},
/**
* 等待 Store 中某个字段存在
*/
async waitForFieldExists(
page: Page,
storeName: StoreName,
fieldPath: string,
options?: { timeout?: number }
): Promise<void> {
const key = STORAGE_KEYS[storeName];
if (!key) return;
await page.waitForFunction(
({ storageKey, path }) => {
const stored = localStorage.getItem(storageKey);
if (!stored) return false;
try {
const parsed = JSON.parse(stored);
const state = parsed.state ? parsed.state : parsed;
const value = path.split('.').reduce((obj: any, key) => obj?.[key], state);
return value !== undefined && value !== null;
} catch {
return false;
}
},
{ storageKey: key, path: fieldPath },
{ timeout: options?.timeout ?? 5000 }
);
},
/**
* 等待消息数量变化
*/
async waitForMessageCount(
page: Page,
expectedCount: number,
options?: { timeout?: number }
): Promise<void> {
await page.waitForFunction(
({ expected, key }) => {
const stored = localStorage.getItem(key);
if (!stored) return false;
try {
const parsed = JSON.parse(stored);
return parsed.state?.messages?.length === expected;
} catch {
return false;
}
},
{ expected: expectedCount, key: STORAGE_KEYS.CHAT },
{ timeout: options?.timeout ?? 10000 }
);
},
/**
* 清除特定 Store 的持久化数据
*/
async clearStore(page: Page, storeName: StoreName): Promise<void> {
const key = STORAGE_KEYS[storeName];
if (key) {
await page.evaluate((storageKey) => {
localStorage.removeItem(storageKey);
}, key);
}
},
/**
* 清除所有 Store 数据
*/
async clearAllStores(page: Page): Promise<void> {
await page.evaluate(() => {
const keys = Object.keys(localStorage);
keys.forEach((key) => {
if (key.startsWith('zclaw-')) {
localStorage.removeItem(key);
}
});
});
},
/**
* 设置 Store 状态(用于测试初始化)
*/
async setStoreState<T>(page: Page, storeName: StoreName, state: T): Promise<void> {
const key = STORAGE_KEYS[storeName];
if (!key) return;
await page.evaluate(
({ storageKey, stateObj }) => {
const data = {
state: stateObj,
version: 0,
};
localStorage.setItem(storageKey, JSON.stringify(data));
},
{ storageKey: key, stateObj: state }
);
},
/**
* 设置 Chat Store 状态
*/
async setChatState<T>(page: Page, state: T): Promise<void> {
await page.evaluate(
({ key, stateObj }) => {
const data = {
state: stateObj,
version: 0,
};
localStorage.setItem(key, JSON.stringify(data));
},
{ key: STORAGE_KEYS.CHAT, stateObj: state }
);
},
/**
* 获取所有 Store 状态快照
*/
async getAllStoresSnapshot(page: Page): Promise<Record<string, unknown>> {
return page.evaluate(() => {
const snapshot: Record<string, unknown> = {};
const keys = Object.keys(localStorage);
keys.forEach((key) => {
if (key.startsWith('zclaw-')) {
const storeName = key.replace('zclaw-', '').replace('-storage', '');
try {
const stored = localStorage.getItem(key);
if (stored) {
const parsed = JSON.parse(stored);
snapshot[storeName] = parsed.state ? parsed.state : parsed;
}
} catch {
// ignore parse errors
}
}
});
return snapshot;
});
},
/**
* 检查 Store 是否持久化
*/
isPersistedStore(storeName: StoreName): boolean {
return STORAGE_KEYS[storeName] !== null;
},
};
/**
* Store 断言工具
*/
export const storeAssertions = {
/**
* 断言 Chat Store 状态匹配预期
*/
async assertChatState<T>(
page: Page,
expected: Partial<T>
): Promise<void> {
const state = await storeInspectors.getChatState<T>(page);
expect(state).not.toBeNull();
for (const [key, value] of Object.entries(expected)) {
expect(state).toHaveProperty(key, value);
}
},
/**
* 断言 Teams Store 状态匹配预期
*/
async assertTeamsState<T>(
page: Page,
expected: Partial<T>
): Promise<void> {
const state = await storeInspectors.getTeamsState<T>(page);
expect(state).not.toBeNull();
for (const [key, value] of Object.entries(expected)) {
expect(state).toHaveProperty(key, value);
}
},
/**
* 断言 Store 字段值
*/
async assertFieldEquals<T>(
page: Page,
storeName: StoreName,
fieldPath: string,
expected: T
): Promise<void> {
const value = await storeInspectors.getStateField<T>(page, storeName, fieldPath);
expect(value).toEqual(expected);
},
/**
* 断言消息数量
*/
async assertMessageCount(page: Page, expected: number): Promise<void> {
const state = await storeInspectors.getChatState<{ messages: unknown[] }>(page);
expect(state?.messages?.length).toBe(expected);
},
/**
* 断言最后一条消息内容
*/
async assertLastMessageContent(page: Page, expected: string): Promise<void> {
const state = await storeInspectors.getChatState<{
messages: Array<{ content: string }>;
}>(page);
const lastMessage = state?.messages?.[state.messages.length - 1];
expect(lastMessage?.content).toContain(expected);
},
/**
* 断言 Gateway 配置
*/
async assertGatewayConfig(
page: Page,
expected: { url?: string; token?: string; deviceId?: string }
): Promise<void> {
const config = await storeInspectors.getGatewayConfig(page);
if (expected.url !== undefined) {
expect(config.url).toBe(expected.url);
}
if (expected.token !== undefined) {
expect(config.token).toBe(expected.token);
}
if (expected.deviceId !== undefined) {
expect(config.deviceId).toBe(expected.deviceId);
}
},
/**
* 断言 Teams 列表非空
*/
async assertTeamsNotEmpty(page: Page): Promise<void> {
const state = await storeInspectors.getTeamsState<{ teams: unknown[] }>(page);
expect(state?.teams?.length).toBeGreaterThan(0);
},
/**
* 断言当前 Agent
*/
async assertCurrentAgent(page: Page, agentId: string): Promise<void> {
const state = await storeInspectors.getChatState<{
currentAgent: { id: string };
}>(page);
expect(state?.currentAgent?.id).toBe(agentId);
},
/**
* 断言 isStreaming 状态
*/
async assertStreamingState(page: Page, expected: boolean): Promise<void> {
const state = await storeInspectors.getChatState<{ isStreaming: boolean }>(page);
expect(state?.isStreaming).toBe(expected);
},
/**
* 断言当前模型
*/
async assertCurrentModel(page: Page, expectedModel: string): Promise<void> {
const state = await storeInspectors.getChatState<{ currentModel: string }>(page);
expect(state?.currentModel).toBe(expectedModel);
},
/**
* 断言会话存在
*/
async assertConversationExists(page: Page, conversationId: string): Promise<void> {
const state = await storeInspectors.getChatState<{
conversations: Array<{ id: string }>;
}>(page);
const exists = state?.conversations?.some(c => c.id === conversationId);
expect(exists).toBe(true);
},
};
/**
* 类型定义 - Chat Store 状态
*/
export interface ChatStoreState {
messages: Array<{
id: string;
role: 'user' | 'assistant' | 'tool' | 'hand' | 'workflow';
content: string;
timestamp: string;
streaming?: boolean;
error?: string;
runId?: string;
toolName?: string;
toolInput?: string;
toolOutput?: string;
handName?: string;
handStatus?: string;
handResult?: unknown;
workflowId?: string;
workflowStep?: string;
workflowStatus?: string;
workflowResult?: unknown;
}>;
conversations: Array<{
id: string;
title: string;
messages: string[];
sessionKey: string | null;
agentId: string | null;
createdAt: string;
updatedAt: string;
}>;
currentConversationId: string | null;
currentAgent: {
id: string;
name: string;
icon: string;
color: string;
lastMessage: string;
time: string;
} | null;
isStreaming: boolean;
currentModel: string;
sessionKey: string | null;
}
/**
* 类型定义 - Team Store 状态
*/
export interface TeamStoreState {
teams: Array<{
id: string;
name: string;
description?: string;
members: Array<{
id: string;
agentId: string;
name: string;
role: 'orchestrator' | 'reviewer' | 'worker';
skills: string[];
workload: number;
status: 'idle' | 'working' | 'offline';
maxConcurrentTasks: number;
currentTasks: string[];
}>;
tasks: Array<{
id: string;
title: string;
description?: string;
status: 'pending' | 'assigned' | 'in_progress' | 'review' | 'completed';
priority: 'low' | 'medium' | 'high';
assigneeId?: string;
dependencies: string[];
type: string;
estimate?: number;
createdAt: string;
updatedAt: string;
}>;
pattern: 'sequential' | 'parallel' | 'pipeline';
activeLoops: Array<{
id: string;
developerId: string;
reviewerId: string;
taskId: string;
state: 'developing' | 'revising' | 'reviewing' | 'approved' | 'escalated';
iterationCount: number;
maxIterations: number;
}>;
status: 'active' | 'paused' | 'completed';
createdAt: string;
updatedAt: string;
}>;
activeTeam: {
id: string;
name: string;
} | null;
metrics: {
tasksCompleted: number;
avgCompletionTime: number;
passRate: number;
avgIterations: number;
escalations: number;
efficiency: number;
} | null;
isLoading: boolean;
error: string | null;
}