# netabrowser-cli S1 基础设施层 设计文档

> Neta 子项目 netabrowser-cli 第一个迭代：浏览器后端 daemon service + CLI 包装 + skill 元数据。
> 不含任何业务 service（业务 service 留给 S2/S3）。

| 元数据 | 值 |
|---|---|
| 子项目 | netabrowser-cli S1（基础设施层） |
| 创建日期 | 2026-05-04 |
| 估期 | ~10-12 天 |
| 状态 | 设计完成，待 user 复核 → writing-plans |
| 关联 | geo S1 联调暂停（Task 22）；netabrowser-cli 完成后 geo PlainChromiumProvider 改调 BrowserDaemonService |

---

## 1. 目标

为 Neta 平台提供**反风控 + 拟人化**的浏览器自动化基础设施，统一服务以下三类调用方：

1. **NetaClaw Agent**：通过 `netabrowser-cli` skill（命令行调用，AI token 友好）
2. **后端业务 service**（如未来的 `XiaohongshuLoginService`）：通过 `@Inject BrowserDaemonService` 直接调
3. **未来的电商自动化模块**：同 #2

S1 完成后，AI Agent 可以用 cli 命令探索任意网页（小红书/淘宝/抖音/拼多多），后续 S2/S3 把固化的 SOP 封装为业务 service。

## 2. 关键决策（brainstorming 阶段已确认）

| 决策点 | 决策 |
|---|---|
| skill 范围 | **三个 skill 并存**：playwright-cli + patchwright-cli + netabrowser-cli。前两个保留不动 |
| 拟人化粒度 | **完整拟人化**：贝塞尔轨迹 + 滚动 + 视觉停顿 + 字符间随机间隔 + 偶尔错字回退 |
| 拟人化触发 | **默认拟人化**：一个命令 = 人类行为；提供 `--no-humanize` 用于测试 |
| daemon 进程 | **嵌入 backend**（Midway @Provide @Singleton），单进程；崩溃隔离的代价不值 |
| 实施风格 | **Service-First**：BrowserDaemonService 为核心；CLI 是 thin client 调 backend HTTP；后端业务直接 @Inject service |
| chromium 打包 | **内嵌完整打包**（Inno Setup [Files]），安装包 ~400MB |
| 拟人化实现 | **ghost-cursor 开源库 + 自加套**：用 ghost-cursor 的贝塞尔轨迹，自己加随机延迟/滚动/停顿/错字模拟 |
| 平台补丁 | S1 **只做通用 CLI**，小红书/抖音/淘宝特定补丁 → S2 |
| S1 范围 | **仅基础设施**（daemon service + cli + skill），无业务 service |
| 数据库 | local + prod 都用 `neta_test`（用户已确认） |

## 3. 已有架构契合度

Neta 在 2026-04-25 后已有完整 windows-runtime 体系：

| 已有机制 | netabrowser-cli S1 复用方式 |
|---|---|
| `pkg/yao-pkg` 打包 backend.exe | netabrowser-cli 单独 pkg 打包成 `netabrowser-cli.exe` |
| Inno Setup 安装器 | 增加 `[Files]` 段把 `chromium/win64/*` 拷到安装目录 |
| .NET 托盘 | 不动 |
| `comm/config-loader.ts` | 不改；netabrowser-cli 通过 backend HTTP 间接读 config |
| `comm/data-dir.ts` | 不改；daemon 内拼子路径 `<dataDir>/browser-profiles/`、`<dataDir>/states/` |
| `comm/runtime-info.ts` | 不改；netabrowser-cli 读 runtime-info.json 拿端口 + secret |
| `/app/base/runtime/status` `/stop` | 不动；新增 `/admin/browser-daemon/*` 平行路径 |
| bundled skills 自动入安装包 | netabrowser-cli skill 沿用 |

**意思**：S1 完全在现有 windows-runtime 框架内增量构建，不重写打包/托盘/安装器。

---

## 4. 架构与目录

### 4.1 三层职责

