cursor-Playwright自动化框架相关的skills

我爱海鲸 2026-07-20 11:09:12 暂无标签

简介nodejs、node、Playwright、pw

可以把技能放到本机用户目录的 ~/.cursor/skills/<技能目录>/(Windows 一般是 C:\Users\你的用户名\.cursor\skills\)。这样任意项目打开后,只要 Cursor 会加载用户级 skills,就不必每个仓库复制一份。


注意:~/.cursor/skills-cursor/ 是 Cursor 内置技能目录,不要把自己的技能放进去;自定义技能用 项目 .cursor/skills/ 或 用户 ~/.cursor/skills/。

在项目的根目录下创建:.cursor\skills\playwright-h5-api-automation

SKILL.md

---
name: playwright-h5-api-automation
description: Scaffolds Playwright + Express services that automate H5 landing pages (send-code, submit-order) via browser replay and downstream API capture. Use when building H5 order automation, Playwright API proxy, browser pool, channel/touchpoint param injection, or Docker headless browser services for similar requirements.
---

# Playwright H5 自动化 API 服务

将「打开 H5 → 填表 → 点击 → 捕获下游接口」封装为 HTTP API。适用于落地页链接固定、触点参数由调用方传入、需服务端无头运行的场景。

## 何时使用

- 需求是:**发码/办理** 必须走真实 H5 + 页面 SDK,不能直接调下游(或需兜底)
- 对外提供 REST API,入参为手机号、验证码、会话 ID 等
- 落地页 URL **不能** 挂业务参数,触点需 API 注入
- 需要 Docker 部署、文件日志、单实例串行

## 架构(四层)

```
Express API (server/index.js)
    ↓
Automation core (scripts/*-automation.js)  — 发码/办理流程
    ↓
Browser pool (scripts/browser-pool.js)     — 单例浏览器 + 串行队列 + 会话
    ↓
Playwright Chromium                          — 打开 H5、点击、拦截 XHR
```

| 能力 | 实现要点 |
|------|----------|
| 发码 | 打开页 → 填手机号 → 点发送 → 捕获 `POST .../sms/send`(路径按项目替换) |
| 办理 | 复用发码会话页 → 填验证码 → 点办理 → 捕获 `POST .../order/submit` |
| 兜底 | 无会话时用页面内 SDK + `fetch` 直调办理接口 |
| 会话 | 发码成功后内存保存 `{ phone, pageUrl, sid, channelTemplate }`,TTL 建议 10 分钟 |
| 并发 | `runExclusive` 串行队列;单实例有效并发 **1**,扩容靠多容器 |

## 推荐目录结构

```
{project}/
├── server/
│   ├── index.js          # Express 路由、入参解析、日志块
│   └── logger.js         # 控制台 + 挂载目录双写
├── scripts/
│   ├── browser-pool.js   # launch、页面缓存、会话、warmup、shutdown
│   ├── *-automation.js   # 发码/办理、route 注入、兜底 fetch
│   ├── channel-params.js # 触点字段白名单与别名(与 H5 主题配置对齐)
│   └── *-cli.js          # 本地调试 CLI(可选)
├── docker/dockerfile     # 官方 Playwright 基础镜像
├── doc/README.md         # 部署与 API 说明
├── package.json
└── playwright.config.js
```

## API 设计规范

### 必备接口

| 方法 | 路径 | 作用 |
|------|------|------|
| GET | `/health` | 存活探针 |
| GET/POST | `/api/sms/send` | 发码 |
| GET/POST | `/api/order/submit` | 办理(需 sid + verifyCode) |

### 入参分层(重要)

| 类型 | 字段示例 | 是否拼落地页 URL | 是否注入下游 POST |
|------|----------|------------------|-------------------|
| 基础 | `phone`, `productCode`, `userAgent`, `headless` | 否 | 否 |
| 页面追踪 | `cpparam` 等 | **是** | 否(仅 H5 侧) |
| 触点 | `channelParams` / `touchpoint` | **否** | **是** |

解析逻辑:

1. 保留字段不进 URL、不进触点
2. 触点字段名与 H5 项目 `productThemes`(或等价配置)白名单一致
3. 支持短别名(如 `rmn` → `reportMaterialsNo`),与 H5 侧别名表同步
4. 落地页 URL 仅保留 `productCode` + 页面追踪类 query

### 对外返回

- 发码:返回**下游原始 JSON**,并在顶层追加 `sid`(优先取唯一流水号,勿用固定占位码)
- 办理:返回**下游办理接口原始 JSON**
- 内部日志:`API 请求/响应`、`下游请求/响应` 分块 JSON 打印

