内容
# PlayGuard
一个位于 AI 代理和浏览器/设计工具之间的 MCP 代理。它封装了 [Playwright MCP](https://github.com/microsoft/playwright-mcp) 和可选的 Figma MCP,公开一个统一的 MCP 服务器,同时透明地添加:
- **自动会话恢复** — 死亡的浏览器会话被恢复;代理永远不会看到崩溃错误
- **令牌优化** — 紧凑的快照,增量差异,快照缓存和评估缓存减少了 70–90% 的浏览器快照成本
- **Figma 响应优化** — 在响应到达代理之前,剥离元数据,不可见的图层,重复的组件实例和 SVG 几何体块
- **截图策略** — 重定向,警告,阻止或允许截图调用
与 Claude Code,Claude Desktop,Cursor,Codex 或任何 MCP 兼容的代理一起使用。
## 架构
```
Claude Code / Claude Desktop / Cursor / Codex
│
│ MCP (stdio)
▼
┌─────────────────┐
│ PlayGuard │ ← 所有内容的单一代理
│ │
│ • 路由器 │ browser_* → Playwright MCP
│ • 恢复 │ figma_* → Figma MCP (可选)
│ • 优化器 │
│ • 缓存 │
│ • 分析 │
└────────┬────────┘
│
┌──────┴──────┐
▼ ▼
Playwright MCP Figma MCP
│
▼
Chromium / Firefox / WebKit
```
Figma MCP 是可选的。如果未设置 `FIGMA_MCP_CMD`,PlayGuard 将在仅浏览器的模式下运行,没有行为更改。Playwright MCP 作为依赖项捆绑在一起 - 不需要单独安装。
## 功能
### 浏览器 (Playwright)
#### 自动会话恢复
PlayGuard 在代理看到之前拦截死会话错误:
```
Target.*closed · Browser.*closed · connect ECONNREFUSED · crashed
```
它重新启动 Playwright MCP,恢复最后一个 URL,并透明地重试调用。崩溃期间的两次并发调用 → 一次重启,两个调用继续。
#### 紧凑的快照
`browser_snapshot` 通常返回完整的 accessibility 树。PlayGuard 仅保留具有 `[ref=]` (交互元素)和结构性地标(`nav`,`main`,`form`,`dialog` 等)的行。静态文本和装饰图像没有引用,不需要用于导航。
```
[PlayGuard 紧凑: 312/1840 行, ~83% 删除, 124.3KB→14.1KB]
```
设置 `PLAYGUARD_COMPACT=false` 以接收原始快照。
#### 增量快照
当页面仅轻微更改时,PlayGuard 返回差异而不是完整快照:
```
[PlayGuard 增量: +3 添加, 1 删除, ~91% 保存]
添加:
- 按钮 "提交" [ref=47]
删除:
- 按钮 "加载..." [ref=44]
```
#### 快照缓存 + 预取
在 `browser_navigate` 之后,PlayGuard 立即以背景方式获取快照。下一个 `browser_snapshot` 从缓存中立即提供服务。未更改的页面返回 `UNCHANGED` 而不访问浏览器。
#### 截图策略
| 模式 | 行为 |
|------|----------|
| `warn` (默认) | 截图执行;警告写入 stderr |
| `redirect` | 替换为 `browser_snapshot`。传递 `{visual:true}` 以获取真正的截图 |
| `block` | 阻止并显示错误;代理被告知要更改的环境变量 |
| `allow` | 无限制 |
#### 评估缓存 + 输出压缩
具有相同脚本的重复 `browser_evaluate` 调用在同一 URL 上从缓存中提供服务(可配置的 TTL)。大型评估输出被截断为字符限制,以避免超出上下文窗口。
### Figma 优化器
PlayGuard 拦截 Figma MCP 响应,并在将其交付给代理之前运行它们通过优化管道:
```
[PlayGuard figma: -68% (284.0KB→91.0KB)]
```
> **注意:** 优化器期望原始 Figma REST API JSON (`document.children`,`componentId`,`fillGeometry`)。如果您的 Figma MCP 上游返回预先简化的或 YAML 输出(例如 Framelink `figma-developer-mcp`),优化器是一个空操作 - `parseSkip` 被写入 stderr 和 NDJSON 日志。
**模块 1 — 元数据清理器:** 删除与布局无关的字段:`createdAt`,`updatedAt`,`creator`,`pluginData`,`sharedPluginData`,`exportSettings`,`reactions`,`interactions` 等。
**模块 2 — 不可见图层修剪器:** 递归删除 `visible === false` 或 `opacity === 0` 的节点。
**模块 3 — 组件重复:** 相同组件的重复实例被折叠为引用。100 个按钮 → 第一个完整定义 + 99 个仅具有覆盖的引用。
```json
{ "type": "INSTANCE", "name": "Button/Primary", "_ref": "123:4", "overrides": [] }
```
**模块 4 — SVG 引用:** 用 `{ "_svgRef": "nodeId" }` 替换内联 SVG 几何体(`fillGeometry`)。代理获取形状标识符而无需数千个路径坐标。
**模块 6 — 布局压缩器:** 删除 Auto Layout 容器内节点的绝对 `x`/`y` — 它们是多余的,因为位置由 `layoutMode`,`gap` 和 `padding` 确定。
## 要求
- Node.js 18+
- npm
## 安装
```bash
git clone https://github.com/ZenyaDAR/PlayGuard.git
cd playguard
npm install
npm run build
```
## 用法
### Claude Code
添加到 `~/.claude/claude_desktop_config.json` (或您的项目的 `.claude/settings.json`):
**仅浏览器:**
```json
{
"mcpServers": {
"playguard": {
"command": "node",
"args": ["/path/to/playguard/dist/index.js"],
"env": {
"PLAYGUARD_SCREENSHOTS": "redirect"
}
}
}
}
```
**浏览器 + Figma:**
```json
{
"mcpServers": {
"playguard": {
"command": "node",
"args": ["/path/to/playguard/dist/index.js"],
"env": {
"PLAYGUARD_SCREENSHOTS": "redirect",
"FIGMA_MCP_CMD": "npx @figma/mcp",
"FIGMA_API_KEY": "your-figma-api-key",
"FIGMA_CACHE_TTL": "60000"
}
}
}
}
```
不要单独添加 Playwright MCP — PlayGuard 自动从其捆绑的依赖项中启动它。
### Claude Desktop
相同的 JSON 格式。配置文件位置:
- **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
- **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
### Windows 路径
在 JSON 字符串中使用正斜杠。路径中的空格不需要转义:
```json
"args": ["C:/Users/YourName/projects/playguard/dist/index.js"]
```
## 环境变量
### Playwright
| 变量 | 默认值 | 描述 |
|----------|---------|-------------|
| `PLAYGUARD_SCREENSHOTS` | `warn` | `allow` / `warn` / `block` / `redirect` |
| `PLAYGUARD_COMPACT` | `true` | 设置 `false` 以禁用紧凑快照并接收原始 accessibility 树 |
| `PLAYGUARD_DELTA` | `true` | 设置 `false` 以禁用增量快照 |
| `PLAYGUARD_DELTA_THRESHOLD` | `0.4` | 必须更改的行的分数,以触发完整快照而不是增量(0–1) |
| `PLAYGUARD_TOKEN_BUDGET` | `0` | 每快照的最大令牌数;在行边界上截断,因此 `[ref=]` 标签永远不会被分割。`0` = 关闭 |
| `PLAYGUARD_HINT_THRESHOLD` | `4` | 在连续快照中没有操作,注入一个提示列表,可用的交互引用 |
| `PLAYGUARD_PREFETCH_SNAPSHOT` | `true` | 设置 `false` 以禁用 `browser_navigate` 后的后台快照预取 |
| `PLAYGUARD_EVAL_CACHE_TTL` | `500` | `browser_evaluate` 缓存 TTL,毫秒。`0` = 关闭 |
| `PLAYGUARD_EVAL_COMPACT` | `8000` | 评估输出的最大字符数。`0` = 关闭 |
| `PLAYWRIGHT_MCP_CMD` | 捆绑的二进制文件 | 覆盖 Playwright MCP 命令 |
| `PLAYWRIGHT_MCP_ARGS` | — | 传递给 Playwright MCP 的额外参数(空格分隔) |
### Figma
| 变量 | 默认值 | 描述 |
|----------|---------|-------------|
| `FIGMA_MCP_CMD` | — | Figma MCP 启动命令。未设置 = Figma 禁用 |
| `FIGMA_MCP_ARGS` | — | Figma MCP 的额外参数(空格分隔) |
| `FIGMA_CACHE_TTL` | `0` | Figma 响应缓存 TTL,毫秒。`0` = 关闭 |
| `FIGMA_SVG_REFS` | `true` | 设置 `false` 以保持 SVG 几何图形内联 |
| `FIGMA_API_KEY` | — | 转发到 Figma MCP 子进程 |
## 配置示例
**无头浏览器 + Figma with cache:**
```json
"env": {
"PLAYWRIGHT_MCP_ARGS": "--headless",
"FIGMA_MCP_CMD": "npx @figma/mcp",
"FIGMA_API_KEY": "your-figma-api-key",
"FIGMA_CACHE_TTL": "120000"
}
```
**Firefox + block 截图:**
```json
"env": {
"PLAYWRIGHT_MCP_ARGS": "--browser firefox",
"PLAYGUARD_SCREENSHOTS": "block"
}
```
**在会话之间保持 cookie:**
```json
"env": {
"PLAYWRIGHT_MCP_ARGS": "--storage-state /tmp/browser-state.json"
}
```
**重定向截图 + 硬令牌上限:**
```json
"env": {
"PLAYGUARD_SCREENSHOTS": "redirect",
"PLAYGUARD_TOKEN_BUDGET": "4000"
}
```
## 分析
所有工具调用都记录到 `logs/YYYY-MM-DD.ndjson`。运行报告:
```bash
npm run analyze
```
样本输出:
```
── 快照令牌节省 ────────────────────────────────
缓存命中: 12/47 快照(8 次来自预取)
缓存节省的字节: 84.3 KB (~21 075 个令牌)
紧凑节省的字节: 312.1 KB (~78 025 个令牌)
总节省: 396.4 KB (~99 100 个令牌)
与原始相比的减少: 83% (82.1 KB 发送 vs 478.5 KB 不使用 PlayGuard)
── PlayGuard 拦截 ───────────────────────────────
截图 → 快照:5 次重定向(2 次来自缓存,~18 400 个令牌节省)
评估缓存命中: 8/11 次评估(~240ms 节省,平均 30ms)
── Figma 优化器 ───────────────────────────────────────
调用: 24 次总计 (6 次缓存命中,18 次优化)
输入令牌: ~71 000 → 输出: ~22 750 (节省 ~48 250,-68%)
── 按工具划分的延迟 ───────────────────────────────────────
| 工具 | 次数 | 错误 | 错误率 | 拦截 | 平均毫秒 | p50 | p95 |
```
每个 NDJSON 行都包含所有字段以进行下游分析:
- **Playwright:** `rawBytes`,`keptBytes`,`savedBytes`,`delta`,`cacheHit`,`prefetchHit`,`snapCount`,`scriptHash`
- **Figma:** `fileKey`,`nodeId`,`inBytes`,`outBytes`,`savedTokens`,`metaKeysDeleted`,`invisiblePruned`,`svgRefsReplaced`,`instancesCollapsed`,`layoutCoordsRemoved`
## 基准测试
```bash
npm run bench
```
衡量代理开销,快照与截图大小,缓存命中率和崩溃恢复时间。没有 LLM 参与 - 仅原始数字。
## 测试
```bash
npm test
```
涵盖 `collapseRuns`,`compactSnap` (包括令牌预算边界)和完整的 Figma 优化器管道(`optimizeFigmaResponse`)。
## 比较
| 场景 | 没有 PlayGuard | 有 PlayGuard |
|----------|-------------------|----------------|
| 浏览器崩溃 | 错误;会话中断 | 自动恢复;代理从未看到它 |
| 崩溃期间的并发调用 | 两个失败或两个浏览器 | 一个重启;两个调用继续 |
| `browser_snapshot` (完整树) | 100–500 KB | 10–80 KB 压缩后 |
| 重复 `browser_snapshot` 相同页面 | 每次重新获取 | 缓存命中或 `UNCHANGED` |
| 具有 50+ 组件的 Figma 文件 | 200–500 KB JSON | 优化后 60–150 KB |
| 代理配置中的两个 MCP | 两个独立的服务器 | 一个 PlayGuard |
| 代理开销 | 无 | ~1–3 毫秒每次 Playwright 调用 |
## 项目结构
```
playguard/
├── src/
│ └── index.ts 所有服务器逻辑(~660 行)
├── dist/ 编译输出(由 npm run build 生成)
├── bench/
│ ├── run.mjs 基准测试:延迟,令牌节省,崩溃恢复
│ └── analyze.mjs NDJSON 日志的分析报告
├── test/
│ └── playguard.test.mjs 测试(Node 内置测试运行器)
├── logs/ 每天的 NDJSON 调用日志(在运行时自动创建)
├── package.json
└── tsconfig.json
```
## 许可证
MIT