```
┌──────────────────────────────────────────────────────────────┐
│ 调用方层（S1 范围外）                                          │
│ NetaClaw Agent  ←─── netabrowser-cli skill（探索期 token 省）│
│ Geo / 电商业务  ←─── @Inject BrowserDaemonService（固化期）  │
└──────────────────────────────────────────────────────────────┘
                              ↓ ↓
┌──────────────────────────────────────────────────────────────┐
│ S1 范围                                                        │
│                                                                │
│ packages/netabrowser-cli/         CLI 包装 + Chromium 二进制 │
│ packages/backend/...browser-daemon/  Daemon Service + HTTP   │
│ packages/backend/skills/netabrowser-cli/  Skill 元数据       │
└──────────────────────────────────────────────────────────────┘
```

### 4.2 后端模块目录

```
packages/backend/src/modules/netaclaw/browser-daemon/
├── service/
│   ├── daemon.service.ts          # ★ BrowserDaemonService（核心，单例）
│   ├── humanizer.service.ts       # 拟人化操作包装（ghost-cursor + 自加套）
│   └── fingerprint.service.ts     # neta-chromium 指纹参数管理
├── controller/admin/
│   ├── session.ts                 # /open /close /list 会话管理
│   ├── interaction.ts             # /click /fill /type /scroll /hover 拟人化交互
│   ├── navigation.ts              # /goto /back /forward /reload
│   ├── state.ts                   # /cookie-list /cookie-set /state-save /state-load
│   └── inspect.ts                 # /snapshot /screenshot /eval /run-code
├── runtime/
│   ├── chromium-launcher.ts       # 启动 neta-chromium 子进程（路径解析、proxy、fingerprint args）
│   ├── session-registry.ts        # Map<sessionName, BrowserContext> 内存表
│   └── cleanup.ts                 # backend 退出时优雅关闭所有 context
└── config.ts
```

### 4.3 netabrowser-cli 包目录

```
packages/netabrowser-cli/
├── package.json                   # bin: netabrowser-cli → dist/bin/main.js
├── src/
│   ├── bin/
│   │   └── main.ts                # CLI 入口（commander 解析）
│   ├── commands/
│   │   ├── open.ts                # netabrowser-cli open <url> --session=xx --proxy=xx --fingerprint=xx
│   │   ├── close.ts
│   │   ├── click.ts / fill.ts / type.ts / scroll.ts / hover.ts / press.ts
│   │   ├── cookie.ts              # cookie-list / cookie-set / cookie-clear
│   │   ├── state.ts               # state-save / state-load
│   │   ├── navigate.ts            # goto / back / forward / reload
│   │   ├── snapshot.ts / screenshot.ts / eval.ts / run-code.ts
│   │   └── list.ts                # netabrowser-cli list（列出所有 session）
│   ├── client/
│   │   ├── http-client.ts         # 调 backend HTTP API
│   │   └── runtime-info.ts        # 解析 backend runtime info 拿端口和 secret
│   └── output/
│       └── formatter.ts           # --raw / 默认格式化输出
├── vendor/                        # patchright 补丁源码——仅作"未来改造参考"，运行时不依赖
│   ├── patchright/                # Kaliiiiiiiiii-Vinyzu/patchright（driver patches 源码，已 vendor）
│   └── patchright-nodejs/         # Kaliiiiiiiiii-Vinyzu/patchright-nodejs（client patches 源码，已 vendor）
│                                  # 运行时直接 npm install patchright（编译产物）；
│                                  # 当未来要改 patch（如加小红书反检测）才需要本地跑 vendor 里的 patches 流程
├── chromium/
│   └── win64/                     # neta-chromium 二进制 397MB（已就位，gitignore）
├── tests/
└── README.md
```

### 4.4 Skill 目录

```
packages/backend/skills/netabrowser-cli/
├── SKILL.md                       # 给 NetaClaw Agent 看的命令清单
└── references/
    ├── humanization.md            # 拟人化能力说明
    ├── fingerprint.md             # 指纹参数说明
    ├── proxy.md                   # 代理配置说明
    └── examples/                  # 典型场景示例
```

---

