# 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 内拼子路径 `/browser-profiles/`、`/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 内存表 │ └── 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 --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: 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 = /browser-profiles/ - 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, `/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. 扫描 /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: ` 必填,不匹配 401 | | CLI 端 | netabrowser-cli 启动时自动读 `/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>` 作为每会话的串行化锁: ```ts private locks = new Map>(); async withLock(sessionName: string, fn: () => Promise): Promise { const prev = this.locks.get(sessionName) ?? Promise.resolve(); let release!: () => void; const next = new Promise(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 --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 \ --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 --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,`/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 --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 兼容 |