# 高危平台注意事项

高危平台（淘宝/京东/生意参谋/银行/小红书等强风控站点）的探查/采集约束。

## 核心约束：CDP 端口会被风控识别

CDP 路由通过 `--remote-debugging-port` 暴露调试端口，**强风控脚本会检测这个端口**
（以及 webdriver 标记）。在淘宝/京东/银行这类站点走 CDP 路由，大概率被判定为自动化，
触发验证码、限流、甚至封号。

## 路由选择

`browser.open({ routeMode })` 的取值(**显式二选一,无 auto,必填**):

| routeMode | 行为 | 适用 |
|---|---|---|
| `extension` | 强制走扩展路(无 CDP 端口,无 webdriver 标记) | **高危平台必选** |
| `cdp` | 强制走 CDP(暴露调试端口) | 静态页/调试/非风控站点 |

**高危平台必须走 extension 路由**:`open({ routeMode: "extension" })`。
若扩展没连上,extension 路会**明确失败退出**(不降级到 CDP)——此时停下来报告用户,
按错误信息检查 prepare/load/port。绝不偷偷 fallback 到 cdp 凑合(高危平台 cdp 会触发风控)。

## WebPlater 扩展安装（每个实例 profile 一次性）

extension 路由依赖 WebPlater 扩展。**扩展装在具体实例的 Chrome profile 里**——
每个实例有独立 profile(`config/browser.json` 里的 `profileDir`),换实例就要重装。

多实例下,**每个实例生成自己的扩展目录**(写死该实例的 WS 端口),扩展不共享:

> JS/npm 环境使用下方的 `npx mooncat-browser`；Python/pip 环境去掉 `npx`，
> 直接执行 `mooncat-browser`。两者调用同一套实例、插件和 browserd 服务。

```bash
# 1. 为实例生成 unpacked 扩展目录(copy 包内模板 + 替换 WS 端口占位符)
npx mooncat-browser prepare-extension <实例名>
#    产物在 <实例 extensionDir>(默认 .browser/<实例名>/extension)
#    里面 offscreen 代码已写死连 ws://127.0.0.1:<该实例 extensionPort>

# 2. 启动实例(会弹出用该实例 profile 的 Chrome)
npx mooncat-browser start <实例名> --route-mode extension

# 3. 在弹出的 Chrome 里手动 load unpacked:
#    chrome://extensions → 开「开发者模式」→「加载已解压的扩展程序」
#    选择:<实例 extensionDir>(校验路径下有 manifest.json)
#    profile 会永久记住这次安装,以后 start 不用再 load
```

首次给新 profile 安装插件时，可以先用 CDP 启动可见 Chrome，手动加载插件后停止，再切换
extension 路；这是 JS/TS 和 Python 两种客户端共同的流程：

```bash
mooncat-browser prepare-extension <实例名>
mooncat-browser start <实例名> --route-mode cdp
# chrome://extensions → 开发者模式 → 加载命令打印的实例插件目录
mooncat-browser stop <实例名>
mooncat-browser start <实例名> --route-mode extension
```

`prepare-extension` 会打印实例 extensionDir 的绝对路径——**把这个路径告诉用户**,
不要让用户自己找。校验路径下有 `manifest.json` 即正确。

> 注意:extension 路不能 headless(Chrome 限制)。高危平台本就要看得见浏览器,不冲突。
> 改了实例 extensionPort 或升级了包(扩展 build 变了),要重 `prepare-extension` 并在
> Chrome 里重新 load unpacked。

## 装了扩展后必须重启浏览器

扩展装上后，`open()` 返回的 `mode` 可能仍是 `cdp`（因为 open 时 routeMode 已锁定）。
**必须 `browser.close()` 再重新 `open()`**，mode 才会切到 extension。不能中途静默切换。

## 探查动作的拟人化

⚠️ **重要**：BrowserClient（薄 RPC 到 browserd）**不暴露 humanize/riskLevel 选项**。
裸 `operate` 的 click/fill 是机器特征（瞬点、无轨迹），高危平台即使走 extension 路由，
风控仍可能通过交互节奏识别。

高危平台探查时：

- **优先只读操作**（status/snapshot/innerText/screenshot）——只读不触发风控交互检测。
- **交互操作（click/fill）谨慎用**：必须交互时，操作间加 `waitForTimeout` 随机延迟，
  降低节奏特征。
- **触发验证码立即停下**：检测到验证码（页面出现验证码元素/被重定向到验证页），
  **STOP，报告用户**，不要尝试自动过验证码。

### probe 长会话分阶段（高危平台是安全红线，不只是效率）

探查天然分阶段——开一次页面 + 导航到位后，**复用同一个 tab**继续探下一个问题，不重开。
**严禁每个小问题都重新 newTab + 导航**。

**为什么在高危平台这是红线，不只是效率问题**:
- 每开一次 newTab + 导航 + 等 SPA = 一次完整页面加载请求。反复重开 = 高频异常访问。
- 生意参谋/天猫/京东等强风控站点，**重开页面越多，风控触发越快**——推向验证码/限流/封号。
- 探查阶段本就高频操作，再叠加反复重开，等于主动触雷。