## 5. 核心数据流

### 5.1 启动会话（探索期 AI 路径）

```
NetaClaw Agent 决定打开淘宝
   ↓
exec("netabrowser-cli open https://www.taobao.com \
        --session=geo-1 \
        --proxy=http://e50b26:7ecdccfd@210.51.27.112:10000 \
        --fingerprint-seed=42 \
        --profile-dir=geo-1 \
        --headed")
   ↓
netabrowser-cli/src/bin/main.ts
   → 解析参数
   → 读 runtime-info.json 拿 backend 端口 + secret
   → POST http://127.0.0.1:8003/admin/browser-daemon/open
        Headers: x-neta-control-secret: <secret>
        Body: {sessionName, url, proxy, fingerprintSeed, profileDir, headed}
   ↓
backend → AdminBrowserDaemonSessionController
   → @Inject BrowserDaemonService
   → daemonService.open({...})
   ↓
BrowserDaemonService.open():
   1. 校验 sessionName 不重复（查 session-registry）
   2. 拼 launch args：
      - executablePath = <安装目录>/chromium/win64/chrome.exe
      - args = ['--fingerprint=42', '--fingerprint-platform=Windows', ...]
      - userDataDir = <dataDir>/browser-profiles/<profileDir>
      - proxy = {server, username, password}
   3. patchright chromium.launchPersistentContext() 启动
   4. 把 BrowserContext 存入 session-registry
   5. 返回 {sessionName, pageCount, url}
   ↓
HTTP 200 → cli 输出格式化结果给 AI
```

### 5.2 拟人化点击

```
AI: netabrowser-cli click e15 --session=geo-1
   ↓
cli → POST /admin/browser-daemon/click {sessionName, ref}
   ↓
daemonService.click(sessionName, ref):
   const ctx = registry.get(sessionName);
   const page = ctx.pages()[0];
   const locator = page.locator(`[data-ai-ref="${ref}"]`);

   await humanizer.click(page, locator);
        ↓
        Humanizer.click:
          1. 计算目标元素 box 中心
          2. ghost-cursor 生成贝塞尔轨迹（鼠标当前位置 → 目标）
          3. 沿轨迹移动，每段 5-15ms；10% 概率轨迹中段微停顿 50-200ms
          4. 到达目标后停顿 100-300ms（人类视觉确认）
          5. mousedown → 50-150ms → mouseup
          6. 20% 概率 click 后 micro-scroll 2-10px
   ↓
返回 {clicked: true, snapshotRef: 'e16'}
```

### 5.3 抓 Cookie + 持久登录态（固化期 service 路径示例）

```ts
// 未来 S3 的 XiaohongshuLoginService（不在 S1 范围）
@Provide()
class XiaohongshuLoginService {
  @Inject() browserDaemon: BrowserDaemonService;
  @Inject() encryptService: GeoEncryptService;

  async loginAndCapture(account: GeoAccountEntity) {
    const session = `geo-${account.id}`;
    await this.browserDaemon.open({
      sessionName: session,
      url: 'https://www.xiaohongshu.com',
      proxy: this.toProxyConfig(account.proxy),
      fingerprintSeed: account.fingerprintSeed,
      profileDir: `geo-${account.id}`,
      headed: true,
    });

    await this.browserDaemon.click(session, '登录按钮 selector');
    await this.browserDaemon.waitForUrl(session, /\/explore\//, 60_000);

    const cookies = await this.browserDaemon.getCookies(session, 'xiaohongshu.com');
    await this.browserDaemon.saveState(session, `<dataDir>/states/geo-${account.id}.json`);

    account.cookies = this.encryptService.encrypt(JSON.stringify(cookies));
    account.loginStatus = 'logged_in';
    await this.accountRepo.save(account);

    await this.browserDaemon.close(session);
  }
}
```

**关键点**：S3 业务 service 通过 @Inject 直接调 `BrowserDaemonService`，不走 HTTP，不走 cli。性能 = 直接 patchright API 调用。

### 5.3.1 service ↔ HTTP 路径等价性约束（强制）