## 触点注入(链接不能带参时)

**禁止**把触点拼进落地页 URL(易触发风控或与 `proxyOrderUrl` 不一致)。

使用 Playwright **路由拦截**,在下游 POST 发出前合并 `channelParams`:

```javascript
// 1. 点击之前注册 route
const routeHook = await installChannelPostOverride(page, SMS_PATTERN, channelParams);

try {
  const responsePromise = page.waitForResponse(matchSmsPost);
  await page.locator(SEND_BTN).click();
  const response = await responsePromise;
  const body = routeHook.getMergedBody() || parseRequestBody(response.request().postData());
  // ...
} finally {
  await routeHook.teardown(); // 必须在收到 response 之后
}
```

### 拦截实现要点

| 规则 | 说明 |
|------|------|
| 时机 | **点击前注册,响应后卸载**;页面常在 SDK 校验后才发 POST,点击后立即 unroute 会失效 |
| 方式 | 优先 `route.fetch({ postData })` + `route.fulfill({ response })` |
| 匹配 | 路由用 glob `**/.../sms/send`,响应用 RegExp 均可 |
| 日志 | 记录 `getMergedBody()`,勿仅信 `request.postData()`(可能是注入前快照) |

## Browser Pool 规范

```javascript
// 串行队列 — Playwright 单页非线程安全
function runExclusive(task) {
  const run = queue.then(task, task);
  queue = run.catch(() => {});
  return run;
}
```

- 单例 `browser` + 缓存 `page`;URL 或 UA 变化时重建
- `pageUsed` 标记:办理/失败后 reload 重置表单
- 启动 `warmup(defaultPageUrl)` 缩短首请求耗时
- `SIGINT`/`SIGTERM` 时 `shutdown()` 关浏览器

### 会话限制(务必写进 doc)

- 全局仅 **1 份** 发码会话;多用户交错时后者覆盖前者
- 办理尽量发码后立刻调用;跨实例需粘滞或接受 SDK 兜底

## Docker 清单

```dockerfile
FROM mcr.microsoft.com/playwright:v{VERSION}-jammy
WORKDIR /app

COPY package.json package-lock.json ./
RUN NPM_CONFIG_PRODUCTION=false npm ci && node -e "require('playwright')"
ENV NODE_ENV=production

COPY server ./server
COPY scripts ./scripts
ENV HEADLESS=true
ENV LOG_PATH=/opt/docker/{service}/logs
CMD ["node", "server/index.js"]
```

| 坑 | 处理 |
|----|------|
| `Cannot find module 'playwright'` | `playwright` 放 **dependencies**;`npm ci` 在 `NODE_ENV=production` **之前** |
| 构建上下文 | `docker build -f docker/dockerfile -t {tag} .` 在项目根目录,非 docker 子目录 |
| 日志挂载 | uid 1000(pwuser);多实例各用独立 logs 目录 |

## 文件日志

- `logger.js`:production 双写;路径 `/opt/docker/{service}/logs/{service}.log`
- 按日/100MB 轮转,对齐 Java logback 习惯
- 环境变量:`LOG_PATH`、`LOG_TO_FILE`

## 实施顺序

1. 手工在浏览器走通 H5,记录 Network 下发码/办理 **Request Payload** 字段
2. 用 Playwright CLI 脚本复现点击与捕获(无 API)
3. 抽 browser-pool + automation core
4. 加 Express API + 结构化日志
5. 加 `channelParams` 注入(若链接不能带参)
6. Docker + doc/README
7. 对比浏览器与 API 日志字段逐项 diff

## 常见故障

| 现象 | 排查 |
|------|------|
| 触点参数未生效 | route 是否过早 unroute;日志是否用 merged body |
| 下游 999 风控 | 对比成功/失败请求的触点字段;勿随意改 `pageName` 等未报备字段 |
| 办理 sid 失效 | 会话被覆盖、超时、或实例漂移 |
| 首请求慢 | warmup;`waitUntil: 'load'` 而非 `networkidle` |
| Docker 模块缺失 | dependencies + NPM_CONFIG_PRODUCTION=false |

## 脱敏要求

编写 skill、doc、注释时:

- 用 `{BASE_URL}`、`{PRODUCT_CODE}`、`{DOWNSTREAM_SMS_PATH}` 占位
- 不写真实域名、产品编码、素材编号、运营商名称
- 触点字段名可保留(通常为对接文档标准字段)

## 延伸阅读

- 模块职责与代码片段:[reference.md](reference.md)

reference.md

# Playwright H5 API 自动化 — 参考实现

