Files
qiming/qimingclaw/specs/gui-agent/gui-agent-task.md

457 lines
14 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.
# GUI Agent MCP Server — Task 任务文档
> **文档类型**: Task任务文档— 描述"具体执行",拆解后的最小行动单元
>
> **基于**: `specs/gui-agent/gui-agent-plan.md` Plan 计划文档
>
> **模块路径**: `crates/agent-gui-server/`
---
## Context
构建 GUI Agent MCP Server — 一个桌面自动化服务,文本 Agentclaude-code、qimingcode通过 MCP 协议调用它,执行截图识别 + 键鼠模拟。项目基于 pi-mono Agent 循环引擎,采用 Streamable HTTP 主模式 + stdio 备选Mutex 互斥锁保证同时只有一个 GUI 任务执行。
---
## 约定
- **ID 格式**: `T{phase}.{seq}`(如 T1.1
- **`[P]`** = 可与同 Wave 内其他任务并行
- **depends** = 必须等待前置任务完成
- 所有文件路径相对于 `crates/agent-gui-server/`
- 参考项目约定: `crates/qiming-mcp-stdio-proxy/`ESM、Node≥22、tsc+esbuild、Vitest
---
## Phase 1: 项目脚手架 + 原子操作
> **目标**: MCP Server 启动Streamable HTTP13 个原子操作可调用
### Wave 1 — 项目初始化
#### T1.1 创建项目脚手架
**创建文件**:
- `package.json``"type": "module"`, Node≥22, `"bin": { "agent-gui-server": "dist/index.js" }`, `"main": "./dist/lib.js"`
- `tsconfig.json` — target ES2022, module/moduleResolution Node16, strict, declaration
- `vitest.config.ts` — globals, node env, 15s timeout, v8 coverage
- `scripts/build.mjs` — esbuild 单文件 bundle + shebang
**依赖**:
```
dependencies:
@modelcontextprotocol/sdk ^1.27.1
@nut-tree/nut-js ^4.2.0
sharp ^0.33.0
clipboardy ^4.0.0
devDependencies:
typescript ^5.7.0
vitest ^2.1.8
@vitest/coverage-v8 ^2.1.9
esbuild ^0.27.3
@types/node ^22.0.0
```
**参考**: `crates/qiming-mcp-stdio-proxy/package.json`, `tsconfig.json`, `vitest.config.ts`, `scripts/build.mjs`
**验收**: `pnpm install` 成功解析新 workspace 成员
---
### Wave 2 — 基础设施层(全部 [P] 并行)
#### T1.2 `src/utils/errors.ts` [P]
5 个结构化错误类,继承 Errorreadonly context 字段:
- `ConfigError(message)`
- `DesktopError(operation, cause)`
- `CoordinateError(coordinateMode, rawX, rawY)`
- `SafetyError(keys, reason)`
- `TaskExecutionError(taskText, step, cause)`
**参考**: `crates/qiming-mcp-stdio-proxy/src/errors.ts`
#### T1.3 `src/utils/logger.ts` [P]
- stderr 输出,格式: `[YYYY-MM-DD HH:mm:ss.SSS] [LEVEL] [gui-agent] message`
- `GUI_AGENT_LOG_FILE` 环境变量启用文件日志,日轮转,保留 7 天
- 导出: `logInfo`, `logWarn`, `logError`, `logDebug`
**参考**: `crates/qiming-mcp-stdio-proxy/src/logger.ts`
#### T1.4 `src/utils/platform.ts` [P]
- `getPlatform(): 'macos' | 'windows' | 'linux'`
- `checkScreenRecordingPermission(): Promise<boolean>`
- `checkAccessibilityPermission(): Promise<boolean>`
- `getPlatformPasteKeys(): Key[]`macOS: Cmd+V, 其他: Ctrl+V
---
### Wave 3 — 配置 + 坐标 + 桌面操作(大部分 [P] 并行)
#### T1.5 `src/config.ts`
**depends**: T1.2, T1.3
- `GuiAgentConfig` 接口,解析所有 `GUI_AGENT_*` 环境变量
- Fail Fast: `API_KEY` 缺失直接 throw `ConfigError`
- 数值范围校验: jpegQuality(1-100), maxSteps(1-200), port(1-65535), stepDelayMs(100-30000)
- 默认值: provider=`anthropic`, model=`claude-sonnet-4-20250514`, port=60008, maxSteps=50, stepDelayMs=1500, stuckThreshold=3, jpegQuality=75, displayIndex=0, transport=`http`
#### T1.6 `src/coordinates/modelProfiles.ts` [P]
**depends**: T1.2
- 7 条正则匹配规则 → `ModelProfile { coordinateMode, coordinateOrder }`
- Gemini → `{ normalized-999, yx }`,其余 → `xy`
- fallback: `image-absolute` + `xy`
- `getModelProfile(modelName, overrideMode?): ModelProfile`
#### T1.7 `src/coordinates/resolver.ts`
**depends**: T1.6
纯函数,零 I/O四步转换
1. 坐标顺序修正yx swap for Gemini
2. 归一化到 0~1
3. 逻辑坐标 = norm × logicalWidth/Height
4. 全局偏移 = local + display.origin
边界 clamp + warning 日志
#### T1.8 `src/desktop/display.ts` [P]
**depends**: T1.2, T1.3
- `DisplayDescriptor { index, label, width, height, scaleFactor, isPrimary, origin }`
- `listDisplays()`, `getDisplay(index)`, `getPrimaryDisplay()`
#### T1.9 `src/desktop/screenshot.ts`
**depends**: T1.8
截图管线:
1. nut.js `screen.capture(region)` 物理分辨率
2. sharp resize 到逻辑分辨率(吸收 scaleFactor
3. 最长边 > 1920 则等比缩放到 1920
4. JPEG encode quality=75
5. 超限降 quality 重试
6. 返回 `ScreenshotResult`base64 + imageWidth/Height + logicalWidth/Height + physicalWidth/Height + scaleFactor
#### T1.10 `src/desktop/mouse.ts` [P]
**depends**: T1.2
click, doubleClick, moveTo, drag, scroll, getPosition — 包装 nut.js
#### T1.11 `src/desktop/keyboard.ts` [P]
**depends**: T1.2, T1.4
- `typeText(text)` — 非 ASCII 或 >50 字符 → clipboard.pasteText否则 nut.js type
- `pressKey(key)`, `hotkey(keys[])`
#### T1.12 `src/desktop/clipboard.ts` [P]
**depends**: T1.2, T1.4
- `pasteText(text)` — 备份剪贴板 → 写入 → Cmd/Ctrl+V → 100ms → 恢复try-catch
- `readClipboard()`, `writeClipboard(text)` — 使用 clipboardy
#### T1.13 `src/desktop/imageSearch.ts` [P]
**depends**: T1.2
- `findImage(templateBase64, confidence?)``{ found, region?, confidence? }`
- `waitForImage(templateBase64, timeout?, confidence?)``{ found, region?, elapsed }`
---
### Wave 4 — 安全层([P] 并行)
#### T1.14 `src/safety/hotkeys.ts` [P]
**depends**: T1.2, T1.4
- `validateHotkey(keys[]): { blocked, reason? }`
- 平台黑名单: macOS(Cmd+Q, Cmd+W, Cmd+Opt+Esc), Windows(Alt+F4, Ctrl+Alt+Del), Linux(Ctrl+Alt+Del)
#### T1.15 `src/safety/auditLog.ts` [P]
**depends**: T1.2
- `AuditLog` class — 环形缓冲 1000 条
- `record(entry)`, `getEntries(count?)`, `clear()`
---
### Wave 5 — MCP 协议层
#### T1.16 `src/mcp/atomicTools.ts`
**depends**: T1.7, T1.9, T1.10, T1.11, T1.12, T1.13, T1.14, T1.15
注册 13 个 MCP tool handler:
`gui_screenshot`, `gui_click`, `gui_double_click`, `gui_move_mouse`, `gui_drag`, `gui_scroll`, `gui_type`, `gui_press_key`, `gui_hotkey`, `gui_cursor_position`, `gui_list_displays`, `gui_find_image`, `gui_wait_for_image`
每个 handler: 输入校验 → safety 检查 → 坐标解析 → desktop 操作 → audit 记录 → 返回结果
#### T1.17 `src/mcp/resources.ts` [P with T1.16]
**depends**: T1.4, T1.15
3 个 MCP Resource: `gui://status`, `gui://permissions`, `gui://audit-log`
#### T1.18 `src/mcp/server.ts`
**depends**: T1.16, T1.17
双模式 MCP Server:
- **HTTP 主模式**: `http.createServer()` 监听 `127.0.0.1:<port>``/mcp` 路径per-session `StreamableHTTPServerTransport` + `Server`session 管理 + 定期清理
- **stdio 备选**: 单 `Server` + `StdioServerTransport`
**参考**: `crates/qiming-mcp-stdio-proxy/src/bridge.ts`PersistentMcpBridge 模式)
#### T1.19 `src/index.ts`
**depends**: T1.5, T1.18
CLI 入口: `--port <number>`, `--transport <http|stdio>`, SIGINT/SIGTERM graceful shutdown
---
### Wave 6 — Phase 1 验收
#### T1.20 冒烟测试
**depends**: T1.19
- `tools/list` 返回 13 个工具
- `gui_screenshot` 返回 base64 + 正确 metadata
- `gui_list_displays` 返回显示器数组
- HTTP 模式支持多客户端并发连接
---
## Phase 2: 坐标转换验证 + 多屏
> **目标**: 坐标转换准确,多屏正确
#### T2.1 `tests/coordinates/resolver.test.ts` [P]
覆盖: 4 种坐标模式 × Gemini yx swap × Retina/HiDPI × 多屏偏移 × 边界 clamp × Spec 3.4.6 示例
#### T2.2 `tests/coordinates/modelProfiles.test.ts` [P]
覆盖: 7 条模型匹配 + fallback + coordinateOrder + override
#### T2.3 `tests/desktop/screenshot.test.ts` [P]
mock nut.js + sharp覆盖: Retina 2880×1800 → 1440×900、4K → 1920×1080、1080p 不缩放、quality 降级重试
#### T2.4 多屏端到端测试
**depends**: T2.1
mock 多显示器,验证主屏/副屏坐标偏移正确
---
## Phase 3: Agent 循环 (gui_execute_task)
> **目标**: gui_execute_task 能执行自然语言 GUI 任务
#### T3.1 添加 pi-mono 依赖
新增 dependencies: `@mariozechner/pi-ai`, `@mariozechner/pi-agent-core`, `@sinclair/typebox`
#### T3.2 `src/agent/systemPrompt.ts` [P]
**depends**: T3.1
`buildSystemPrompt(taskText, memoryText): string` — 角色描述、7 个内部工具说明、坐标指引、popup 处理、computer_done 使用指导、memory 注入点
#### T3.3 `src/agent/stuckDetector.ts` [P]
`StuckDetector` class — 截图缩放 32×32 → 像素均值差异 < 5% 连续 N 次 → 判定卡死
#### T3.4 `src/agent/memoryManager.ts`
**depends**: T3.1
三层记忆管理(参考 TuriX-CUA:
```
summaryMemory (budget 2000 chars) ← LLM 摘要压缩
recentMemory (budget 500 chars) ← finalizeStep 写入
pendingMemory (no budget) ← addPendingStep 写入
```
核心 API:
- `addPendingStep(stepId, goal)`
- `async finalizeStep(stepId, evaluation)` — pending→recent超 budget 触发 `complete(memoryModel, context)` 摘要
- `compose(): string` — 组合三层文本
- `pruneScreenshots(messages): AgentMessage[]` — 保留最近 3 步截图,老截图替换为文字占位
记忆摘要通过独立 `complete()` 调用(不走 Agent 循环)
#### T3.5 `src/agent/taskRunner.ts`
**depends**: T3.2, T3.3, T3.4, desktop/*, safety/*
**核心**: 配置 pi-mono `Agent` 实例,不手写循环
```typescript
const agent = new Agent({
initialState: { systemPrompt, model, thinkingLevel: 'off', tools: guiTools, messages: [] },
toolExecution: 'sequential',
transformContext: (msgs) => memoryManager.pruneScreenshots(msgs),
convertToLlm: (msgs) => msgs.filter(m => ['user','assistant','toolResult'].includes(m.role)),
beforeToolCall: ({ toolCall, args }) => hotkeys ,
afterToolCall: ({ toolCall, args, result, isError }) => auditLog ,
});
```
7 个 `AgentTool[]`: computer_screenshot, computer_click, computer_type, computer_scroll, computer_hotkey, computer_wait, computer_done
事件订阅: `turn_end` → stepCount++, finalizeStep, stuckDetector, 更新 systemPrompt(compose()), maxSteps 检查
返回 `TaskResult { success, result?, finalScreenshot?, steps[], error? }`
#### T3.6 `src/mcp/taskTools.ts`
**depends**: T3.5
- `gui_execute_task` MCP tool handler
- Mutex 互斥锁(`let currentTask: Promise | null`
- handler: acquire mutex → taskRunner.run(taskText, extra.signal, progressCb) → release in finally
- 进度: `extra.sendNotification({ method: 'notifications/progress', params: { progressToken, progress, total } })`
- 取消: `extra.signal``agent.abort()`
#### T3.7 更新 `src/mcp/server.ts`
**depends**: T3.6
在 session Server 注册中添加 `registerTaskTools()`
**验收**: `tools/list` 返回 14 个工具,`gui_execute_task("打开 Finder")` 自动完成并返回步骤日志
#### T3.8 Agent 单元测试 [P with T3.7]
- `tests/agent/taskRunner.test.ts` — mock Agent/getModel/desktop测试正常完成、abort、maxSteps
- `tests/agent/memoryManager.test.ts` — budget 触发、摘要调用、compose 输出、pruneScreenshots
- `tests/agent/stuckDetector.test.ts` — 相同/不同截图判定
---
## Phase 4: SDK 导出
#### T4.1 `src/lib.ts`
**depends**: T3.7
- `createGuiAgentMcpServer(config: Partial<GuiAgentConfig>): { start(), stop() }`
- Re-export: `GuiAgentConfig`, `ScreenshotResult`, `DisplayDescriptor`, `TaskResult`
---
## Phase 5: Electron 客户端集成
#### T5.1 类型定义
修改 `crates/agent-electron-client/src/shared/types/computerTypes.ts` — 新增 `GuiVisionModelConfig`
#### T5.2 computerServer 路由
**depends**: T5.1
修改 `crates/agent-electron-client/src/main/services/computerServer.ts` — 4 个新路由:
- `POST/GET /computer/gui-agent/vision-model`
- `GET /computer/gui-agent/displays`
- `POST /computer/gui-agent/display`
#### T5.3 GUIAgentSettings 组件 [P with T5.2]
新建 `crates/agent-electron-client/src/renderer/components/GUIAgentSettings.tsx` — 显示器选择 + 视觉模型配置
#### T5.4 集成 SettingsPage
**depends**: T5.3
修改 `crates/agent-electron-client/src/renderer/components/SettingsPage.tsx` — 添加 GUIAgentSettings 标签页
#### T5.5 workspace 集成
修改 `crates/agent-electron-client/package.json``"agent-gui-server": "workspace:*"` + extraResources
---
## Phase 6: 集成测试 + 文档
#### T6.1 MCP 集成测试
claude-code 通过 MCP URL 连接list tools调用 gui_screenshot
#### T6.2 跨平台验证
macOS + Windows + Linux(X11) 基本功能验证
#### T6.3 README.md
安装、MCP 配置示例HTTP + stdio、环境变量说明、开发指南
---
## 执行顺序总览
```
Phase 1 (脚手架 + 原子操作):
Wave 1: T1.1
Wave 2: T1.2 | T1.3 | T1.4
Wave 3: T1.5, T1.6 | T1.8 | T1.10 | T1.11 | T1.12 | T1.13
Wave 4: T1.7, T1.9, T1.14 | T1.15
Wave 5: T1.16 | T1.17
Wave 6: T1.18
Wave 7: T1.19
Wave 8: T1.20
Phase 2 (坐标验证):
T2.1 | T2.2 | T2.3 → T2.4
Phase 3 (Agent 循环):
T3.1 → T3.2 | T3.3 | T3.4 → T3.5 → T3.6 → T3.7
T3.8 (parallel)
Phase 4: T4.1
Phase 5: T5.1 → T5.2 | T5.3 → T5.4, T5.5
Phase 6: T6.1 → T6.2 → T6.3
```
---
## 验证方式
1. **Phase 1 验收**: 启动 server → `curl POST /mcp` 调用 `tools/list` 返回 13 工具 → `gui_screenshot` 返回 base64
2. **Phase 2 验收**: `pnpm test` 全部通过,坐标覆盖率 >90%
3. **Phase 3 验收**: `gui_execute_task("打开 Finder")` → 自动截图分析+点击 → 返回 steps + finalScreenshot
4. **Phase 4 验收**: `import { createGuiAgentMcpServer } from 'agent-gui-server'` 可用
5. **Phase 5 验收**: Electron Settings UI 可配置视觉模型和显示器
6. **Phase 6 验收**: claude-code MCP 配置后能调用 gui_execute_task
---
## 关键参考文件
| 文件 | 用途 |
|------|------|
| `specs/gui-agent/gui-agent.md` | Spec 规范文档(权威需求源) |
| `specs/gui-agent/gui-agent-plan.md` | Plan 计划文档(技术方案源) |
| `crates/qiming-mcp-stdio-proxy/package.json` | 工程约定模板 |
| `crates/qiming-mcp-stdio-proxy/scripts/build.mjs` | esbuild 构建脚本模板 |
| `crates/qiming-mcp-stdio-proxy/src/bridge.ts` | Streamable HTTP + session 管理模式 |
| `crates/qiming-mcp-stdio-proxy/src/logger.ts` | 日志模块模板 |
| `crates/qiming-mcp-stdio-proxy/src/errors.ts` | 错误类模板 |