CLI 路径与业务 @Inject 路径必须**严格等价**：调用同一个 BrowserDaemonService 方法，得到同样的副作用。

**强制规则**：
1. `controller/admin/*.ts` 必须是 service 方法的 thin wrapper，**不允许加任何业务逻辑**（参数转换、auth 检查、日志记录都不行）
2. controller 只做 4 件事：① 解析 HTTP body → DTO；② 校验 auth（见 §5.5）；③ 调 `this.daemonService.xxx(dto)`；④ 序列化返回值
3. 业务逻辑（重试、降级、补偿、校验）必须在 service 层实现
4. 测试加 contract test：每个 HTTP endpoint 跟同名 service 方法行为一一对照（mock service，verify HTTP 调用 1:1 转发）

### 5.4 Backend 重启时的会话恢复

```
backend 启动 → BrowserDaemonService @Init:
   1. 扫描 <dataDir>/states/*.json
   2. 不主动 launch（避免启动阻塞），仅记录 known sessions
   3. 当 cli/business 第一次 open(sessionName='geo-1')，发现该 sessionName 有持久化 state，自动 loadState

backend 关闭 → BrowserDaemonService @Destroy:
   1. 遍历 session-registry
   2. 每个 context 先 saveState 到 dataDir
   3. context.close() 优雅关闭
   4. 超时 5s 强制 kill
```

**重启后状态语义**（重要）：

- ✅ **可恢复**：sessionName、cookie、localStorage（通过 storageState 文件）
- ❌ **不可恢复**：snapshot ref（如 `e15`）、page handles、in-memory selectors

→ AI Agent 在 backend 重启后**必须重新调 `snapshot` 命令获取新 ref**。spec/skill 文档明确说明此约束。

### 5.5 HTTP API 鉴权模型

**威胁模型**：netabrowser-cli daemon API 能驱动浏览器登录用户社媒账号、抓 cookie，权限极高。本机其他进程也可能调用 → 必须鉴权。

**方案**（与 Neta 现有 `/app/base/runtime/*` 一致）：

| 层 | 措施 |
|---|---|
| 网络层 | controller 中间件强制 `req.ip === '127.0.0.1'` 或 `::1`，非 loopback 直接 403 |
| 凭据层 | 复用 `runtime-info.json` 中的 `controlSecret`（已有机制）。每次 HTTP 请求 header `x-neta-control-secret: <secret>` 必填，不匹配 401 |
| CLI 端 | netabrowser-cli 启动时自动读 `<dataDir>/runtime-info.json` 获取 secret，每次请求自动附 header |
| service 层 | @Inject 调用方（业务代码）天然在 backend 进程内，不需要 secret |

**实现锚点**：参考 `packages/backend/src/modules/base/middleware/runtime-control.middleware.ts`（runtime-info secret 现有中间件，按需复用或新建一个 `browser-daemon-auth.middleware.ts`）。

### 5.6 并发与会话锁

**问题**：cli 和 service 可能同时调 `open(sessionName='geo-1')`，两个请求都看到 sessionName 不存在 → 都启动 chrome → registry 第二次 set 覆盖第一次 → 第一个 chrome 进程残留。

**方案**：BrowserDaemonService 内部维护 `Map<sessionName, Promise<void>>` 作为每会话的串行化锁：

```ts
private locks = new Map<string, Promise<void>>();

async withLock<T>(sessionName: string, fn: () => Promise<T>): Promise<T> {
  const prev = this.locks.get(sessionName) ?? Promise.resolve();
  let release!: () => void;
  const next = new Promise<void>(r => (release = r));
  this.locks.set(sessionName, prev.then(() => next));
  try {
    await prev;
    return await fn();
  } finally {
    release();
    if (this.locks.get(sessionName) === next) this.locks.delete(sessionName);
  }
}

async open(opts) {
  return this.withLock(opts.sessionName, async () => {
    // 安全地 check-then-launch
  });
}
```

**所有 service 方法**（open/close/click/fill/cookie-list/...）都在该锁内执行，保证同 sessionName 操作严格串行。