本仓库可参考 `front-pj/*-order-pj/` 下已落地项目(实施时替换为你的 H5 与下游路径,勿复制业务常量)。

## 模块职责

| 文件 | 职责 |
|------|------|
| `server/index.js` | Express、`parseSendParams` / `parseOrderParams`、`logBlock`、graceful shutdown |
| `server/logger.js` | 双写、轮转、`LOG_PATH` |
| `scripts/browser-pool.js` | `runExclusive`、`preparePage`、`saveSession`、`warmup` |
| `scripts/*-automation.js` | `sendViaPage`、`submitViaPage`、`installChannelPostOverride`、`submitViaFetch` |
| `scripts/channel-params.js` | `collectTouchpointParams`、白名单、别名 |

## channel-params 白名单模板

与 H5 前端 `productThemes`(或等价)保持同步:

```javascript
const CHANNEL_PARAM_KEYS = new Set([
  'equityProductCode',
  'accessChannel',
  'packageName',
  'packageRoute',
  'sChannelNumber',
  'firstChannel',
  'secondChannel',
  'reportMaterialsNo',
  'fixedPositionCode',
  'fixedPositionPath',
  'pageName',
  'pageRoute',
  'clientIp',
  'proxyOrderUrl',
  'userAgent',
]);

const CHANNEL_PARAM_ALIASES = {
  rmn: 'reportMaterialsNo',
  fpc: 'fixedPositionCode',
  fpp: 'fixedPositionPath',
  pgn: 'pageName',
  // ... 与 H5 侧一致
};
```

## installChannelPostOverride 模板

```javascript
async function installChannelPostOverride(page, urlPattern, channelParams) {
  const overrides =
    channelParams && Object.keys(channelParams).length > 0 ? channelParams : null;
  if (!overrides) {
    return { teardown: async () => {}, getMergedBody: () => null };
  }

  let mergedBody = null;
  const routeGlob = '**/your-downstream-path/sms/send'; // 按项目替换

  const handler = async (route) => {
    const request = route.request();
    if (request.method() !== 'POST') {
      await route.continue();
      return;
    }
    let body = {};
    try {
      body = request.postDataJSON() || {};
    } catch {
      await route.continue();
      return;
    }
    mergedBody = { ...body, ...overrides };
    const response = await route.fetch({ postData: JSON.stringify(mergedBody) });
    await route.fulfill({ response });
  };

  await page.route(routeGlob, handler);
  return {
    teardown: async () => page.unroute(routeGlob, handler),
    getMergedBody: () => mergedBody,
  };
}
```

## API 入参解析模板

```javascript
function parseSendParams(source) {
  const reserved = new Set([
    'phone', 'productCode', 'userAgent', 'headless',
    'extraParams', 'channelParams', 'touchpoint',
  ]);
  const { hncnParams: channelParams, urlParams: extraParams } =
    collectTouchpointParams(source, reserved);

  return {
    phone: source.phone,
    productCode: source.productCode || DEFAULT_PRODUCT_CODE,
    channelParams,
    extraParams,
    pageUrl: buildPageUrl({ productCode, extraParams }),
    // ...
  };
}
```

## POST 调用示例(脱敏)

```bash
curl -X POST "http://localhost:{PORT}/api/sms/send" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "1XXXXXXXXXX",
    "productCode": "PROD_XXX",
    "channelParams": {
      "reportMaterialsNo": "{APPROVED_MATERIAL_ID}",
      "fixedPositionCode": "{POSITION_CODE}"
    }
  }'
```

```bash
curl -X POST "http://localhost:{PORT}/api/order/submit" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "1XXXXXXXXXX",
    "sid": "{SEND_REQ_NO}",
    "verifyCode": "123456",
    "productCode": "PROD_XXX",
    "channelParams": {
      "reportMaterialsNo": "{APPROVED_MATERIAL_ID}"
    }
  }'
```

## sid 提取优先级

```javascript
function extractSid(body) {
  const data = body?.data;
  if (!data) return null;
  return data.sendReqNo ?? data.querySmsNumber ?? data.sid ?? data.verifyNo ?? null;
}
```

勿把固定占位 verifyNo 当作 sid。

## 容量规划

| 指标 | 单实例参考 |
|------|------------|
| Playwright 并发 | 1 |
| 预热后发码 | ~2s/次,约 30 次/分钟 |
| 会话 TTL | 10 分钟 |
| 内存 | 建议 ≥ 2G |

高 QPS:多实例 + 负载均衡,每实例独立浏览器与会话。

 

你好:我的2025

上一篇:Playwright自动化框架

下一篇:最好的告别