→ **同一个页面一旦打开，在会话内反复复用，用多次 evaluate 探不同问题，绝不为每个小问题
  重新 newTab+导航。** 探查动作只读优先（status/snapshot/screenshot），减少交互请求；
  必须交互时控制频率。

复用方式: `browser.listTabs()` 按 url 匹配找到已开的 tab，拿它的 `pageHandle` 继续操作。

## 验证码检测

操作后用只读 snapshot/innerText 检测是否被验证码拦截。**注意:iframe 内的拦截要从对应 frameId 读**
（淘宝/阿里统一登录在 havanalogin iframe，顶层 document 读不到）。

```ts
// 顶层页面文本
const topText = await browser.operate({
  pageHandle: tab.pageHandle, action: "innerText",
  params: { selector: "body" },
});
// 登录 iframe 内文本 (跨域, 必须用 frameId eval)
const iframeText = await browser.operate({
  pageHandle: tab.pageHandle, action: "evaluate",
  params: { frameId: loginFrameId, source: "() => document.body ? document.body.innerText : ''" },
});
const combined = String(topText.value) + String(iframeText?.value || iframeText || "");
if (/验证码|滑动验证|请完成验证|向右滑动|captcha|拖动|安全验证/i.test(combined)) {
  // STOP: 截图 + 报告用户被验证码拦截, 不自动过
}
```

命中后**绝不**继续操作，绝不尝试自动识别/绕过验证码。停下来让用户手动过。

## 不可自动化的拦截类型（检测到即报告用户，绝不对抗）

高危平台登录链路有一类拦截是**根本无法自动化**的（自动过 = 对抗风控 = 封号风险）。
这些不是 bug，是平台风控设计，唯一正确动作是**检测 → 报告用户 → 暂停等人工**。

| 拦截类型 | 识别特征（页面文本） | 能否自动 |
|---|---|---|
| **滑块验证** | `向右滑动验证` / `拖动滑块` / `滑动验证` | ❌ 不能。轨迹是风控核心，机器滑动必被识破 |
| **手机/短信验证码** | `短信验证码` / `获取验证码` / `手机验证` | ❌ 不能。要人手机收码 |
| **图形验证码** | `输入图中字符` / `看不清` / 图形验证 | ❌ 不能。OCR 过码违反平台规则 |
| **人脸/实名** | `人脸识别` / `实名认证` | ❌ 不能。生物特征，只能人 |
| **风险提醒** | `检测到风险` / `异常登录` / `环境异常` | ❌ 不能。需人工按提示操作 |

**统一处理范式**（无论哪种拦截，用截图 + 报告代替通知框架）:

```ts
// 检测到拦截 (滑块/验证码/风险提醒任一)
if (blocked) {
  // 1. 截图取证 (extension 模式先 activate, 否则 screenshot 报错)
  await browser.operate({ pageHandle: tab.pageHandle, action: "activate" }).catch(() => {});
  const shot = await browser.operate({ pageHandle: tab.pageHandle, action: "screenshot" });
  let imgPath = null;
  if (shot?.dataUrl) {
    const { writeFileSync } = await import("node:fs");
    imgPath = "./trace/login-blocked.png";
    writeFileSync(imgPath, Buffer.from(shot.dataUrl.split(",")[1], "base64"));
  }

  // 2. 报告用户叫人来处理 (用你应用层的通知机制; mooncat-browser 不带通知能力)
  //    内容: 拦截类型 + 截图路径 + "请打开浏览器完成验证, 会话将在登录态恢复后继续"
  reportToUser({ type: "登录拦截", 拦截类型, screenshot: imgPath });

  // 3. STOP — 不重试, 不对抗, 等人。会话在此挂起, 下次重新检测登录态
  return;
}
```

> 上例的 `reportToUser` 是占位——你的应用层用什么通知（飞书/Slack/webhook/console 打印 +
> 等待）就用什么。`mooncat-browser` 是独立工具包，**不带任何通知能力**，本 skill 只规定
> "必须可见化 + 叫人"的纪律，不规定通知后端。

**关键心态**：这些拦截"不能搞定"是**正常预期**，不是失败。职责是**第一时间发现并叫人**，
不是去破解。把"无法自动过"当问题去解，就是在对抗风控，违反铁律。

### 登录态复用（避免每次都撞拦截）

既然登录拦截无法自动，正确架构是 **不碰登录填表，只做登录态检测**:
- 启动 → `browser.open()` + 访问目标页 → 读 url/snapshot 判断是否已登录
- 已登录（cookie 在 profile 里）→ 直接进业务步骤
- 未登录/被拦 → 报告用户叫人，会话挂起
- 人工登录一次后（过滑块/验证码），cookie 留在 profile，后续会话复用，不再撞拦截

这条"登录态复用"是高危平台的标准模式。

## 出错纪律（高危平台更严）

1. **STOP**，不要重试轰炸。
2. 截图记录当前状态（`screenshot` 到产物）。
3. 报告用户：错误 + 截图 + 判断（验证码？登录失效？被限流？）。
4. 等用户决定下一步。

**绝不**：强杀 Chrome（`taskkill`）、清 cookie 重来、自动切换 profile——这些在高危平台都会
放大风险（清 cookie 可能触发异地登录风控，切 profile 丢登录态）。