### 5.7 内存与会话调度策略

**问题**：100 个 chrome × 200-400MB ≈ 20-40GB RAM，普通 16GB 机器扛不住。

**策略**：

| 维度 | 决策 |
|---|---|
| **软上限** | 默认同时活跃 session 数上限 = `min(物理内存GB / 0.5, 50)`。可通过 `config.yaml` 的 `browserDaemon.maxActiveSessions` 覆写 |
| **空闲回收** | session 超过 60min 无命令 → 自动 saveState 后 close（释放 chrome 进程内存）；下次 open 同 sessionName 自动 loadState 重启 |
| **触达上限** | 新 open 请求 → 先 LRU 回收最久未使用的 idle session；如全部 active 中无 idle 可回收 → 返回 503 + 重试建议 |
| **优先级** | DTO 加 `priority?: 'low' \| 'normal' \| 'high'` 字段，high 优先级抢占 low 优先级的 session 位 |
| **拒绝策略** | 默认是 fail-fast（503）；DTO 可选 `--queue=true` 让请求排队等位（最长 30s） |
| **指标** | service 暴露 `getStats()`：activeCount/idleCount/totalRamMB/queueLength，供监控面板和决策使用 |

**实现锚点**：`runtime/session-scheduler.ts` 实现 LRU + 软上限 + 队列。

### 5.5 错误处理

| 场景 | 处理 |
|---|---|
| sessionName 已存在 | 返回 409 Conflict + 现有 session 信息 |
| 找不到 chrome.exe | 抛 `ChromiumNotInstalled`，提示重新安装 |
| chrome 启动失败 | 抛 `LaunchFailed`，附带 chromium stderr 最后 50 行 |
| 元素 ref 找不到 | 抛 `RefNotFound`，附带最近 snapshot 相邻元素 |
| 拟人化轨迹超时（10s） | 降级直接 click，记 warn |
| backend 崩溃 | 所有 context 跟着崩；持久化 state 仍在；下次启动重新 launch |

---

## 6. 拟人化与指纹细节

### 6.1 拟人化能力清单（默认开启）

| 操作 | 行为 |
|---|---|
| **click** | ghost-cursor 贝塞尔轨迹 → 停顿 100-300ms → mousedown(50-150ms) → mouseup |
| **type** | 字符间随机间隔 80-250ms；5% 概率模拟错字 → backspace → 重输 |
| **fill** | 先 click focus，再走 type 流程 |
| **press** | 按下后 30-100ms 抬起 |
| **hover** | 贝塞尔轨迹移到目标，停留 200-800ms |
| **scroll** | wheel events 而非 jump-to-position；多次 30-80px，间隔 50-150ms |
| **goto** | 页面 load 后随机停留 1-3s |
| **idle**（隐式） | 每命令完成后 50-200ms 默认延迟 |

### 6.2 拟人化的三档可调

**问题**：完整拟人化每命令 +2-5s。100 账号 × 30 操作 = 4 小时纯延迟，**批量场景会卡死**。

**方案**：三档 `--humanize-mode`：

| 档位 | 行为 | 单命令开销 | 适用 |
|---|---|---|---|
| **`full`**（默认） | 贝塞尔轨迹 + 视觉停顿 + 字符间隔 + 错字回退 + micro-scroll | 2-5s | 养号、敏感操作、AI 探索 |
| **`fast`** | 保留延迟（200-500ms）但**去掉**鼠标轨迹、错字、micro-scroll | 0.3-0.7s | 批量发布、批量评论、批量数据采集 |
| **`off`** | 完全关闭，立即执行 | <100ms | 测试、CI |

**触发方式**：

```bash
# 命令级（最高优先级）
netabrowser-cli click e15 --humanize-mode=fast

# 会话级（open 时设默认值）
netabrowser-cli open <url> --session=xx --humanize-mode=fast

# 全局环境变量
NETA_BROWSER_HUMANIZE_MODE=fast netabrowser-cli click e15

# 兼容老开关
netabrowser-cli click e15 --no-humanize    # 等价于 --humanize-mode=off
```

