fix(presentation): 修复 presentation 模块类型错误和语法问题
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled

- 创建 types.ts 定义完整的类型系统
- 重写 DocumentRenderer.tsx 修复语法错误
- 重写 QuizRenderer.tsx 修复语法错误
- 重写 PresentationContainer.tsx 添加类型守卫
- 重写 TypeSwitcher.tsx 修复类型引用
- 更新 index.ts 移除不存在的 ChartRenderer 导出

审计结果:
- 类型检查: 通过
- 单元测试: 222 passed
- 构建: 成功
This commit is contained in:
iven
2026-03-26 17:19:28 +08:00
parent d0c6319fc1
commit b7f3d94950
71 changed files with 15896 additions and 1133 deletions

View File

@@ -15,7 +15,9 @@ knowledge-base/
├── agent-provider-config.md # Agent 和 LLM 提供商配置
├── tauri-desktop.md # Tauri 桌面端开发笔记
├── feature-checklist.md # 功能清单和验证状态
── hands-integration-lessons.md # Hands 集成经验总结
── hands-integration-lessons.md # Hands 集成经验总结
├── openmaic-analysis.md # OpenMAIC 项目深度分析
└── openmaic-zclaw-comparison.md # OpenMAIC vs ZCLAW 对比分析
```
## 快速索引
@@ -46,11 +48,19 @@ knowledge-base/
| 功能清单 | [feature-checklist.md](./feature-checklist.md) | 所有功能的验证状态 |
| Hands 集成 | [hands-integration-lessons.md](./hands-integration-lessons.md) | Hands 功能集成经验 |
### 参考项目分析
| 主题 | 文件 | 说明 |
|------|------|------|
| OpenMAIC 分析 | [openmaic-analysis.md](./openmaic-analysis.md) | 清华大学 AI 教育平台深度分析 |
| 对比分析 | [openmaic-zclaw-comparison.md](./openmaic-zclaw-comparison.md) | OpenMAIC vs ZCLAW 功能对比 |
## 版本历史
| 日期 | 版本 | 变更 |
|------|------|------|
| 2026-03-19 | v2.0 | 重构为 ZCLAW 独立产品文档 |
| 2026-03-26 | v2.1 | 添加 OpenMAIC 深度分析,补充 StreamBuffer、Director、Action 引擎架构 |
| 2026-03-22 | v2.0 | 重构为 ZCLAW 独立产品文档,添加 OpenMAIC 对比分析 |
| 2026-03-14 | v1.1 | 添加 Hands 集成经验总结、功能清单 |
| 2026-03-14 | v1.0 | 初始创建 |

View File

@@ -1,7 +1,8 @@
# OpenMAIC 深度分析报告
> **来源**: https://github.com/THU-MAIC/OpenMAIC
> **分析日期**: 2026-03-22
> **本地路径**: G:\edu\OpenMAIC
> **分析日期**: 2026-03-22 (初版) / 2026-03-26 (深度分析)
> **许可证**: AGPL-3.0
## 1. 项目概述
@@ -556,3 +557,454 @@ skills/classroom-generator/SKILL.md # 课堂生成
- [ ] 创建教育类 Handswhiteboard、slideshow、speech、quiz
- [ ] 开发 classroom-generator Skill
- [ ] 增强工作流编排能力DAG、条件分支
---
## 11. 深度架构分析 (2026-03-26 补充)
### 11.1 Director Graph 核心实现
**文件**: `lib/orchestration/director-graph.ts`
#### 11.1.1 状态定义
```typescript
const OrchestratorState = Annotation.Root({
// 输入 (图入口时设置一次)
messages: Annotation<UIMessage<ChatMessageMetadata>[]>,
storeState: Annotation<{
stage: Stage | null;
scenes: Scene[];
currentSceneId: string | null;
mode: StageMode;
whiteboardOpen: boolean;
}>,
availableAgentIds: Annotation<string[]>,
maxTurns: Annotation<number>,
languageModel: Annotation<LanguageModel>,
triggerAgentId: Annotation<string | null>,
agentConfigOverrides: Annotation<Record<string, AgentConfig>>,
// 可变 (节点更新)
currentAgentId: Annotation<string | null>,
turnCount: Annotation<number>,
agentResponses: Annotation<AgentTurnSummary[]>({
reducer: (prev, update) => [...prev, ...update],
default: () => [],
}),
whiteboardLedger: Annotation<WhiteboardActionRecord[]>({
reducer: (prev, update) => [...prev, ...update],
default: () => [],
}),
shouldEnd: Annotation<boolean>,
totalActions: Annotation<number>,
});
```
#### 11.1.2 Director 节点核心逻辑
```typescript
async function directorNode(state, config) {
const write = config.writer as (chunk: StatelessEvent) => void;
const isSingleAgent = state.availableAgentIds.length <= 1;
// Turn limit check
if (state.turnCount >= state.maxTurns) {
return { shouldEnd: true };
}
// 单 Agent: 纯代码逻辑,无 LLM 调用
if (isSingleAgent) {
const agentId = state.availableAgentIds[0] || 'default-1';
if (state.turnCount === 0) {
write({ type: 'thinking', data: { stage: 'agent_loading', agentId } });
return { currentAgentId: agentId, shouldEnd: false };
}
write({ type: 'cue_user', data: { fromAgentId: agentId } });
return { shouldEnd: true };
}
// 多 Agent: 快速路径 - 触发 Agent
if (state.turnCount === 0 && state.triggerAgentId) {
const triggerId = state.triggerAgentId;
if (state.availableAgentIds.includes(triggerId)) {
write({ type: 'thinking', data: { stage: 'agent_loading', agentId: triggerId } });
return { currentAgentId: triggerId, shouldEnd: false };
}
}
// 多 Agent: LLM 决策
write({ type: 'thinking', data: { stage: 'director' } });
const prompt = buildDirectorPrompt(agents, conversationSummary, ...);
const result = await adapter._generate([new SystemMessage(prompt), ...]);
const decision = parseDirectorDecision(result.generations[0]?.text || '');
if (decision.nextAgentId === 'USER') {
write({ type: 'cue_user', data: { fromAgentId: state.currentAgentId } });
return { shouldEnd: true };
}
write({ type: 'thinking', data: { stage: 'agent_loading', agentId: decision.nextAgentId } });
return { currentAgentId: decision.nextAgentId, shouldEnd: false };
}
```
### 11.2 StreamBuffer 节奏控制
**文件**: `lib/buffer/stream-buffer.ts`
#### 11.2.1 设计目的
位于 SSE 流和 React 状态之间的统一内容展示节奏控制层:
- 固定速率 tick 循环逐字显示文本
- 按顺序触发 Action 回调
- 避免 LLM 流式输出和前端打字机的双重效果
#### 11.2.2 缓冲项类型
```typescript
type BufferItem =
| { kind: 'agent_start'; messageId: string; agentId: string; agentName: string; avatar?: string; color?: string }
| { kind: 'agent_end'; messageId: string; agentId: string }
| { kind: 'text'; messageId: string; agentId: string; partId: string; text: string; sealed: boolean }
| { kind: 'action'; messageId: string; actionId: string; actionName: string; params: Record<string, unknown>; agentId: string }
| { kind: 'thinking'; stage: string; agentId?: string }
| { kind: 'cue_user'; fromAgentId?: string; prompt?: string }
| { kind: 'done'; totalActions: number; totalAgents: number; directorState?: DirectorState }
| { kind: 'error'; message: string };
```
#### 11.2.3 回调接口
```typescript
interface StreamBufferCallbacks {
onAgentStart(data: AgentStartItem): void;
onAgentEnd(data: AgentEndItem): void;
onTextReveal(messageId: string, partId: string, revealedText: string, isComplete: boolean): void;
onActionReady(messageId: string, data: ActionItem): void;
onLiveSpeech(text: string | null, agentId: string | null): void; // Roundtable 实时语音
onSpeechProgress(ratio: number | null): void; // 播放进度
onThinking(data: { stage: string; agentId?: string } | null): void;
onCueUser(fromAgentId?: string, prompt?: string): void;
onDone(data: { totalActions: number; totalAgents: number; directorState?: DirectorState }): void;
onError(message: string): void;
}
```
#### 11.2.4 Tick 循环核心逻辑
```typescript
private tick(): void {
if (this._paused || this._disposed) return;
const item = this.items[this.readIndex];
if (!item) return;
switch (item.kind) {
case 'text': {
// 逐字显示
this.charCursor = Math.min(this.charCursor + this.charsPerTick, item.text.length);
const revealed = item.text.slice(0, this.charCursor);
const fullyRevealed = this.charCursor >= item.text.length;
const isComplete = fullyRevealed && item.sealed;
this.cb.onTextReveal(item.messageId, item.partId, revealed, isComplete);
this.cb.onLiveSpeech(revealed, this.currentAgentId);
this.cb.onSpeechProgress(item.text.length > 0 ? this.charCursor / item.text.length : 1);
if (isComplete) {
this.readIndex++;
this.charCursor = 0;
this.advanceNonText(); // 处理后续非文本项
}
break;
}
case 'action': {
this.cb.onActionReady(item.messageId, item);
this.readIndex++;
// Action 后延迟,让动画有时间播放
if (this.actionDelayTicks > 0) {
this._dwellTicksRemaining = this.actionDelayTicks;
}
break;
}
// ... 其他类型
}
}
```
#### 11.2.5 配置选项
```typescript
interface StreamBufferOptions {
tickMs?: number; // Tick 间隔,默认 30ms
charsPerTick?: number; // 每 tick 显示字符数,默认 1
postTextDelayMs?: number; // 文本完成后延迟
actionDelayMs?: number; // Action 后延迟
}
```
### 11.3 Action 引擎详细实现
**文件**: `lib/action/engine.ts`
#### 11.3.1 执行模式
| 模式 | 动作 | 行为 |
|------|------|------|
| **Fire-and-forget** | spotlight, laser | 立即返回,不等待 |
| **Synchronous** | speech, wb_*, play_video, discussion | 返回 Promise等待完成 |
#### 11.3.2 核心执行流程
```typescript
export class ActionEngine {
private stageStore: StageStore;
private audioPlayer: AudioPlayer | null;
private effectTimer: ReturnType<typeof setTimeout> | null = null;
async execute(action: Action): Promise<void> {
// 自动打开白板
if (action.type.startsWith('wb_') && action.type !== 'wb_open' && action.type !== 'wb_close') {
await this.ensureWhiteboardOpen();
}
switch (action.type) {
case 'spotlight':
this.executeSpotlight(action);
return; // Fire-and-forget
case 'speech':
return this.executeSpeech(action); // Synchronous
// ... 其他
}
}
// 视觉特效自动清除
private scheduleEffectClear(): void {
if (this.effectTimer) clearTimeout(this.effectTimer);
this.effectTimer = setTimeout(() => {
useCanvasStore.getState().clearAllEffects();
}, 5000); // 5 秒后自动清除
}
}
```
#### 11.3.3 白板动作实现
```typescript
private async executeWbDrawText(action: WbDrawTextAction): Promise<void> {
const wb = this.stageAPI.whiteboard.get();
if (!wb.success || !wb.data) return;
this.stageAPI.whiteboard.addElement({
id: action.elementId || '',
type: 'text',
content: action.content,
left: action.x,
top: action.y,
width: action.width ?? 400,
height: action.height ?? 100,
defaultColor: action.color ?? '#333333',
}, wb.data.id);
await delay(800); // 等待淡入动画
}
```
### 11.4 设置状态管理
**文件**: `lib/store/settings.ts`
#### 11.4.1 状态结构
```typescript
interface SettingsState {
// 模型选择
providerId: ProviderId;
modelId: string;
providersConfig: ProvidersConfig;
// TTS/ASR 设置
ttsProviderId: TTSProviderId;
ttsVoice: string;
ttsSpeed: number;
asrProviderId: ASRProviderId;
asrLanguage: string;
ttsProvidersConfig: Record<TTSProviderId, {...}>;
asrProvidersConfig: Record<ASRProviderId, {...}>;
// 媒体生成
imageProviderId: ImageProviderId;
imageModelId: string;
videoProviderId: VideoProviderId;
videoModelId: string;
imageGenerationEnabled: boolean;
videoGenerationEnabled: boolean;
// Web Search
webSearchProviderId: WebSearchProviderId;
webSearchProvidersConfig: Record<WebSearchProviderId, {...}>;
// Agent 设置
selectedAgentIds: string[];
maxTurns: string;
agentMode: 'preset' | 'auto';
autoAgentCount: number;
// 播放控制
ttsMuted: boolean;
ttsVolume: number;
autoPlayLecture: boolean;
playbackSpeed: PlaybackSpeed;
// 布局
sidebarCollapsed: boolean;
chatAreaCollapsed: boolean;
chatAreaWidth: number;
}
```
#### 11.4.2 持久化与迁移
```typescript
export const useSettingsStore = create<SettingsState>()(
persist(
(set) => ({ /* state and actions */ }),
{
name: 'settings-storage',
version: 2,
migrate: (persistedState, version) => {
// 版本迁移逻辑
if (version === 0) { /* ... */ }
if (version < 2) { /* ... */ }
return state;
},
merge: (persistedState, currentState) => {
// 合并内置 Provider 配置
const merged = { ...currentState, ...persistedState };
ensureBuiltInProviders(merged);
return merged;
},
},
),
);
```
#### 11.4.3 服务器配置合并
```typescript
fetchServerProviders: async () => {
const res = await fetch('/api/server-providers');
const data = await res.json();
set((state) => {
// 重置所有服务器标记
// 合并服务器配置
// 自动选择/启用 (仅首次)
return { /* updated state */ };
});
}
```
### 11.5 无状态请求设计
**文件**: `lib/types/chat.ts`
```typescript
interface StatelessChatRequest {
// 对话历史 (客户端维护)
messages: UIMessage<ChatMessageMetadata>[];
// 当前应用状态
storeState: {
stage: Stage | null;
scenes: Scene[];
currentSceneId: string | null;
mode: StageMode;
whiteboardOpen: boolean;
};
// Agent 配置
config: {
agentIds: string[];
sessionType?: 'qa' | 'discussion';
discussionTopic?: string;
triggerAgentId?: string;
agentConfigs?: Array<{...}>; // 动态生成的 Agent
};
// 跨轮次状态 (Director)
directorState?: DirectorState;
// 用户配置
userProfile?: { nickname?: string; bio?: string };
// API 凭证
apiKey: string;
baseUrl?: string;
model?: string;
}
```
### 11.6 SSE 事件类型完整定义
```typescript
type StatelessEvent =
| { type: 'agent_start'; data: { messageId, agentId, agentName, agentAvatar?, agentColor? } }
| { type: 'agent_end'; data: { messageId, agentId } }
| { type: 'text_delta'; data: { content, messageId? } }
| { type: 'action'; data: { actionId, actionName, params, agentId, messageId? } }
| { type: 'thinking'; data: { stage: 'director' | 'agent_loading'; agentId? } }
| { type: 'cue_user'; data: { fromAgentId?, prompt? } }
| { type: 'done'; data: { totalActions, totalAgents, agentHadContent?, directorState? } }
| { type: 'error'; data: { message } };
```
---
## 12. 对 ZCLAW 的关键借鉴点
### 12.1 StreamBuffer 节奏控制
**问题**: ZCLAW 目前可能存在 LLM 流式输出和前端打字机的双重效果
**解决方案**:
1. 引入类似 StreamBuffer 的中间层
2. 统一 Chat 和 Agent 回复的内容展示节奏
3. 支持暂停/恢复/刷新
### 12.2 Director 快速路径优化
**问题**: 每次都需要 LLM 决策下一个 Agent
**解决方案**:
1. 单 Agent 场景跳过 LLM 调用
2. 触发 Agent 场景直接调度
3. 仅多 Agent 复杂场景使用 LLM 决策
### 12.3 无状态设计
**问题**: ZCLAW 服务端 Session 管理复杂
**解决方案**:
1. 考虑将部分状态迁移到客户端
2. 服务端只做生成,不维护会话状态
3. 每次请求携带完整上下文
### 12.4 Action 引擎统一执行
**问题**: ZCLAW Hands 系统执行逻辑分散
**解决方案**:
1. 创建统一的 ActionEngine 类
2. 区分 Fire-and-forget 和 Synchronous 模式
3. 自动处理前置条件 (如白板自动打开)
### 12.5 设置版本迁移
**问题**: ZCLAW 配置更新时需要清理缓存
**解决方案**:
1. 实现 Zustand persist 的 migrate 函数
2. 支持 merge 函数合并新默认值
3. 保持用户配置不丢失

View File

@@ -1,6 +1,6 @@
# OpenMAIC vs ZCLAW 功能对比分析
> **分析日期**: 2026-03-22
> **分析日期**: 2026-03-22 (初版) / 2026-03-26 (深度分析)
> **目的**: 论证 ZCLAW 是否能实现 OpenMAIC 相同的产出
---
@@ -382,3 +382,261 @@ P2 (增强):
5. **课堂生成 Skill**
- `skills/classroom-generator/SKILL.md` - 完整的技能定义
---
## 7. 深度架构对比 (2026-03-26 补充)
### 7.1 流式响应处理对比
| 维度 | OpenMAIC | ZCLAW |
|------|----------|-------|
| **传输协议** | SSE (Server-Sent Events) | gRPC Stream / Tauri Events |
| **节奏控制** | StreamBuffer (中间层) | 无统一控制 |
| **打字机效果** | 单层 (StreamBuffer tick) | 可能双层 (LLM + 前端) |
| **暂停/恢复** | StreamBuffer.pause/resume | 需实现 |
| **刷新** | StreamBuffer.flush | 需实现 |
**OpenMAIC StreamBuffer 优势**:
- 统一的内容展示节奏控制
- 避免 LLM 流式输出和前端打字机的双重效果
- 精确控制 Action 触发时机
- 支持 Roundtable 实时语音显示
### 7.2 多 Agent 编排对比
| 维度 | OpenMAIC | ZCLAW |
|------|----------|-------|
| **编排引擎** | LangGraph StateGraph | 自定义 Director |
| **单 Agent 优化** | 纯代码逻辑,无 LLM | 需实现 |
| **多 Agent 决策** | LLM + 快速路径 | A2A Router |
| **状态传递** | OrchestratorState Annotation | DirectorState struct |
| **轮次管理** | turnCount + maxTurns | 需实现 |
**OpenMAIC Director 策略**:
```typescript
// 单 Agent: 纯代码逻辑
if (isSingleAgent) {
if (turnCount === 0) return { currentAgentId: agentId };
return { cueUser: true };
}
// 多 Agent: 快速路径
if (turnCount === 0 && triggerAgentId) {
return { currentAgentId: triggerAgentId };
}
// 多 Agent: LLM 决策
const decision = await llm.decide(agents, context);
return { currentAgentId: decision.nextAgentId };
```
### 7.3 工具/动作执行对比
| 维度 | OpenMAIC | ZCLAW |
|------|----------|-------|
| **执行引擎** | ActionEngine (统一类) | Hands (Trait) |
| **动作数量** | 28+ 种 | 8 个 Hand |
| **执行模式** | Fire-and-forget / Synchronous | needs_approval |
| **前置条件** | 自动处理 (如白板) | dependencies |
| **动画协调** | delay 等待 | 无 |
**OpenMAIC Action 分类**:
```typescript
// Fire-and-forget: 立即返回
case 'spotlight':
case 'laser':
executeImmediate(action);
return;
// Synchronous: 等待完成
case 'speech':
await playTTS(action);
return;
case 'wb_draw_text':
await drawOnWhiteboard(action);
await delay(800); // 等待动画
return;
```
### 7.4 状态管理对比
| 维度 | OpenMAIC | ZCLAW |
|------|----------|-------|
| **后端状态** | 无状态 | SQLite Session |
| **客户端状态** | Zustand + IndexedDB | Tauri 前端 |
| **持久化** | persist middleware | 需实现 |
| **版本迁移** | migrate 函数 | 需实现 |
| **服务器配置合并** | fetchServerProviders | 需实现 |
**OpenMAIC 无状态设计**:
- 所有状态由客户端维护
- 每次请求携带完整上下文
- 后端只做生成,不存储会话
- 便于水平扩展
### 7.5 提示词管理对比
| 维度 | OpenMAIC | ZCLAW |
|------|----------|-------|
| **Agent 提示词** | persona 字段 | SKILL.md |
| **系统提示词构建** | prompt-builder.ts | 需实现 |
| **上下文注入** | 结构化 (scene, whiteboard, etc.) | 需实现 |
| **Director 提示词** | director-prompt.ts | 无 |
### 7.6 媒体生成对比
| 维度 | OpenMAIC | ZCLAW |
|------|----------|-------|
| **图像生成** | 多 Provider (Seedream, Qwen, etc.) | 无 |
| **视频生成** | 多 Provider (Seedance, Kling, etc.) | 无 |
| **TTS** | 多 Provider (OpenAI, Azure, GLM, etc.) | speech.HAND.toml |
| **ASR** | 多 Provider (OpenAI, Qwen, etc.) | 无 |
---
## 8. ZCLAW 优化建议 (基于深度分析)
### 8.1 优先级 P0: StreamBuffer 实现
**目标**: 统一内容展示节奏控制
**实现步骤**:
1. 创建 `StreamBuffer`
2. 定义缓冲项类型
3. 实现 tick 循环
4. 连接到 Tauri 事件系统
**预期效果**:
- 消除双重打字机效果
- 支持暂停/恢复/刷新
- 精确控制 Action 触发
### 8.2 优先级 P1: Director 快速路径
**目标**: 优化单 Agent 场景性能
**实现步骤**:
1. 检测 Agent 数量
2. 单 Agent 场景跳过 LLM 决策
3. 触发 Agent 场景直接调度
4. 仅复杂场景使用 LLM
**预期效果**:
- 减少不必要的 LLM 调用
- 降低延迟
- 节省成本
### 8.3 优先级 P1: Action 引擎增强
**目标**: 统一动作执行接口
**实现步骤**:
1. 创建 `ActionEngine`
2. 区分 Fire-and-forget / Synchronous
3. 实现自动前置条件处理
4. 添加动画协调
**预期效果**:
- 统一的执行接口
- 更好的动画协调
- 更清晰的动作分类
### 8.4 优先级 P2: 设置版本迁移
**目标**: 支持配置升级不丢失
**实现步骤**:
1. 实现 Zustand persist migrate
2. 实现 merge 函数
3. 测试版本升级场景
**预期效果**:
- 配置升级无损
- 新默认值自动合并
---
## 9. 代码参考: StreamBuffer 核心实现
```typescript
// lib/buffer/stream-buffer.ts (OpenMAIC)
export class StreamBuffer {
private items: BufferItem[] = [];
private readIndex = 0;
private charCursor = 0;
private _paused = false;
private timer: ReturnType<typeof setInterval> | null = null;
constructor(
private cb: StreamBufferCallbacks,
private options?: StreamBufferOptions,
) {
this.tickMs = options?.tickMs ?? 30;
this.charsPerTick = options?.charsPerTick ?? 1;
}
start(): void {
if (this.timer) return;
this.timer = setInterval(() => this.tick(), this.tickMs);
}
pause(): void { this._paused = true; }
resume(): void { this._paused = false; }
flush(): void {
while (this.readIndex < this.items.length) {
// 立即处理所有项
}
}
private tick(): void {
if (this._paused) return;
const item = this.items[this.readIndex];
if (!item) return;
if (item.kind === 'text') {
this.charCursor = Math.min(this.charCursor + this.charsPerTick, item.text.length);
const revealed = item.text.slice(0, this.charCursor);
this.cb.onTextReveal(item.messageId, item.partId, revealed, ...);
if (this.charCursor >= item.text.length && item.sealed) {
this.readIndex++;
this.charCursor = 0;
}
}
// ... 其他类型
}
}
```
---
## 10. 总结
### 10.1 OpenMAIC 的核心优势
1. **StreamBuffer** - 统一的内容展示节奏控制
2. **Director 优化** - 单 Agent 场景无 LLM 调用
3. **无状态设计** - 易于水平扩展
4. **Action 引擎** - 统一的执行接口
5. **多 Provider** - 灵活的服务集成
### 10.2 ZCLAW 可直接借鉴
| 功能 | 复杂度 | 价值 |
|------|--------|------|
| StreamBuffer | 中 | 高 |
| Director 快速路径 | 低 | 高 |
| 设置迁移 | 低 | 中 |
| Action 模式分类 | 低 | 中 |
### 10.3 ZCLAW 需要自研
| 功能 | 原因 |
|------|------|
| Tauri 事件集成 | OpenMAIC 是 Web |
| SQLite 状态管理 | OpenMAIC 无状态 |
| Hands 执行实现 | OpenMAIC 有完整实现 |

View File

@@ -1569,6 +1569,101 @@ async fn load_skill_from_dir(&self, dir: &PathBuf) -> Result<()> {
---
## 10.4 Pipeline YAML 解析失败 - 类型不匹配
**症状**:
- Pipeline 列表显示为空Found 0 pipelines
- 后端调试日志显示扫描目录成功但没有找到任何 Pipeline
- 没有明显的错误消息
**根本原因**: YAML 文件中的字段类型与 Rust 类型定义不匹配
**问题分析**:
1. **FileExport action formats 字段类型不匹配**:
- Rust 定义:`formats: Vec<ExportFormat>`(枚举数组)
- YAML 写法:`formats: ${inputs.export_formats}`(表达式字符串)
- serde_yaml 无法将字符串解析为枚举数组,静默失败
2. **InputType serde rename_all 配置错误**:
- YAML 使用 `multi-select`kebab-case
- Rust serde 配置 `rename_all = "snake_case"`
- 期望 `multi_select` 但收到 `multi-select`,解析失败
**修复方案**:
1. **将 formats 字段改为 String 类型** (`types.rs`):
```rust
FileExport {
formats: String, // 从 Vec<ExportFormat> 改为 String
input: String,
output_dir: Option<String>,
}
```
2. **在运行时解析 formats 表达式** (`executor.rs`):
```rust
let resolved_formats = context.resolve(formats)?;
let format_strings: Vec<String> = if resolved_formats.is_array() {
resolved_formats.as_array()?
.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
} else if resolved_formats.is_string() {
// 尝试解析为 JSON 数组
serde_json::from_str(s).unwrap_or_else(|_| vec![s.to_string()])
} else {
return Err(...);
};
// 转换为 ExportFormat 枚举
let export_formats: Vec<ExportFormat> = format_strings
.iter()
.filter_map(|s| match s.to_lowercase().as_str() {
"pptx" => Some(ExportFormat::Pptx),
"html" => Some(ExportFormat::Html),
"pdf" => Some(ExportFormat::Pdf),
"markdown" | "md" => Some(ExportFormat::Markdown),
"json" => Some(ExportFormat::Json),
_ => None,
})
.collect();
```
3. **修正 InputType serde 配置** (`types.rs`):
```rust
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "kebab-case")] // 从 snake_case 改为 kebab-case
pub enum InputType {
#[default]
String,
Number,
Boolean,
Select,
MultiSelect, // YAML 中写 multi-select
File,
Text,
}
```
**影响范围**:
- `crates/zclaw-pipeline/src/types.rs` - InputType serde, FileExport formats
- `crates/zclaw-pipeline/src/executor.rs` - 运行时解析 formats
- `pipelines/**/*.yaml` - 确保使用 `multi-select` 而非 `multi_select`
**验证修复**:
```
[DEBUG pipeline_list] Found 5 pipelines
[DEBUG pipeline_list] Pipeline: classroom-generator -> category: education, industry: 'education'
```
**最佳实践**:
- YAML 中的表达式(如 `${inputs.xxx}`)应该定义为 String 类型
- 在运行时通过 ExecutionContext.resolve() 解析表达式
- 使用 `kebab-case` 命名风格更符合 YAML 惯例
---
## 11. 相关文档
- [OpenFang 配置指南](./openfang-configuration.md) - 配置文件位置、格式和最佳实践

View File

@@ -0,0 +1,757 @@
# ZCLAW Agent 成长功能设计规格
> **版本**: 1.0
> **日期**: 2026-03-26
> **状态**: 已批准
> **作者**: Claude + 用户协作设计
---
## 一、概述
### 1.1 背景
ZCLAW 当前的学习系统存在**前后端分离问题**
- 前端有完整的学习逻辑 (`active-learning.ts`, `memory-extractor.ts`)
- 但这些学习结果存储在 localStorage/IndexedDB
- 后端执行系统 (Rust) 无法获取这些学习结果
- 导致 Agent 无法真正"成长"
### 1.2 目标
设计并实现完整的 Agent 成长功能,让 Agent 像个人管家一样:
- **记住偏好**:用户的沟通风格、回复格式、语言偏好等
- **积累知识**:从对话中学习用户相关事实、领域知识、经验教训
- **掌握技能**:记录技能/Hand 的使用模式,优化执行效率
### 1.3 需求决策
| 维度 | 决策 | 理由 |
|------|------|------|
| 成长维度 | 偏好 + 知识 + 技能(全部) | 完整的管家式成长体验 |
| 整合策略 | 完全后端化Rust 重写 | 避免前后端数据隔离问题 |
| 存储架构 | OpenViking 作为完整记忆层 | 利用现有的 L0/L1/L2 分层 + 语义搜索 |
| 学习触发 | 对话后自动 + 用户显式触发 | 平衡自动化和可控性 |
| 行为影响 | 智能检索 + Token 预算控制 | 解决长期使用后数据量过大的问题 |
---
## 二、系统架构
### 2.1 整体架构图
```
┌─────────────────────────────────────────────────────────────────┐
│ ZCLAW Agent 成长系统 │
├─────────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ zclaw-growth (新 Crate) │ │
│ │ ────────────────────────────────────────────────────── │ │
│ │ • MemoryExtractor - 从对话中提取偏好/知识/经验 │ │
│ │ • MemoryRetriever - 语义检索相关记忆 │ │
│ │ • PromptInjector - 动态构建 system_prompt │ │
│ │ • GrowthTracker - 追踪成长指标和演化 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ OpenViking (记忆层) │ │
│ │ ────────────────────────────────────────────────────── │ │
│ │ URI 结构: │ │
│ │ • agent://{id}/preferences/{category} - 用户偏好 │ │
│ │ • agent://{id}/knowledge/{domain} - 知识积累 │ │
│ │ • agent://{id}/experience/{skill} - 技能经验 │ │
│ │ • agent://{id}/sessions/{sid} - 对话历史 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ zclaw-runtime (修改) │ │
│ │ ────────────────────────────────────────────────────── │ │
│ │ AgentLoop 集成: │ │
│ │ 1. 对话前 → MemoryRetriever 检索相关记忆 │ │
│ │ 2. 构建请求 → PromptInjector 注入记忆 │ │
│ │ 3. 对话后 → MemoryExtractor 提取新记忆 │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
### 2.2 数据流
```
用户输入
┌─────────────────────────────────────────┐
│ 1. 记忆检索 │
│ • 用当前输入查询 OpenViking │
│ • 召回 Top-5 相关记忆 │
│ • Token 预算控制 (500 tokens) │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ 2. Prompt 构建 │
│ system_prompt = base + │
│ "## 用户偏好\n" + preferences + │
│ "## 相关知识\n" + knowledge │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ 3. LLM 对话 │
│ • 正常的 AgentLoop 执行 │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ 4. 记忆提取 (对话后) │
│ • 分析对话内容 │
│ • 提取偏好/知识/经验 │
│ • 写入 OpenViking (L0/L1/L2) │
└─────────────────────────────────────────┘
```
### 2.3 OpenViking URI 结构
```
agent://{agent_id}/
├── preferences/
│ ├── communication-style # 沟通风格偏好
│ ├── response-format # 回复格式偏好
│ ├── language-preference # 语言偏好
│ └── topic-interests # 主题兴趣
├── knowledge/
│ ├── user-facts # 用户相关事实
│ ├── domain-knowledge # 领域知识
│ └── lessons-learned # 经验教训
├── experience/
│ ├── skill-{id} # 技能使用经验
│ └── hand-{id} # Hand 使用经验
└── sessions/
└── {session_id}/ # 对话历史
├── raw # 原始对话 (L0)
├── summary # 摘要 (L1)
└── keywords # 关键词 (L2)
```
---
## 三、详细设计
### 3.1 新 Crate 结构
```
crates/zclaw-growth/
├── Cargo.toml
├── src/
│ ├── lib.rs # 入口和公共 API
│ ├── extractor.rs # 记忆提取器
│ ├── retriever.rs # 记忆检索器
│ ├── injector.rs # Prompt 注入器
│ ├── tracker.rs # 成长追踪器
│ ├── types.rs # 类型定义
│ └── viking_adapter.rs # OpenViking 适配器
```
### 3.2 核心类型定义
```rust
// types.rs
/// 记忆类型
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MemoryType {
Preference, // 偏好
Knowledge, // 知识
Experience, // 经验
Session, // 对话
}
/// 记忆条目
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryEntry {
pub uri: String,
pub memory_type: MemoryType,
pub content: String,
pub keywords: Vec<String>,
pub importance: u8, // 1-10
pub access_count: u32,
pub created_at: DateTime<Utc>,
pub last_accessed: DateTime<Utc>,
}
/// 提取的记忆
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExtractedMemory {
pub memory_type: MemoryType,
pub category: String,
pub content: String,
pub confidence: f32, // 提取置信度 0.0-1.0
pub source_session: SessionId,
}
/// 检索配置
#[derive(Debug, Clone)]
pub struct RetrievalConfig {
pub max_tokens: usize, // 总 Token 预算,默认 500
pub preference_budget: usize, // 偏好 Token 预算,默认 200
pub knowledge_budget: usize, // 知识 Token 预算,默认 200
pub experience_budget: usize, // 经验 Token 预算,默认 100
pub min_similarity: f32, // 最小相似度阈值,默认 0.7
pub max_results: usize, // 最大返回数量,默认 10
}
impl Default for RetrievalConfig {
fn default() -> Self {
Self {
max_tokens: 500,
preference_budget: 200,
knowledge_budget: 200,
experience_budget: 100,
min_similarity: 0.7,
max_results: 10,
}
}
}
/// 检索结果
#[derive(Debug, Clone, Default)]
pub struct RetrievalResult {
pub preferences: Vec<MemoryEntry>,
pub knowledge: Vec<MemoryEntry>,
pub experience: Vec<MemoryEntry>,
pub total_tokens: usize,
}
/// 提取配置
#[derive(Debug, Clone)]
pub struct ExtractionConfig {
pub extract_preferences: bool, // 是否提取偏好,默认 true
pub extract_knowledge: bool, // 是否提取知识,默认 true
pub extract_experience: bool, // 是否提取经验,默认 true
pub min_confidence: f32, // 最小置信度阈值,默认 0.6
}
impl Default for ExtractionConfig {
fn default() -> Self {
Self {
extract_preferences: true,
extract_knowledge: true,
extract_experience: true,
min_confidence: 0.6,
}
}
}
```
### 3.3 MemoryExtractor 接口
```rust
// extractor.rs
/// 记忆提取器 - 从对话中提取有价值的记忆
pub struct MemoryExtractor {
llm_driver: Arc<dyn LlmDriver>,
}
impl MemoryExtractor {
pub fn new(llm_driver: Arc<dyn LlmDriver>) -> Self {
Self { llm_driver }
}
/// 从对话中提取记忆
pub async fn extract(
&self,
messages: &[Message],
config: &ExtractionConfig,
) -> Result<Vec<ExtractedMemory>> {
let mut results = Vec::new();
if config.extract_preferences {
let prefs = self.extract_preferences(messages).await?;
results.extend(prefs);
}
if config.extract_knowledge {
let knowledge = self.extract_knowledge(messages).await?;
results.extend(knowledge);
}
if config.extract_experience {
let experience = self.extract_experience(messages).await?;
results.extend(experience);
}
// 过滤低置信度结果
results.retain(|m| m.confidence >= config.min_confidence);
Ok(results)
}
/// 提取偏好
async fn extract_preferences(
&self,
messages: &[Message],
) -> Result<Vec<ExtractedMemory>> {
// 使用 LLM 分析对话,提取用户偏好
// 例如:用户喜欢简洁的回复、用户偏好中文等
// ...
}
/// 提取知识
async fn extract_knowledge(
&self,
messages: &[Message],
) -> Result<Vec<ExtractedMemory>> {
// 使用 LLM 分析对话,提取有价值的事实和知识
// 例如:用户是程序员、用户在做一个 Rust 项目等
// ...
}
/// 提取经验
async fn extract_experience(
&self,
messages: &[Message],
) -> Result<Vec<ExtractedMemory>> {
// 分析对话中的技能/工具使用,提取经验教训
// 例如:某个技能执行失败、某个工具效果很好等
// ...
}
}
```
### 3.4 MemoryRetriever 接口
```rust
// retriever.rs
/// 记忆检索器 - 从 OpenViking 检索相关记忆
pub struct MemoryRetriever {
viking: Arc<VikingAdapter>,
}
impl MemoryRetriever {
pub fn new(viking: Arc<VikingAdapter>) -> Self {
Self { viking }
}
/// 检索与当前输入相关的记忆
pub async fn retrieve(
&self,
agent_id: &AgentId,
query: &str,
config: &RetrievalConfig,
) -> Result<RetrievalResult> {
// 1. 检索偏好
let preferences = self.retrieve_by_type(
agent_id,
MemoryType::Preference,
query,
config.max_results,
).await?;
// 2. 检索知识
let knowledge = self.retrieve_by_type(
agent_id,
MemoryType::Knowledge,
query,
config.max_results,
).await?;
// 3. 检索经验
let experience = self.retrieve_by_type(
agent_id,
MemoryType::Experience,
query,
config.max_results / 2,
).await?;
// 4. 计算 Token 使用
let total_tokens = self.estimate_tokens(&preferences, &knowledge, &experience);
Ok(RetrievalResult {
preferences,
knowledge,
experience,
total_tokens,
})
}
/// 按类型检索
async fn retrieve_by_type(
&self,
agent_id: &AgentId,
memory_type: MemoryType,
query: &str,
limit: usize,
) -> Result<Vec<MemoryEntry>> {
let scope = format!("agent://{}/{}", agent_id, memory_type_to_scope(&memory_type));
let results = self.viking.find(query, FindOptions {
scope: Some(scope),
limit: Some(limit),
level: Some("L1"), // 使用摘要级别
}).await?;
// 转换为 MemoryEntry
// ...
}
fn estimate_tokens(
&self,
preferences: &[MemoryEntry],
knowledge: &[MemoryEntry],
experience: &[MemoryEntry],
) -> usize {
// 简单估算:约 4 字符 = 1 token
let total_chars: usize = preferences.iter()
.chain(knowledge.iter())
.chain(experience.iter())
.map(|m| m.content.len())
.sum();
total_chars / 4
}
}
fn memory_type_to_scope(ty: &MemoryType) -> &'static str {
match ty {
MemoryType::Preference => "preferences",
MemoryType::Knowledge => "knowledge",
MemoryType::Experience => "experience",
MemoryType::Session => "sessions",
}
}
```
### 3.5 PromptInjector 接口
```rust
// injector.rs
/// Prompt 注入器 - 将记忆动态注入 system_prompt
pub struct PromptInjector {
config: RetrievalConfig,
}
impl PromptInjector {
pub fn new(config: RetrievalConfig) -> Self {
Self { config }
}
/// 构建增强的 system_prompt
pub fn inject(
&self,
base_prompt: &str,
memories: &RetrievalResult,
) -> String {
let mut result = base_prompt.to_string();
// 注入偏好
if !memories.preferences.is_empty() {
let prefs_section = self.format_preferences(
&memories.preferences,
self.config.preference_budget,
);
result.push_str("\n\n## 用户偏好\n");
result.push_str(&prefs_section);
}
// 注入知识
if !memories.knowledge.is_empty() {
let knowledge_section = self.format_knowledge(
&memories.knowledge,
self.config.knowledge_budget,
);
result.push_str("\n\n## 相关知识\n");
result.push_str(&knowledge_section);
}
// 注入经验
if !memories.experience.is_empty() {
let exp_section = self.format_experience(
&memories.experience,
self.config.experience_budget,
);
result.push_str("\n\n## 经验参考\n");
result.push_str(&exp_section);
}
result
}
fn format_preferences(&self, entries: &[MemoryEntry], budget: usize) -> String {
let mut result = String::new();
let mut used = 0;
for entry in entries.iter().take(5) { // 最多 5 条偏好
let line = format!("- {}\n", entry.content);
let line_tokens = line.len() / 4;
if used + line_tokens > budget {
break;
}
result.push_str(&line);
used += line_tokens;
}
result
}
fn format_knowledge(&self, entries: &[MemoryEntry], budget: usize) -> String {
// 类似 format_preferences
// ...
}
fn format_experience(&self, entries: &[MemoryEntry], budget: usize) -> String {
// 类似 format_preferences
// ...
}
}
```
### 3.6 AgentLoop 集成
修改 `crates/zclaw-runtime/src/loop_runner.rs`
```rust
pub struct AgentLoop {
agent_id: AgentId,
driver: Arc<dyn LlmDriver>,
tools: ToolRegistry,
memory: Arc<MemoryStore>,
model: String,
system_prompt: Option<String>,
max_tokens: u32,
temperature: f32,
skill_executor: Option<Arc<dyn SkillExecutor>>,
// 新增:成长系统
memory_retriever: Option<Arc<MemoryRetriever>>,
memory_extractor: Option<Arc<MemoryExtractor>>,
prompt_injector: Option<PromptInjector>,
growth_enabled: bool,
}
impl AgentLoop {
pub async fn run(&self, session_id: SessionId, input: String) -> Result<AgentLoopResult> {
// 1. 检索相关记忆 (新增)
let memories = if self.growth_enabled {
if let Some(retriever) = &self.memory_retriever {
retriever.retrieve(
&self.agent_id,
&input,
&RetrievalConfig::default(),
).await.unwrap_or_default()
} else {
RetrievalResult::default()
}
} else {
RetrievalResult::default()
};
// 2. 构建增强的 system_prompt (修改)
let enhanced_prompt = if self.growth_enabled {
if let Some(injector) = &self.prompt_injector {
injector.inject(
self.system_prompt.as_deref().unwrap_or(""),
&memories,
)
} else {
self.system_prompt.clone().unwrap_or_default()
}
} else {
self.system_prompt.clone().unwrap_or_default()
};
// 3. 添加用户消息
let user_message = Message::user(input);
self.memory.append_message(&session_id, &user_message).await?;
// 4. 获取完整上下文
let mut messages = self.memory.get_messages(&session_id).await?;
// 5. 执行 LLM 循环 (使用增强的 prompt)
let mut iterations = 0;
let max_iterations = 10;
loop {
// ... 现有的 LLM 循环逻辑
// 使用 enhanced_prompt 作为 system message
}
// 6. 对话结束后提取记忆 (新增)
if self.growth_enabled {
if let Some(extractor) = &self.memory_extractor {
let final_messages = self.memory.get_messages(&session_id).await?;
let extracted = extractor.extract(
&final_messages,
&ExtractionConfig::default(),
).await?;
// 写入 OpenViking
for memory in extracted {
// 通过 VikingAdapter 写入
}
}
}
Ok(result)
}
}
```
---
## 四、前端变化
### 4.1 新增组件
```typescript
// desktop/src/components/GrowthPanel.tsx
interface GrowthPanelProps {
agentId: string;
}
export function GrowthPanel({ agentId }: GrowthPanelProps) {
// 功能:
// - 显示 Agent 成长指标
// - 手动触发学习
// - 查看/编辑记忆
// - 配置学习参数
}
```
### 4.2 Store 扩展
```typescript
// desktop/src/store/agentStore.ts
interface AgentState {
// ... 现有字段
// 新增:成长相关
growthEnabled: boolean;
memoryStats: {
totalMemories: number;
preferences: number;
knowledge: number;
experience: number;
lastLearningTime: string | null;
};
}
```
### 4.3 Tauri Commands
```rust
// desktop/src-tauri/src/growth_commands.rs
#[tauri::command]
async fn get_memory_stats(agent_id: String) -> Result<MemoryStats, String>;
#[tauri::command]
async fn trigger_learning(agent_id: String, session_id: String) -> Result<Vec<ExtractedMemory>, String>;
#[tauri::command]
async fn get_memories(agent_id: String, memory_type: Option<String>) -> Result<Vec<MemoryEntry>, String>;
#[tauri::command]
async fn delete_memory(agent_id: String, uri: String) -> Result<(), String>;
#[tauri::command]
async fn update_memory(agent_id: String, uri: String, content: String) -> Result<(), String>;
```
---
## 五、执行计划
### 5.1 Phase 1: Crate 骨架 (1-2 天)
- [ ] 创建 `crates/zclaw-growth/` 目录结构
- [ ] 定义 `types.rs` 核心类型
- [ ] 设置 `Cargo.toml` 依赖
### 5.2 Phase 2: 检索系统 (2-3 天)
- [ ] 实现 `VikingAdapter` 封装
- [ ] 实现 `MemoryRetriever`
- [ ] 单元测试
### 5.3 Phase 3: 注入 + 集成 (2-3 天)
- [ ] 实现 `PromptInjector`
- [ ] 修改 `AgentLoop` 集成点
- [ ] 集成测试
### 5.4 Phase 4: 提取系统 (3-4 天)
- [ ] 实现 `MemoryExtractor`
- [ ] 设计 LLM prompt 模板
- [ ] 测试提取质量
### 5.5 Phase 5: 前端 UI (2-3 天)
- [ ] 实现 `GrowthPanel` 组件
- [ ] 扩展 Agent Store
- [ ] 添加 Tauri Commands
### 5.6 Phase 6: 测试 + 优化 (2-3 天)
- [ ] 端到端测试
- [ ] 性能优化
- [ ] 文档完善
**总计**: 约 12-18 天
---
## 六、关键文件路径
### 核心类型
- `crates/zclaw-types/src/agent.rs` - AgentConfig
- `crates/zclaw-types/src/message.rs` - Message
- `crates/zclaw-types/src/id.rs` - AgentId, SessionId
### 存储层
- `crates/zclaw-memory/src/store.rs` - MemoryStore
- `crates/zclaw-memory/src/schema.rs` - SQLite Schema
### 运行时
- `crates/zclaw-runtime/src/loop_runner.rs` - AgentLoop
### OpenViking 集成
- `desktop/src/lib/viking-client.ts` - 前端客户端
- `desktop/src-tauri/src/viking_commands.rs` - Tauri 命令
- `docs/features/03-context-database/00-openviking-integration.md` - 文档
### 前端学习系统(将被后端化)
- `desktop/src/lib/active-learning.ts`
- `desktop/src/lib/memory-extractor.ts`
- `desktop/src/store/activeLearningStore.ts`
---
## 七、风险与缓解
| 风险 | 影响 | 缓解措施 |
|------|------|---------|
| OpenViking 不可用 | 高 | 实现 LocalStorageAdapter 降级方案 |
| 记忆提取质量低 | 中 | 可配置的置信度阈值 + 人工审核 |
| Token 预算超限 | 中 | 严格的 Token 控制和截断 |
| 前端学习数据丢失 | 高 | 提供迁移脚本导入旧数据 |
---
## 八、新会话执行指南
在新会话中执行此方案时,请:
1. **阅读本文档**`docs/superpowers/specs/2026-03-26-agent-growth-design.md`
2. **参考计划文件**`plans/crispy-spinning-reef.md`(包含更多分析细节)
3. **从 Phase 1 开始**:创建 zclaw-growth crate 骨架
4. **遵循设计**:严格按照本文档的接口定义实现
5. **保持沟通**:如有疑问,与用户确认后再修改设计