Files
qiming/qimingclaw/crates/agent-electron-client/docs/v2/09-GUI-AGENT-IMPLEMENTATION.md

778 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# QimingClaw GUI Agent 实现方案
> 调研时间2026-03-18
> 调研项目OSWorld, UI-TARS-desktop, ScreenAgent, Pi-Agent
> 目标:独立且小巧的 GUI Agent 方案
---
## 一、调研结果对比
### 1.1 项目特性对比
| 项目 | 类型 | 核心特点 | 复杂度 | 适用性 |
|------|------|---------|--------|--------|
| **OSWorld** | 评测基准 | 完整 GUI 操作空间,跨平台 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| **UI-TARS-desktop** | 生产 Agent | 多模态 + 视觉定位 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| **ScreenAgent** | 模型训练 | VLM 微调,屏幕理解 | ⭐⭐⭐ | ⭐⭐ |
| **Pi-Agent** | Agent 框架 | 轻量级,事件驱动 | ⭐⭐ | ⭐⭐⭐⭐⭐ |
| **Agent-S** | 现有方案 | 已集成 | ⭐⭐⭐⭐ | ⭐⭐⭐ |
### 1.2 GUI 操作原语对比
| 操作类型 | OSWorld | UI-TARS | QimingClaw 建议实现 |
|---------|---------|---------|-----------------|
| **鼠标移动** | MOVE_TO | - | ✅ `move_to(x, y)` |
| **点击** | CLICK | `click(point)` | ✅ `click(x, y, button?)` |
| **双击** | DOUBLE_CLICK | `left_double(point)` | ✅ `double_click(x, y)` |
| **右键** | RIGHT_CLICK | `right_single(point)` | ✅ `right_click(x, y)` |
| **拖拽** | DRAG_TO | `drag(start, end)` | ✅ `drag(x1, y1, x2, y2)` |
| **滚动** | SCROLL(dx, dy) | `scroll(point, direction)` | ✅ `scroll(dx, dy)` |
| **输入文本** | TYPING | `type(content)` | ✅ `type_text(text, speed?)` |
| **按键** | PRESS, KEY_DOWN, KEY_UP | - | ✅ `press(key)` |
| **快捷键** | HOTKEY | `hotkey(key)` | ✅ `hotkey(...keys)` |
| **截图** | - | screenshot() | ✅ `screenshot(region?)` |
| **等待** | WAIT | `wait()` | ✅ `wait(ms?)` |
---
## 二、QimingClaw GUI Agent 架构设计
### 2.1 整体架构
```
┌─────────────────────────────────────────────────────┐
│ QimingClaw Main Agent (ACP Engine) │
│ │
│ ┌───────────────────────────────────────────────┐ │
│ │ GUI Agent (独立微服务/MCP Server) │ │
│ │ │ │
│ │ ┌─────────────┐ ┌──────────────────┐ │ │
│ │ │ GUI Tools │──────▶│ Desktop Operator │ │ │
│ │ │ │ │ │ │ │
│ │ │ - screenshot│ │ - pyautogui │ │ │
│ │ │ - click │ │ - robotjs │ │ │
│ │ │ - type │ │ - nut.js │ │ │
│ │ │ - scroll │ │ │ │ │
│ │ └─────────────┘ └──────────────────┘ │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ Pi-Agent Core (轻量运行时) │ │ │
│ │ │ │ │ │
│ │ │ - AgentState (状态管理) │ │ │
│ │ │ - EventStream (事件流) │ │ │
│ │ │ - Hook System (权限控制) │ │ │
│ │ │ - ToolExecutor (工具执行) │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌──────────────────────┐ │ │
│ │ │ VLM (视觉语言模型) │ │ │
│ │ │ │ │ │
│ │ │ - GPT-4V │ │ │
│ │ │ - Claude Vision │ │ │
│ │ │ - Gemini Vision │ │ │
│ │ │ - 自定义 VLM │ │ │
│ │ └──────────────────────┘ │ │
│ └───────────────────────────────────────────────┘ │
│ ▲ │
│ │ MCP / IPC │
│ │ │
│ ┌──────────┴──────────┐ │
│ │ Main Agent Bridge │ │
│ └─────────────────────┘ │
└─────────────────────────────────────────────────────┘
```
### 2.2 核心模块
#### 2.2.1 GUI Tools (工具层)
**参考OSWorld ACTION_SPACE + UI-TARS BrowserGUIAgent**
```typescript
import { Type } from '@sinclair/typebox';
import type { AgentTool, AgentToolResult } from '@qimingclaw/agent';
export const guiTools: AgentTool[] = [
{
name: 'screenshot',
label: '📸 截取屏幕',
description: '截取屏幕或指定区域,返回 base64 图片',
parameters: Type.Object({
region: Type.Optional(Type.Object({
x: Type.Number({ minimum: 0 }),
y: Type.Number({ minimum: 0 }),
width: Type.Number({ minimum: 1 }),
height: Type.Number({ minimum: 1 }),
})),
format: Type.Optional(Type.Union([
Type.Literal('png'),
Type.Literal('jpeg'),
Type.Literal('webp'),
])),
quality: Type.Optional(Type.Number({ minimum: 1, maximum: 100 })),
}),
execute: async (callId, params, signal, onUpdate) => {
onUpdate({ status: 'capturing', progress: 0 });
const screenshot = await desktopCapturer.capture(params.region);
// 压缩图片(借鉴 UI-TARS
if (params.format === 'webp' || params.quality) {
const compressed = await imageCompressor.compress(screenshot.buffer, {
format: params.format || 'webp',
quality: params.quality || 80,
});
screenshot.buffer = compressed;
}
onUpdate({ status: 'capturing', progress: 100 });
return {
content: [{
type: 'image',
data: screenshot.base64,
mimeType: `image/${params.format || 'png'}`,
}],
details: {
width: screenshot.width,
height: screenshot.height,
size: screenshot.buffer.length,
timestamp: Date.now(),
},
};
},
},
{
name: 'click',
label: '👆 点击',
description: '点击屏幕指定位置',
parameters: Type.Object({
x: Type.Number({ minimum: 0 }),
y: Type.Number({ minimum: 0 }),
button: Type.Optional(Type.Union([
Type.Literal('left'),
Type.Literal('right'),
Type.Literal('middle'),
])),
numClicks: Type.Optional(Type.Number({ minimum: 1, maximum: 3 })),
}),
execute: async (callId, params, signal) => {
// 权限检查(通过 Hook 系统)
// 移动鼠标
await robotjs.moveMouse(params.x, params.y);
// 点击
await robotjs.mouseClick(params.button || 'left', params.numClicks || 1);
return {
content: [{ type: 'text', text: `已点击 (${params.x}, ${params.y})` }],
details: params,
};
},
},
{
name: 'type_text',
label: '⌨️ 输入文本',
description: '输入文本(支持流式进度)',
parameters: Type.Object({
text: Type.String({ minLength: 1 }),
typingSpeed: Type.Optional(Type.Number({ minimum: 0, maximum: 1000 })),
}),
execute: async (callId, params, signal, onUpdate) => {
const chars = params.text.length;
const speed = params.typingSpeed || 50; // ms per char
for (let i = 0; i < chars; i++) {
// 检查取消信号
if (signal?.aborted) {
throw new Error('Typing aborted by user');
}
// 输入字符
await robotjs.typeString(params.text[i]);
// 流式进度更新
if (i % 10 === 0 || i === chars - 1) {
onUpdate({
progress: (i / chars) * 100,
charsTyped: i + 1,
totalChars: chars,
});
}
// 延迟
if (speed > 0) {
await sleep(speed);
}
}
return {
content: [{ type: 'text', text: `已输入 ${chars} 个字符` }],
details: { length: chars, speed },
};
},
},
{
name: 'scroll',
label: '📜 滚动',
description: '滚动鼠标滚轮',
parameters: Type.Object({
dx: Type.Optional(Type.Number()),
dy: Type.Number(),
}),
execute: async (callId, params, signal) => {
await robotjs.scrollMouse(params.dx || 0, params.dy);
return {
content: [{ type: 'text', text: `已滚动 (${params.dx || 0}, ${params.dy})` }],
details: params,
};
},
},
{
name: 'hotkey',
label: '⌨️ 快捷键',
description: '按下组合键',
parameters: Type.Object({
keys: Type.Array(Type.String()),
}),
execute: async (callId, params, signal) => {
await robotjs.keyTap(params.keys.join('+'));
return {
content: [{ type: 'text', text: `已按下 ${params.keys.join(' + ')}` }],
details: { keys: params.keys },
};
},
},
{
name: 'wait',
label: '⏳ 等待',
description: '等待指定时间或条件',
parameters: Type.Object({
ms: Type.Optional(Type.Number({ minimum: 0 })),
condition: Type.Optional(Type.String()),
}),
execute: async (callId, params, signal, onUpdate) => {
if (params.ms) {
await sleep(params.ms);
} else if (params.condition) {
// 等待条件满足(轮询检查)
const startTime = Date.now();
while (Date.now() - startTime < 30000) { // 最多 30 秒
if (signal?.aborted) {
throw new Error('Wait aborted');
}
// 检查条件(例如:窗口出现、元素可见等)
const satisfied = await checkCondition(params.condition);
if (satisfied) {
break;
}
onUpdate({ elapsed: Date.now() - startTime });
await sleep(500);
}
}
return {
content: [{ type: 'text', text: '等待完成' }],
details: params,
};
},
},
];
```
#### 2.2.2 Pi-Agent Core (运行时)
**借鉴Pi-Agent 事件系统 + Hook 机制**
```typescript
import { Agent, type AgentEvent } from '@qimingclaw/pi-agent';
export class QimingClawGUIAgent {
private agent: Agent;
constructor(config: {
vlmModel: ModelConfig;
tools?: AgentTool[];
permissions?: PermissionConfig;
}) {
this.agent = new Agent({
initialState: {
systemPrompt: `你是 QimingClaw 的 GUI 操作专家。
支持的操作:
- screenshot: 截取屏幕
- click: 点击
- type_text: 输入文本
- scroll: 滚动
- hotkey: 快捷键
- wait: 等待
每次操作前:
1. 先截图观察当前状态
2. 分析需要操作的元素位置
3. 执行操作
4. 再次截图验证结果
注意:
- 操作前需要用户确认(可通过配置跳过)
- 支持撤销操作
- 失败时自动重试(最多 3 次)`,
model: config.vlmModel,
tools: config.tools || guiTools,
},
});
// 设置权限控制 Hook
this.agent.setBeforeToolCall(async (context, signal) => {
// 检查是否需要用户确认
if (this.needsConfirmation(context.toolCall.name)) {
const confirmed = await this.showConfirmationDialog(context);
if (!confirmed) {
return {
block: true,
reason: '用户拒绝操作',
};
}
}
// 记录审计日志
this.logAction(context);
return undefined; // 继续执行
});
// 设置结果处理 Hook
this.agent.setAfterToolCall(async (context, signal) => {
// 截图后自动添加标记
if (context.toolName === 'screenshot' && !context.isError) {
// 在图片上标记可点击元素(借鉴 UI-TARS
const markedImage = await this.markClickableElements(context.result);
return {
content: [{ type: 'image', ...markedImage }],
};
}
// 失败时自动重试
if (context.isError && this.shouldRetry(context)) {
return this.retryToolCall(context);
}
return undefined;
});
}
async execute(instruction: string): Promise<void> {
return this.agent.prompt(instruction);
}
subscribe(listener: (event: AgentEvent) => void): () => void {
return this.agent.subscribe(listener);
}
abort(): void {
this.agent.abort();
}
}
```
#### 2.2.3 MCP Server 集成
**将 GUI Agent 打包为 MCP Server**
```typescript
// mcp-servers/gui-agent/index.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { QimingClawGUIAgent } from '@qimingclaw/gui-agent';
const server = new Server({
name: 'qimingclaw-gui-agent',
version: '1.0.0',
}, {
capabilities: {
tools: {},
},
});
const guiAgent = new QimingClawGUIAgent({
vlmModel: {
provider: 'anthropic',
id: 'claude-3-5-sonnet-20241022',
},
});
// 注册工具
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: guiTools.map(tool => ({
name: tool.name,
description: tool.description,
inputSchema: tool.parameters,
})),
};
});
// 执行工具
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
const tool = guiTools.find(t => t.name === name);
if (!tool) {
throw new Error(`Tool ${name} not found`);
}
const result = await tool.execute(
generateCallId(),
args,
undefined,
(update) => {
// 发送进度更新(通过 MCP 通知)
server.notification({
method: 'notifications/progress',
params: update,
});
},
);
return {
content: result.content.map(c => {
if (c.type === 'text') {
return { type: 'text', text: c.text };
} else if (c.type === 'image') {
return {
type: 'image',
data: c.data,
mimeType: c.mimeType,
};
}
}),
};
});
// 启动服务器
const transport = new StdioServerTransport();
server.connect(transport);
```
#### 2.2.4 Main Agent 集成
**在 QimingClaw 主 Agent 中调用**
```typescript
// qimingclaw-agent/src/core/gui-bridge.ts
import type { AgentTool } from '@qimingclaw/agent';
export const guiBridgeTools: AgentTool[] = [
{
name: 'gui_execute',
label: '🖥️ 执行 GUI 操作',
description: '委托 GUI Agent 执行屏幕操作',
parameters: Type.Object({
instruction: Type.String(),
timeout: Type.Optional(Type.Number()),
}),
execute: async (callId, params, signal, onUpdate) => {
onUpdate({ status: 'delegating', instruction: params.instruction });
// 通过 MCP 调用 GUI Agent
const mcpClient = getMCPClient('qimingclaw-gui-agent');
const result = await mcpClient.callTool('execute', {
instruction: params.instruction,
timeout: params.timeout || 60000,
});
return {
content: [{ type: 'text', text: result.summary }],
details: {
actions: result.actions,
screenshots: result.screenshots,
duration: result.duration,
},
};
},
},
{
name: 'gui_screenshot',
label: '📸 获取屏幕截图',
description: '从 GUI Agent 获取当前屏幕截图',
parameters: Type.Object({
region: Type.Optional(Type.Object({
x: Type.Number(),
y: Type.Number(),
width: Type.Number(),
height: Type.Number(),
})),
}),
execute: async (callId, params, signal) => {
const mcpClient = getMCPClient('qimingclaw-gui-agent');
const result = await mcpClient.callTool('screenshot', params);
return {
content: [result.image],
details: result.details,
};
},
},
];
```
---
## 三、实施路线图
### Phase 1: 核心实现1-2 周)
**目标MVP 可用**
- [ ] **GUI Tools 实现**
- [ ] screenshot截图 + 压缩)
- [ ] click点击 + 坐标验证)
- [ ] type_text输入 + 流式进度)
- [ ] scroll滚动
- [ ] hotkey快捷键
- [ ] wait等待
- [ ] **Desktop Operator 实现**
- [ ] 跨平台鼠标/键盘控制robotjs / nut.js
- [ ] 屏幕捕获desktopCapturer
- [ ] 图片压缩ImageCompressor
- [ ] **Pi-Agent Core 集成**
- [ ] Agent 类封装
- [ ] 事件系统4 级生命周期)
- [ ] Hook 系统(权限 + 审计)
### Phase 2: MCP 集成1 周)
**目标:独立微服务化**
- [ ] **MCP Server 打包**
- [ ] 工具注册
- [ ] 进度通知
- [ ] 错误处理
- [ ] **配置管理**
- [ ] VLM 模型配置
- [ ] 权限策略配置
- [ ] 性能参数配置
- [ ] **测试**
- [ ] 单元测试
- [ ] 集成测试
- [ ] E2E 测试OSWorld benchmark
### Phase 3: Main Agent 集成1 周)
**目标:与 QimingClaw 主 Agent 协同**
- [ ] **Bridge Tools**
- [ ] gui_execute
- [ ] gui_screenshot
- [ ] **权限控制 UI**
- [ ] 确认对话框
- [ ] 审计日志查看
- [ ] 撤销操作
- [ ] **配置界面**
- [ ] VLM 模型选择
- [ ] 权限级别设置
- [ ] 性能调优
### Phase 4: 优化与增强(持续)
**目标:生产级稳定性**
- [ ] **性能优化**
- [ ] 图片压缩优化
- [ ] 操作延迟优化
- [ ] 内存管理
- [ ] **错误恢复**
- [ ] 自动重试
- [ ] 操作回滚
- [ ] 状态恢复
- [ ] **高级功能**
- [ ] 元素识别OCR + 视觉定位)
- [ ] 录制回放
- [ ] 脚本生成
---
## 四、关键技术细节
### 4.1 跨平台实现
```typescript
// 选择合适的底层库
const desktopOperator = {
// 方案 A: robotjs (Node.js 原生)
mouse: {
move: (x, y) => robotjs.moveMouse(x, y),
click: (button) => robotjs.mouseClick(button),
scroll: (dx, dy) => robotjs.scrollMouse(dx, dy),
},
keyboard: {
type: (text) => robotjs.typeString(text),
tap: (key) => robotjs.keyTap(key),
hotkey: (...keys) => robotjs.keyTap(keys.join('+')),
},
screen: {
capture: (region) => desktopCapturer.capture(region),
},
// 方案 B: nut.js (更现代,支持 Promise)
// ...
};
```
### 4.2 图片压缩(借鉴 UI-TARS
```typescript
import sharp from 'sharp';
export class ImageCompressor {
async compress(
buffer: Buffer,
options: { format: 'webp' | 'jpeg'; quality: number },
): Promise<Buffer> {
return sharp(buffer)
.toFormat(options.format, { quality: options.quality })
.toBuffer();
}
}
// 使用
const compressed = await compressor.compress(screenshot, {
format: 'webp',
quality: 80,
});
// 通常可减少 70-90% 体积
```
### 4.3 坐标归一化
```typescript
// 借鉴 UI-TARS 的坐标处理
export function normalizeCoordinates(
coords: { x: number; y: number },
screenWidth: number,
screenHeight: number,
): { normalized: { x: number; y: number }; absolute: { x: number; y: number } } {
return {
normalized: {
x: coords.x / 1000, // 归一化到 [0, 1]
y: coords.y / 1000,
},
absolute: {
x: (coords.x / 1000) * screenWidth,
y: (coords.y / 1000) * screenHeight,
},
};
}
```
---
## 五、测试与验证
### 5.1 OSWorld Benchmark 测试
```bash
# 运行 OSWorld 评测
cd OSWorld
python run.py --agent qimingclaw --max_steps 50 --test_set mini
```
### 5.2 单元测试
```typescript
describe('QimingClawGUIAgent', () => {
it('should take screenshot', async () => {
const agent = new QimingClawGUIAgent(config);
const result = await agent.execute('截取屏幕');
expect(result.content[0].type).toBe('image');
expect(result.details.width).toBeGreaterThan(0);
});
it('should click with confirmation', async () => {
const agent = new QimingClawGUIAgent(config);
// Mock confirmation dialog
mockConfirmation(true);
const result = await agent.execute('点击 (100, 100)');
expect(result.content[0].text).toContain('已点击');
});
});
```
---
## 六、总结
### 6.1 核心优势
1. **独立且小巧**:基于 Pi-Agent 轻量运行时
2. **借鉴业界最佳实践**OSWorld 操作空间 + UI-TARS 视觉定位
3. **灵活集成**:支持 MCP / IPC / 直接调用
4. **完善的安全机制**Hook 权限控制 + 审计日志
5. **生产级可靠性**:错误恢复 + 自动重试
### 6.2 与现有方案对比
| 特性 | Agent-S (现有) | QimingClaw GUI Agent (新) |
|------|----------------|------------------------|
| **架构** | 集成式 | 独立微服务 |
| **模型** | 共享 | 独立 VLM 配置 |
| **权限** | 简单 | 完善的 Hook 系统 |
| **事件** | 基础 | 4 级生命周期 |
| **测试** | 手动 | OSWorld benchmark |
| **复用性** | 低 | 高MCP Server |
### 6.3 关键决策
**推荐采用Pi-Agent + OSWorld 操作空间 + UI-TARS 视觉定位**
**理由:**
1. Pi-Agent 最轻量,事件系统完善
2. OSWorld 操作空间最标准化
3. UI-TARS 图片压缩和坐标处理最成熟
4. 可独立演进,不影响主 Agent
---
**下一步行动:**
1. ✅ 在 `worktree/docs/gpui-research` 分支创建 PoC
2. ✅ 实现核心 GUI Toolsscreenshot/click/type
3. ✅ 集成 Pi-Agent 运行时
4. ✅ 打包为 MCP Server
5. ✅ 与主 Agent 集成测试
---
**参考资源:**
- OSWorld: https://github.com/xlang-ai/OSWorld
- UI-TARS-desktop: https://github.com/bytedance/UI-TARS-desktop
- Pi-Agent: `/Users/apple/workspace/pi-mono/packages/agent`
- Pi-Agent 深度研究: `/Users/apple/workspace/pi-mono/PI-AGENT-DEEP-RESEARCH.md`