**实现要点**：humanizer 接受 `mode: 'full' | 'fast' | 'off'`，每个动作内部按 mode 分支选择行为。

### 6.3 ghost-cursor 集成

```ts
import { createCursor } from 'ghost-cursor';

class Humanizer {
  async click(page: Page, locator: Locator) {
    const box = await locator.boundingBox();
    if (!box) throw new Error('Element not visible');

    const cursor = createCursor(page as any, undefined, true);
    await cursor.moveTo({ x: box.x + box.width/2, y: box.y + box.height/2 }, {
      randomizeMoveDelay: true,
      moveDelay: random(50, 150),
    });
    await sleep(random(100, 300));
    await page.mouse.down();
    await sleep(random(50, 150));
    await page.mouse.up();
    if (Math.random() < 0.2) {
      await page.mouse.wheel(0, random(2, 10));
    }
  }
}
```

### 6.4 neta-chromium 指纹参数

```bash
netabrowser-cli open <url> \
  --session=geo-1 \
  --fingerprint-seed=42                  # 单一 seed 自动派生所有维度（推荐）

# 或细粒度控制：
  --fingerprint-platform=Windows \
  --fingerprint-platform-version=10.0.0 \
  --fingerprint-brand=Chrome \
  --fingerprint-brand-version=144 \
  --fingerprint-hardware-concurrency=8 \
  --fingerprint-language=zh-CN \
  --fingerprint-timezone=Asia/Shanghai
```

底层翻译为 chrome.exe 命令行参数（已通过 chrome 144.0.7559.132 验证：seed=11111 与 seed=22222 产生不同 canvas/webgl 指纹）。

### 6.5 代理参数

```bash
# 完整代理 URL（最常用）
netabrowser-cli open <url> --session=xx \
  --proxy=http://e50b26:7ecdccfd@210.51.27.112:10000

# 拆分参数
  --proxy-server=http://210.51.27.112:10000 \
  --proxy-username=e50b26 \
  --proxy-password=7ecdccfd
```

底层用 patchright `launchPersistentContext({proxy: {...}})`，已验证出口 IP 真实变成代理 IP。

### 6.6 会话生命周期

| 状态 | 触发 | 行为 |
|---|---|---|
| **created** | `open` 调用 | 内存 context + 持久 profileDir + session-registry 记录 |
| **active** | 同上 | 等待命令 |
| **closed** | `close` 调用 / backend 退出 | saveState → context.close() → 从 registry 删除 |
| **idle-timeout** | 60min 无命令 | 自动 saveState 到磁盘，关闭 context；下次 open 同 sessionName 自动 loadState |

`--no-idle-timeout` 关闭自动空闲回收（养号常驻场景）。

---

## 7. 集成 + 打包

### 7.1 与 Neta 现有代码集成清单

| 组件 | 改动 |
|---|---|
| `pnpm-workspace.yaml` | 加 `packages/netabrowser-cli` |
| `packages/backend/package.json` | 加 `patchright`（npm 包，编译产物，**不**依赖 vendor 的 patches 源码）+ `ghost-cursor` 依赖 |
| `packages/backend/src/modules/netaclaw/` | 新增 browser-daemon 子目录 |
| `packages/backend/src/comm/data-dir.ts` | **不改**；daemon 内拼子路径 |
| `packages/backend/src/comm/runtime-info.ts` | **不改** |
| `packages/backend/installer/setup.iss` | 增加 `[Files]`：`chromium/win64/*` |
| `packages/backend/scripts/build-windows-installer.js` | 增加复制 chromium 二进制 |
| `packages/backend/scripts/pkg-build.js` | **不改**；netabrowser-cli 单独 pkg 打包 |
| `packages/backend/skills/netabrowser-cli/` | 新增 skill 目录 |

### 7.2 chromium 路径解析

```ts
function resolveChromiumPath(): string {
  if (process.env.NETA_CHROMIUM_PATH) return process.env.NETA_CHROMIUM_PATH;
  if (isPkg()) {
    return path.join(path.dirname(process.execPath), 'chromium', 'win64', 'chrome.exe');
  }
  return path.resolve(__dirname, '../../../../../../netabrowser-cli/chromium/win64/chrome.exe');
}
```

