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
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:
@@ -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 | 初始创建 |
|
||||
|
||||
|
||||
@@ -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 # 课堂生成
|
||||
- [ ] 创建教育类 Hands(whiteboard、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. 保持用户配置不丢失
|
||||
|
||||
@@ -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 有完整实现 |
|
||||
|
||||
@@ -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) - 配置文件位置、格式和最佳实践
|
||||
|
||||
Reference in New Issue
Block a user