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
139 lines
3.9 KiB
Markdown
139 lines
3.9 KiB
Markdown
# 团队功能开发笔记
|
||
|
||
**完成日期**: 2026-03-19
|
||
**任务**: 修复团队功能页面空白问题
|
||
|
||
---
|
||
|
||
## 一、问题描述
|
||
|
||
点击"团队"导航后,页面显示空白,控制台报错 `teams.map is not a function`。
|
||
|
||
## 二、根因分析
|
||
|
||
### 2.1 数据格式冲突
|
||
|
||
Zustand 的 `persist` 中间件存储格式为:
|
||
```json
|
||
{
|
||
"state": { "teams": [...], "activeTeam": ... },
|
||
"version": 0
|
||
}
|
||
```
|
||
|
||
但 `loadTeams` 函数期望的是直接的数组格式 `Team[]`。
|
||
|
||
### 2.2 类型安全问题
|
||
|
||
TeamList 组件中的 `availableAgents` 变量使用了条件表达式,返回类型不一致:
|
||
- `clones` 是 `Clone[]` 类型
|
||
- `agents.map(...)` 返回的是 `{ id, name, role }[]` 类型
|
||
|
||
TypeScript 无法推断统一类型,运行时可能导致错误。
|
||
|
||
## 三、解决方案
|
||
|
||
### 3.1 修复 loadTeams 函数
|
||
|
||
```typescript
|
||
loadTeams: async () => {
|
||
set({ isLoading: true, error: null });
|
||
try {
|
||
const stored = localStorage.getItem('zclaw-teams');
|
||
let teams: Team[] = [];
|
||
|
||
if (stored) {
|
||
const parsed = JSON.parse(stored);
|
||
// 处理 persist 中间件格式
|
||
if (parsed?.state?.teams && Array.isArray(parsed.state.teams)) {
|
||
teams = parsed.state.teams;
|
||
} else if (Array.isArray(parsed)) {
|
||
teams = parsed;
|
||
}
|
||
}
|
||
|
||
set({ teams, isLoading: false });
|
||
} catch (error) {
|
||
set({ teams: [], isLoading: false });
|
||
}
|
||
},
|
||
```
|
||
|
||
### 3.2 修复 availableAgents 类型
|
||
|
||
```typescript
|
||
const availableAgents: Array<{ id: string; name: string; role?: string }> =
|
||
(clones && clones.length > 0)
|
||
? clones.map(c => ({ id: c.id, name: c.name, role: c.role }))
|
||
: (agents && agents.length > 0)
|
||
? agents.map(a => ({ id: a.id, name: a.name, role: '默认助手' }))
|
||
: [];
|
||
```
|
||
|
||
### 3.3 添加防御性检查
|
||
|
||
```typescript
|
||
// TeamList.tsx
|
||
{!Array.isArray(teams) || teams.length === 0 ? (
|
||
<EmptyState ... />
|
||
) : (
|
||
teams.map(...)
|
||
)}
|
||
```
|
||
|
||
## 四、相关文件
|
||
|
||
| 文件 | 修改内容 |
|
||
|------|----------|
|
||
| `store/teamStore.ts` | loadTeams 函数处理 persist 格式 |
|
||
| `components/TeamList.tsx` | 类型修复、防御性检查、中文化 |
|
||
| `components/ui/EmptyState.tsx` | CSS 修复 (flex-1 → h-full) |
|
||
| `App.tsx` | motion.main 添加 flex flex-col |
|
||
|
||
## 五、经验教训
|
||
|
||
1. **persist 中间件存储格式**: Zustand persist 存储的是 `{ state, version }` 结构,不是直接的状态值
|
||
2. **条件表达式类型一致性**: 三元表达式的两个分支必须返回相同类型
|
||
3. **防御性编程**: 对从 store 获取的数据进行 Array.isArray 检查
|
||
|
||
---
|
||
|
||
*文档创建: 2026-03-19*
|
||
|
||
---
|
||
|
||
## 六、协作功能修复 (2026-03-19)
|
||
|
||
### 6.1 问题描述
|
||
|
||
1. **UI 颜色不一致**: SwarmDashboard 使用蓝色(blue-500)作为主色调,与系统的橙色/灰色风格不匹配
|
||
2. **内容重复渲染**: 左侧边栏和主内容区同时渲染 SwarmDashboard,导致内容重复
|
||
|
||
### 6.2 解决方案
|
||
|
||
**问题 1: 内容重复**
|
||
- 从 `Sidebar.tsx` 移除 `{activeTab === 'swarm' && <SwarmDashboard />}` 渲染
|
||
- 只保留 `App.tsx` 中的主内容区渲染
|
||
- 移除未使用的 `import { SwarmDashboard }` 语句
|
||
|
||
**问题 2: 颜色一致性**
|
||
修改 `SwarmDashboard.tsx` 中的配色:
|
||
- 主色调: `blue-500` → `orange-500`
|
||
- 按钮背景: `bg-blue-500` → `bg-orange-500`
|
||
- Filter tabs: `bg-blue-100` → `bg-orange-100`
|
||
- 选中边框: `border-blue-500` → `border-orange-500`
|
||
- Focus ring: `ring-blue-500` → `ring-orange-500`
|
||
- 保留执行状态(`executing`/`running`)的蓝色作为状态指示色
|
||
|
||
### 6.3 相关文件
|
||
|
||
| 文件 | 修改内容 |
|
||
|------|----------|
|
||
| `components/Sidebar.tsx` | 移除 SwarmDashboard 渲染和 import |
|
||
| `components/SwarmDashboard.tsx` | 配色从蓝色改为橙色 |
|
||
|
||
### 6.4 设计原则
|
||
|
||
1. **单一渲染原则**: 每个视图组件只在唯一位置渲染,避免多处同时显示
|
||
2. **颜色一致性**: 交互元素使用系统主色调(橙色),状态指示可保留语义色(蓝色=执行中,绿色=完成,红色=失败)
|