### 7.3 安装包目录结构（prod 模式）

```
C:\Program Files\Neta\
├── backend.exe
├── Neta.Tray.exe
├── netabrowser-cli.exe            ★ 新增
├── config.yaml
├── chromium\
│   └── win64\                     ★ 新增 397MB
│       ├── chrome.exe / chrome.dll / ...
├── data\
│   ├── browser-profiles\          # 每会话独立 profile
│   ├── states\                    # 持久化登录态
│   ├── logs\
│   └── runtime-info.json
└── skills\
    ├── netabrowser-cli\           ★ 新增
    ├── playwright-cli\
    └── patchwright-cli\
```

---

## 8. 测试策略

| 层 | 方法 |
|---|---|
| **Humanizer 单元** | mock page，验证 ghost-cursor 调用 + 延迟范围 + 错字回退 |
| **BrowserDaemonService 单元** | mock patchright，验证 session 注册/launch args 拼装/清理 |
| **chromium-launcher 单元** | 验证路径解析（dev/pkg/env 三种）|
| **Controller 集成** | jest + supertest，跑 open → click → cookie → close（mock page）|
| **CLI bin 单元** | mock HTTP，验证参数解析、output formatter |
| **真机冒烟** | 手工启动 backend，cli 调 `open https://httpbin.org/ip --proxy=...`，验证出口 IP；访问 https://abrahamjuliot.github.io/creepjs/ 看 trust score |

---

## 9. 验收标准

1. ✅ `pnpm dev` 启动 backend，`<dataDir>/runtime-info.json` 写入
2. ✅ `netabrowser-cli list` 返回空 `[]`
3. ✅ `netabrowser-cli open https://httpbin.org/ip --session=test --proxy=http://e50b26:7ecdccfd@210.51.27.112:10000` 返回 200，出口 IP **210.51.27.112**
4. ✅ `netabrowser-cli open https://creepjs.com --session=test --fingerprint-seed=42 --headed` 启动有头浏览器，creepjs **trust score ≥ 70%**
5. ✅ `netabrowser-cli click <ref> --session=test` 触发拟人化轨迹（mock 验证 + 真机肉眼）
5b. ✅ `--humanize-mode=fast` 单命令延迟 < 1s；`--humanize-mode=off` 单命令延迟 < 200ms
5c. ✅ HTTP API 不带 `x-neta-control-secret` header → 401；非 loopback → 403
5d. ✅ 同 sessionName 并发 open 两次，第二次返回 409 Conflict（锁机制有效）
5e. ✅ 软上限触达后，新 open 返回 503（含 `retryAfter` 字段）；`--queue=true` 时排队等位
6. ✅ `netabrowser-cli cookie-list --session=test --domain=httpbin.org` 返回 cookie JSON
7. ✅ `netabrowser-cli state-save --session=test --output=/tmp/state.json` 写文件，`state-load` 能恢复
8. ✅ `netabrowser-cli close --session=test` 关闭，`list` 不再显示
9. ✅ backend 重启后，已 saveState 的 session 通过 `open --session=同名` 自动 loadState 恢复
10. ✅ Inno Setup 打包后的安装包安装到全新 Windows，启动后能跑通 1-9
11. ✅ Skill 元数据完整（SKILL.md 命令清单 + references/），NetaClaw Agent 能识别

---

## 10. 范围红线（不在 S1）

- ❌ 平台特定补丁（小红书/抖音/淘宝特殊反风控）→ S2
- ❌ 业务 service（XiaohongshuLoginService 等）→ S2/S3
- ❌ geo BrowserAutomationService 迁移 → S2 单独 spec
- ❌ macOS / Linux 支持（chromium 只打包 win64）→ 后续
- ❌ 多 chrome 实例分布式调度 → 后续
- ❌ skill SKILL.md 国内平台示例（仅留通用例子）→ S2
- ❌ 替换/删除现有 playwright-cli / patchwright-cli skill → 不动

---

## 11. 风险与依赖

| 风险 | 缓解 |
|---|---|
| 上游 patchright 修复 bug 但 npm 包发布慢 | 通常上游 release 后 npm publish 间隔几天可接受；急用时启用 vendor 的 patches 本地构建 |
| 国内场景出现 patchright 默认未覆盖的反检测 | vendor 的 patches 源码是改造起点，那时下载 playwright 源码本地跑 patches 流程，发布到 monorepo 内部 npm 仓 |
| ghost-cursor 不兼容 patchright | 已知 ghost-cursor 接受 puppeteer-style page，patchright 兼容；冒烟阶段实测 |
| 安装包 400MB 用户嫌大 | 安装界面提示"含浏览器内核 ~300MB"；后续可选"在线安装版" |
| chrome.exe 进程残留 | daemon @Destroy 优雅关闭 + 启动时扫 chromium/win64/chrome.exe 进程清理 |
| 100 账号同时启动 OOM | session-registry 设上限（如 50），超过自动 idle-timeout 回收最久未用的 |
| AI 误调拟人化命令导致超慢 | 拟人化每命令 +2-5s（full）；提供 `--humanize-mode=fast/off` 三档兜底；SKILL.md 明确说明性能特征与档位选择 |
| chromium 144.0.7559.132 不是真 fingerprint-chromium | 已验证：seed=11111 与 seed=22222 产生不同 canvas/webgl，确认是真 fingerprint-chromium |
| **AI ref 协议（e15）依赖 playwright-cli 内部状态，patchright 无现成实现** | **plan 阶段 spike #1**：选定方案（自实现 ref 协议 / 改用 selector / vendor playwright-cli ref 代码）|
| **ghost-cursor 是为 puppeteer/playwright 设计，patchright 改了 mouse.* 实现，可能不兼容** | **plan 阶段 spike #2**：跑一个 demo 在 patchright + neta-chromium 上用 ghost-cursor click，验证轨迹和反检测都生效 |

---

## 12. 工作流

完成本 spec 后：

1. user 复核本文档
2. 调用 `superpowers:writing-plans` 生成 `docs/superpowers/plans/2026-05-04-netabrowser-cli-s1-plan.md`
3. **plan 第一阶段必须包含两个 spike**（在写实现代码前完成）：
   - **Spike #1：AI ref 协议**——验证一种方案：① 自实现（snapshot 时 inject `data-ai-ref` 到 DOM + 内存 ref→selector 表）；② 改用 selector 字符串作为命令参数；③ vendor playwright-cli 的 ref 协议代码进 netabrowser-cli。**spike 产出**：选定一种 + demo 跑通 click(e15) 整套流程
   - **Spike #2：ghost-cursor on patchright + neta-chromium**——demo：用 patchright launchPersistentContext 启动 neta-chromium → 用 ghost-cursor 在页面上轨迹移动鼠标 click 一个按钮 → 验证 ① 鼠标确实走贝塞尔轨迹 ② 在 brotector/creepjs 上仍过反检测
4. user 复核 plan
5. `superpowers:subagent-driven-development` 实施
6. `superpowers:verification-before-completion` 逐条核对验收标准
7. `superpowers:requesting-code-review`
8. 完成后回头继续 geo S1 联调（Task 22）：geo PlainChromiumProvider 改调 BrowserDaemonService

---

## 13. 变更日志

| 日期 | 变更 |
|---|---|
| 2026-05-04 | 初稿（brainstorming 完成后产出） |
| 2026-05-04 | 架构 review v2 修复 7 项必修：①patchright 改用 npm 包+vendor 仅参考 ②HTTP API auth 模型（loopback+secret） ③service↔HTTP 等价性约束 ④sessionName 串行化锁 ⑤内存调度策略（LRU+软上限+优先级） ⑥拟人化三档（full/fast/off） ⑦backend 重启后 ref 必须重 snapshot；plan 阶段加两个 spike：AI ref 协议 + ghost-cursor 兼容 |
