无头浏览器 CDP 自动化 / Headless Browser CDP Automation

作者:红叶开发工具v1

浏览器自动化:接管用户已登录的浏览器会话,用 playwright-core CDP 驱动无头实例做表单填写、上传、发布、抓取。自动探测本机 Chromium 系浏览器(Edge/Chrome/华为/360/搜狗/Opera)与多 user-data-dir,支持 Firefox 备选;含登录态判定(只信接口不信页面文字)、DPAPI 解密 cookie 注入、无头启动参数、沙箱代理、DOM click 提交、微信扫码、进程清理(只杀无头不杀用户浏览器)、跨机器迁移与依赖自检、打包前脱敏红线。触发词:接管我的登录会话、我已经登录了 XX 帮我自动操作、批量上传/发布/抓取、用浏览器帮我点、扫码登录、浏览器自动化、headless CDP。

下载量
347
点赞
85
价格
¥0.02
精选

技能文档

---
name: headless-browser-cdp-automation
display_name: 无头浏览器 CDP 自动化 / Headless Browser CDP Automation
description: 浏览器自动化:接管用户已登录的浏览器会话,用 playwright-core CDP 驱动无头实例做表单填写、上传、发布、抓取。自动探测本机 Chromium 系浏览器(Edge/Chrome/华为/360/搜狗/Opera)与多 user-data-dir,支持 Firefox 备选;含登录态判定(只信接口不信页面文字)、DPAPI 解密 cookie 注入、无头启动参数、沙箱代理、DOM click 提交、微信扫码、进程清理(只杀无头不杀用户浏览器)、跨机器迁移与依赖自检、打包前脱敏红线。触发词:接管我的登录会话、我已经登录了 XX 帮我自动操作、批量上传/发布/抓取、用浏览器帮我点、扫码登录、浏览器自动化、headless CDP。
summary_zh: 复用用户真实浏览器登录态的无头自动化方法论:多浏览器自动探测、CDP 接管、DPAPI cookie 注入、登录态判定、multipart 抓包、安全收尾,避免误杀用户浏览器。
summary_en: "Headless browser automation that reuses the user's real logged-in session - multi-browser detection, CDP attach, DPAPI cookie injection, login-state verification, multipart payload capture, and safe teardown that never kills the user's own browser."
version: 1.1.0
category: automation
tags: [automation, browser, cdp, playwright, headless, scraping, upload]
agent_created: true
---

# 无头浏览器 CDP 自动化(多浏览器接管真实登录态)

## 何时用

- 用户说「我已经登录了 XX,帮我自动化操作」「接管我的登录会话」「批量上传/发布/抓取」。
- 目标站需要真实账号登录态(SkillHub、腾讯文档、内网 CRM 等),AI 拿不到 cookie 也不能重新扫码。
- 典型:批量把 Skill 发布到 SkillHub、往某平台连续传文件、抓取需登录的结构。

**核心前提**:复用**用户真实的浏览器 user-data-dir / 持久 profile**(含登录 cookie),而不是另起隔离 profile。

## ★ 两种工作模式(先判断走哪种,API 完全不同)

| | 模式 A:接管已运行的实例(CDP) | 模式 B:复用持久 profile 启动新实例 |
|---|---|---|
| 场景 | 用户浏览器**正开着且已登录**目标站;或沙箱起好无头在跑 | 需要一个**长期持久**的登录会话(用户先手动登录一次存盘,之后自动复用,免重复登录) |
| 登录态来源 | 该实例内存 + 磁盘 cookie | 持久 user-data-dir / profile 里的磁盘 cookie |
| 连接 API | `chromium.connectOverCDP(debugPort)` | `chromium.launchPersistentContext(profileDir, {...})`(**不是** `launch`,launch 不收 userDataDir) |
| 端口 | 复用已有远程调试端口 | 可带 `--remote-debugging-port` 或无 |
| 典型 | SkillHub 发布、接管用户正开的内网 | 内网业务系统(CRM / 工单 / 商品库)的抓取会话(8 月大量实战) |

**判据**:用户说「我已经在浏览器登录了」且浏览器开着 → **模式 A**;说「你自动跑,登录态之前存过 profile」或要长期免登录 → **模式 B**。

两种模式对本技能的核心思想(登录态判定靠接口、DOM click 提交、只杀无头不杀用户浏览器)完全共用,只是「连 vs 启」不同。多数「登录后操作」需求走 **模式 A**。

## 支持矩阵(Windows)

| 浏览器 | 引擎 | CDP 接管 | 无头启动 | 备注 |
|---|---|---|---|---|
| Microsoft Edge | Chromium | ✅ | ✅ | 默认已装;实例合并 + 持久 profile 单例锁(见第二节) |
| Google Chrome | Chromium | ✅ | ✅ | |
| 华为浏览器 | Chromium | ✅ | ✅ | 本机实际场景(Chromium 99) |
| 360 / 搜狗 / Opera / Vivaldi / Brave | Chromium 系 | ✅ | ✅ | 大同小异 |
| Firefox | Gecko | ❌ CDP | ⚠️ | 走 playwright `firefox.launchPersistentContext`,协议不同需特判 |

**结论**:Windows 上 99% 目标都是 Chromium 系,**同一套 CDP 流程通用**,无需为每款重写。唯一要做的差异处理是「探测哪个浏览器装了 + 对应 user-data-dir 路径 + Edge 特例」。

## 一、自动探测本机浏览器(优先做这步,别写死路径)

```bash
# 常见安装路径探测(按优先级,找到即用)
/cygdrive/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe
/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe
/c/Program Files/Google/Chrome/Application/chrome.exe
/c/Program Files (x86)/Google/Chrome/Application/chrome.exe
/c/Program Files/Huawei/Browser/HuaweiBrowser.exe
/c/Program Files (x86)/360/360Chrome/Chrome/Application/360chrome.exe
# user-data-dir 探测
/c/Users/<user>/AppData/Local/Microsoft/Edge/User Data
/c/Users/<user>/AppData/Local/Google/Chrome/User Data
/c/Users/<user>/AppData/Local/Huawei/HuaweiBrowser/User Data
```

探测顺序建议:**先问用户"你平时用哪个浏览器登录了目标站"** → 再按用户说的那个去取路径 + user-data-dir → 若找不到再回落到自动扫描全表。**绝不要想当然用 Chrome**(很多国内机器根本没装 Chrome,只有 Edge/360/华为)。

**Windows 路径注意**:Git Bash 下 `C:\Program Files (x86)\...` 里的空格和括号要处理;用 `ls` 或 `[ -f ]` 逐条确认,勿在脚本里裸拼。

## 二、Edge 特例(最容易翻车):实例合并 + 持久 profile 单例锁

Edge 有个已知行为:**如果已经有一个 Edge 实例在跑(即使不是无头),再 launch 带 `--user-data-dir=<同一目录>` 会静默把新请求"合并"进已有窗口**,`--headless`/`--remote-debugging-port` 参数会被忽略 → 无头连不上 9222。

对策(任选其一):
1. **先彻底结束该 user-data-dir 的所有现有实例**,再以无头 + 该目录启动。但**别杀用户的正常窗口**——若用户正开着想复用的那个 profile 窗口,先请他关掉该浏览器(会保留登录态在磁盘 cookie 里)。
2. 或**复用已有实例的调试端口**:若目标浏览器本就以 `--remote-debugging-port` 起过,直接 connectOverCDP 即可,无需重启。
3. Edge 用户数据目录同一时间通常被后台进程占用,杀干净再启。

**模式 B(持久 profile)在 Edge 上的额外坑**(8 月内网业务系统(CRM / 工单 / 商品库)抓取血泪,跨机器通用):
- **单例锁 / headless 崩 exit21**:对同一持久 profile 同时有多个进程(用户自己的 Edge + 自动抓取)会撞锁,headless Edge 直接崩(exit code 21)。**同一持久 profile 同一时刻只允许一个进程**。复用前先确认该 profile 无其它 Edge 占用(`proc_check.js` 按 profile 目录关键字筛,别误杀用户浏览器)。
- **launchPersistentContext 用错 API 崩在启动**:持久会话必须 `chromium.launchPersistentContext(profileDir, {...})`;**没有 `userDataDir` 这种 launch 选项**(写成 launch 的 userDataDir 会崩,不弹窗)。
- **窗口不置顶 → 用户登错窗口**:headful `launchPersistentContext` 开的新窗口不保证置顶/最大化,常被用户已有窗口或 Windows 前台锁挡在背后 → 用户可能把登录登进**自己的** Edge 而不是脚本控制的窗口。缓解:用 CDP 控 OS 窗口 `Browser.getWindowForTarget` 拿 windowId → `Browser.setWindowBounds` **先设 normal 再设 maximized 两次状态变化**才能穿透前台锁真正激活(仅 `--start-maximized` 不够)。
- **登录态可能"不过期但每次要手动"**:某些内网(7060/6687)持久会话不自动带登录态,需在弹窗手动登录一次;别把"没跳登录页"误当已登录——用探针确认落点。

> Chrome/华为等非 Edge 的 Chromium 一般允许多实例不同 user-data-dir 并存,冲突没 Edge 那么强,但**同一 user-data-dir 仍只允许一个进程持有锁**。

## 二b、playwright-core 版本兼容(跨浏览器通用)

- `page.evaluate(fn, a, b)` **多参数在部分版本会抛** `Too many arguments... wrap them in an object`,且若被 `.catch(()=>false)` 会**静默吞掉 → 所有调用误判失败**。**一律单对象传参**:`page.evaluate(({x,y})=>..., {x,y})`。
- `.click({force:true})` **绕不过 `display:none` 折叠项**(H-ui 类菜单子项不可见时仍失败)→ 用原生 `element.click()`(见第七节)。
- `playwright-core` 只要 ≥1.4x 就有 `connectOverCDP` 与 `launchPersistentContext`;本机实际 1.62.1。

## 三、无头启动通用姿势(Chromium 系通用)

```bash
"<浏览器exe路径>" \
  --headless=new \
  --disable-gpu \
  --no-sandbox \
  --proxy-server=http://127.0.0.1:32848 \
  --remote-debugging-port=<端口,惯用9222> \
  --remote-allow-origins=* \
  --no-first-run \
  "--user-data-dir=<对应真实User Data>"
```

铁律(跨浏览器通用):
1. **必须 `--headless=new --disable-gpu`**:窗口模式 + GPU 必崩 `GPU process isn't usable. Goodbye`;headless 仍要 `--disable-gpu`。
2. **必须走沙箱代理 `--proxy-server=...`**:绝不可 `--no-proxy-server`(直连被封,浏览器全超时,curl 却通——假象)。**代理端口每次沙箱会话会变**,先用 `echo "$HTTPS_PROXY"`(Git Bash / Linux)或 `$env:HTTPS_PROXY`(PowerShell)看当前端口,再替换示例里的 `32848`。**示例里的 32848 只是占位符,硬抄必失败。**
3. **user-data-dir 必须指向用户那个浏览器的真实 User Data**,否则无登录态。但同一 user-data-dir 同一时间只能一个实例持有。
4. `--remote-allow-origins=*` 否则 CDP 握手被拒。
5. **换浏览器时端口可以复用 9222**(同一时间只跑一个无头),或错开到 9223/9224 避免与别的无头冲突。

## 四、CDP 连接(agent-browser connect 与低版本 Chromium 不兼容)

`agent-browser connect` 会握手挂起(尤其 Chromium 99 / 老内核)。改用 **playwright-core CDP 直连**,它对浏览器内核版本不敏感:

```js
const { chromium } = require('playwright-core');
const b = await chromium.connectOverCDP('http://127.0.0.1:9222');
const ctx = b.contexts()[0];
const page = ctx.pages().find(p => p.url().includes('目标域名')) || await ctx.newPage();
```

playwright-core 版本 ≥1.4x 即可;channel 参数(`'msedge'/'chrome'`)仅在需要 `launchPersistentContext` 时才用,CDP 接管不需要。

## 五、Firefox 备选(非 CDP)

若目标站登录态只在 Firefox:
- 用 playwright-core 的 `firefox.launchPersistentContext(<firefox profile>, { headless })`。
- Firefox profile 路径通常在 `C:\Users\<user>\AppData\Roaming\Mozilla\Firefox\Profiles\<random>.default`(找含 `.default`/`.default-release` 且 modified 最新的)。
- **协议不同**(Juggler 而非 CDP),connectOverCDP 无效,必须走 playwright 原生 API。
- 属于低频场景,仅在用户明确说"我用的 Firefox 登录的"才走这条。

## 六、登录态判定:只看接口,不信页面文字

- **坑**:页面顶部有"发布 Skill / 登录"按钮 ≠ 已登录(首页本来就有)。把"出现某按钮"当登录判据 = 假阳性,且可能把别人的推荐作品当成自己账号内容,用户会质疑"进错账号"。
- **正确**:调真实鉴权接口拿身份。SkillHub 是 `GET https://api.skillhub.cn/api/v1/auth/me`(注意 `api.` 子域;根域下 fetch 会得 SPA 的 HTML 404)。返回 200 + 用户名即已登录且能确认是哪个账号。
- 先 `fetch` 探测该域名的鉴权接口(登录后浏览器里 cookie 自动带上),能 200 才算真登录。**这一步与浏览器无关,任何被接管会话都能用。**

## 七、表单操作:DOM click,禁用坐标/playwright click

- 遮罩层/弹窗遮挡会让 playwright `.click()` 超时,或误点到右上角 Close 把弹窗关了但**没提交**。
- 一律用 `page.evaluate(() => btn.click())` 原生 DOM click。
- 判定提交成功:抓网络请求。SkillHub 发布是 `POST api.skillhub.cn/api/v1/community/skills/publish`,返回 **201 + ok:true** 才算成功(HTTP 200≠成功,要业务码)。
- 文本匹配按钮用**宽松 `includes('提交审核')`**,别用精确相等(按钮文字带空格/图标会漏,导致 `SUBMIT_DOM:false` 误报)。

## 八、微信扫码登录(若目标无登录态需扫码)

- 二维码 in iframe `open.weixin.qq.com/connect/qrconnect`,img 直链 `open.weixin.qq.com/connect/qrcode/<xxx>`,下载 PNG 发给用户扫。
- 保持实例运行,轮询 `auth/me` 直到 200。
- 换浏览器不影响此流程,扫码绑定的是"该 user-data-dir 的 cookie"。

## 九、收尾必做:释放端口,且只杀无头、别杀用户浏览器

后台无头实例退出后**不自动释放** 9222,且下次用户自己开浏览器会因端口/锁冲突失败。必须清理:
1. 用 netstat 找端口监听 PID → **看该 PID 命令行含 `--headless` + 该浏览器名**才确认是 AI 起的无头实例。
2. 只杀那个主 PID(子进程 crashpad/gpu/renderer 会随主进程一起消失)。
3. **绝不可** `taskkill /IM <浏览器>.exe`——会把用户手动开的正常浏览器一起杀掉。
4. 区分技巧:命令行含 `--headless=new --remote-debugging-port=<端口>` = AI 起的;命令行裸启动(无 headless、PPID 是桌面 explorer 系)= 用户浏览器,保留。
5. **跨浏览器通用**:杀的进程名随浏览器变(msedge.exe / chrome.exe / HuaweiBrowser.exe / 360chrome.exe...),但判断逻辑不变——**只杀含无头参数的那个 PID**,别按 exe 名一锅端。

排查进程命令行(PowerShell 输出有时被吞,先重定向到文件再读):
```powershell
Get-CimInstance Win32_Process | Where-Object { $_.Name -match 'node|msedge|chrome|HuaweiBrowser|360chrome' } |
  ForEach-Object { "PID $($_.ProcessId) PPID $($_.ParentProcessId) :: $($_.CommandLine)" } |
  Set-Content "$env:USERPROFILE\.workbuddy\tmp_proc.txt"
```
再删临时文件。杀进程:`Stop-Process -Id <无头PID> -Force`。

## 十、一体化解法:现成探测脚本

本技能自带可直接运行的探测脚本,先跑它拿到「本机哪款浏览器 + user-data-dir + 是否有无头占用」,再决定接管谁:

```bash
node "<技能目录>/scripts/detect_browsers.cjs"
node "<技能目录>/scripts/detect_browsers.cjs" edge          # 只看某款
node "<技能目录>/scripts/detect_browsers.cjs" --port 9333   # 换端口检查占用
```

输出示例(本机实测):
```
[✓ 可用] Edge      exe=C:/Program Files (x86)/.../msedge.exe   ud=.../Edge/User Data
[✓ 可用] Huawei    exe=C:/Program Files/Huawei/.../HuaweiBrowser.exe  ud=.../HuaweiBrowser/User Data
[✗ 未装] Chrome
检查调试端口 9222: 空闲
```
拿到 exe + ud 后,按第三节命令以无头启动、第四节 CDP 接管即可。脚本也顺带检测目标端口是否已被某无头实例占用(帮你判断是直接连还是先杀)。

**换机器后先跑自检**(零依赖,装没装 playwright-core 都能跑):

```bash
node "<技能目录>/scripts/bootstrap.cjs"
```

一次性检查 4 项并给出修复命令:① node 版本 ② playwright-core 是否已装(含路径) ③ 本机浏览器探测 ④ 调试端口占用者**是不是无头实例**(会明确标注「无头=可清理 / 用户浏览器=切勿杀」)。

## 十一、自动触发机制与跨机器分发(重要)

### 本机:不需要用户显式点名

技能放在 **user-level** 目录 `~/.workbuddy/skills/headless-browser-cdp-automation/`,WorkBuddy 会话启动时会读取所有技能的 `description`,用户只要用自然语言说出匹配意图(「我登录了 XX,帮我自动操作」「上传到已登录的平台」「接管浏览器会话」等),就会**自动匹配并加载**,无需说「调用 XX 技能」。

- 触发靠的是 frontmatter 里的 `description`,所以描述里要写全用户可能的口语说法与场景词。
- 若某次没自动触发,用户说一句相关的话即可(例如「用浏览器自动化那个技能」),或按 `Skill` 显式加载。
- 生效范围:**这台机器的所有项目**(user-level,不随单个项目走)。

### 换机器:必须带走,不会自动同步

`~/.workbuddy/skills/` 是本机目录,**不随项目 git、不跨机器同步**。新机器要用的三种方式:

| 方式 | 做法 | 适用 |
|---|---|---|
| **① 直接拷目录**(最快) | 整个 `headless-browser-cdp-automation/` 拷到新机器的 `~/.workbuddy/skills/` 下 | 自己的多台机器 |
| **② 从 SkillHub 安装** | 把技能打包发布到 SkillHub,新机器上搜索安装 | 公开分发 / 给别人用 |
| **③ 项目级内置** | 放到项目 `.workbuddy/skills/` 下随仓库走 | 只给某个项目用,且需团队共享 |

> 本技能跨项目通用(任何"已登录 → 自动化"场景都能用),推荐 **①或②**,不要塞进单个项目。

### 新机器的依赖(别漏)

| 依赖 | 是否必需 | 说明 |
|---|---|---|
| Node.js 18+ | ✅ 必需 | 跑脚本用 |
| `playwright-core` | ⚠️ 仅驱动时需要 | `scripts/detect_browsers.cjs` 与 `bootstrap.cjs` **零第三方依赖**,纯 `fs/os/child_process`,没装也能跑探测与自检;只有真正 `connectOverCDP` / `launchPersistentContext` 才需要它 |
| 目标浏览器本身 | ✅ 必需 | 装了才能接管其登录态 |

装 playwright-core(装到 WorkBuddy 隔离工作区,不污染全局):

```bash
cd ~/.workbuddy/binaries/node/workspace
npm install playwright-core
```
运行时带 `NODE_PATH=<workspace>/node_modules`,或直接在 workspace 目录下跑脚本。

## 十二、登录态获取:三种路径对比(DPAPI 注入最优)

拿到本机真实 user-data-dir 后,怎么"拿到"登录态是真正的问题。三种路径:

| 路径 | 做法 | 是否打扰用户 | 成功率 | 适用 |
|---|---|---|---|---|
| **A. 复制轻量 profile + 无头启动** | 只复制 Local State + Cookies + Local Storage + Session Storage + Web Data + Login Data 等关键文件到新目录,用 `--user-data-dir=<副本>` 起无头 | ✅ 完全不打断 | ⚠️ **实测多数失败**(国产浏览器尤其,cookie 在副本 profile 里直接被丢弃) | 用户浏览器支持完整 cookie 解密,且**允许临时关浏览器** |
| **B. 直接用原目录起无头** | 让用户关掉浏览器,用真实 user-data-dir 启动无头 | ❌ 必须关浏览器 | ✅ 最高(cookie 原样加载) | 用户愿意配合 / 浏览器本身未开 |
| **C. DPAPI 解密 cookie + addCookies 注入** | 在原 profile(用户浏览器运行中)读 Local State 的 `os_crypt.encrypted_key` 和 Cookies 的 `encrypted_value`,DPAPI 解出 AES 主密钥 → AES-256-GCM 解密 → `context.addCookies()` 注入到**新**无头实例 | ✅ 完全不打断 | ✅ 高(同机同用户,DPAPI 必能解) | **最推荐**:用户浏览器不能关(如内网抓取、需持续在用的场景) |

**默认走 C(DPAPI 注入)**——它既不打断用户,又稳定可靠,是 2026-09 在 SkillPie 上首次跑通的方案。

### 路径 C 实操步骤(已验证,SkillPie 上线为案例)

```bash
# 1. 读 Local State 的 encrypted_key + Cookies 的 encrypted_value(host 过滤目标站)
#    python sqlite3 只读打开 <UD>/Default/Network/Cookies,查 host_key
# 2. 用 ctypes 调 Windows crypt32.CryptUnprotectData 解出 32 字节主密钥
#    (注意:encrypted_key 是 "DPAPI" 前缀 + DPAPI 密文,需去掉前 5 字节再 Unprotect)
# 3. AES-256-GCM 解密 encrypted_value:
#    - 前 3 字节 "v10"
#    - 3~15 字节 = nonce(12 字节)
#    - 15 ~ len-16 = ciphertext
#    - 末 16 字节 = auth tag
# 4. 无头实例启动后 connectOverCDP → addCookies([{name,value,domain,path,expires,httpOnly,secure,sameSite}])
#    expires 转换:Math.floor(expires_utc / 1000000) - 11644473600(expires_utc 是 1601 起微秒)
#    sameSite 映射:1→Lax, 2→Strict, 其他→None
# 5. 注入后刷新 / 调 /api/user/me 验证;返回 user 非空才算登录
```

### 为什么路径 A 失败

国产浏览器(华为/360 等)在复制出 Local State 后,浏览器启动时会**校验** `Local State.encrypted_key` 与本地环境的绑定关系(如机器指纹、注册表项、自身保护模块),发现不在原环境就读不到密钥,**直接丢弃 cookie 文件**,建一个空的。换句话说:你以为你只复制了 cookie,但浏览器认为你的整个环境是陌生的。

**改用 C 是关键升级点**:C 完全跳过"复制 profile"这一步,直接从用户浏览器**仍在用的**原 profile 解出 cookie 喂给无头实例。

### 收尾安全红线(用 DPAPI 后必须做)

- 任何含明文 session 或 AES 主密钥的临时文件(如 `pie_cookie.json`),**任务结束立即删除**。
- profile 副本目录(如果用过路径 A),**任务结束立即删除**(`mv` 进 `.trash/` 更好)。
- `--proxy-server` 端口从环境变量 `$HTTPS_PROXY` 取,**别写死**。

## 十三、批量操作平台时的抓包与判定技巧

用无头实例往第三方平台批量上传/发布时,这几条决定了你能不能判断"到底成没成"。

### 抓 multipart 请求体

`request.postData()` 对 `multipart/form-data` **返回空**,看不到上传的字段。改用劫持:

```js
await page.addInitScript(() => {
  const origAppend = FormData.prototype.append;
  window.__fd = [];
  FormData.prototype.append = function (k, v) {
    try { window.__fd.push([k, typeof v, v instanceof File ? v.name : String(v).slice(0, 80)]); }
    catch (e) { window.__fd.push([k, 'ERR']); }
    return origAppend.apply(this, arguments);
  };
});
// 提交后:await page.evaluate(() => window.__fd)
```

### 只想抓包不想真提交

```js
await page.route('**/api/**/publish*', route => route.abort());  // 拦下即丢弃
```
配合上面的劫持,能完整看到字段名与值而不产生任何真实数据。适合先做一次"空跑"验证字段拼装是否正确。

### 判定成功别截断响应体

```js
const body = (await resp.text()).slice(0, 6000);   // ← 别用 700
```
截太短会让 `JSON.parse` 抛错 → `skillId` 恒为 null → 把**成功误判成失败**(2026-09 SkillPie 实测踩过)。

同理:平台列表接口可能不回显某些字段(如 `usageInstructions: null`),**不代表没存**。要确认就抓公开详情页正文,别只看列表 API。

### 平台报「缺少 name / description」多半是假象

发布平台的前端逻辑是 `正则抽 frontmatter → YAML 解析 → catch 吞异常 → 走同一个"缺少字段"分支`。所以报错说缺字段,**真实原因常常是 YAML 解析失败**(重复键、值里含半角 `: `、Tab 缩进)。排查见配套技能 `skill-publish-preflight`。

## 操作序列(发布到 SkillHub 示例,多浏览器版)

1. **问清 + 探测**:用户用哪个浏览器登录的 → 落到该浏览器路径 + user-data-dir;没有则自动扫全表。
2. 探活:若目标 user-data-dir 已有实例在跑且用户不想关,看能否直接连已有调试端口;否则结束该目录实例后以无头 + 该目录启动,`GET api.skillhub.cn/api/v1/auth/me` 确认登录 + 账号身份。
3. CDP 连上 → 导航到目标(SkillHub dashboard `/dashboard` 即"我的 Skills")。
4. 打开发布/编辑表单,上传 zip、填字段、DOM click 提交。
5. 抓 publish API 201 确认成功。
6. 收尾:杀无头主 PID(按命令行判,不按 exe 名)、确认端口释放、确认用户正常浏览器未被误杀。

## 已沉淀经验(踩坑点速查)

- GPU 崩 → `--headless=new --disable-gpu`
- 浏览器断网/全超时(curl 却通)→ 缺 `--proxy-server` 沙箱代理
- CDP 握手挂起 → 换 playwright-core `connectOverCDP`
- Edge 起不来无头 → 同一 user-data-dir 已有实例,被"合并"进现有窗口;先清该目录实例再启
- Edge 持久 profile headless 崩 exit21 → 单例锁,同一 profile 只允许一个进程
- 持久会话必须 `launchPersistentContext`(**无 `userDataDir` 这种 launch 选项**)
- headful 新窗口不置顶 → 用户可能登错浏览器窗口;用 `Browser.setWindowBounds` 两次状态切换激活
- `page.evaluate(fn,a,b)` 多参部分版本抛错被静默吞 → 一律单对象 `{...}` 传参
- `.click({force:true})` 绕不过 `display:none` 折叠项 → 原生 `element.click()`
- 登录态假阳性 / 进错账号 → 只用 `auth/me` 接口 200 + 用户名判定
- DOM click 提交(坐标点会误关弹窗)
- SkillHub 发布成功 = POST publish 返回 201 ok:true
- 误杀用户浏览器 → 只杀命令行含 `--headless` + 端口的主 PID,**别按 exe 名 taskkill**
- 别默认 Chrome:本机多半只有 Edge/360/华为,先探测再动手
- 模式判别:浏览器正开且已登录→connectOverCDP;需长期持久→launchPersistentContext
- 复制 profile 取登录态多数失败(国产浏览器丢弃 cookie)→ 改用 DPAPI 解密 + addCookies 注入
- DPAPI:encrypted_key = "DPAPI"前缀(5B)+密文;AES-256-GCM 解 v10:nonce=[3:15], ct=[15:-16], tag=末16B
- expires_utc 微秒→Unix 秒:`Math.floor(e/1e6) - 11644473600`(0 或 undefined=会话 cookie)
- 沙箱代理端口每次会话会变 → `echo $HTTPS_PROXY` 现取,别写死
- 含明文 session / AES 主密钥的临时文件,任务结束立即删除
- 抓 multipart 请求体:`request.postData()` 为空 → `addInitScript` 劫持 `FormData.prototype.append`
- 只想抓包不落库 → `page.route('**/...', route.abort())`
- 判定发布结果:响应体别截太短(700 会让 JSON.parse 失败 → 成功被误判成失败)
- 列表 API 不回显某字段 ≠ 没存 → 抓公开详情页正文确认
- 平台报「缺少 name/description」→ 多半是 YAML 解析异常被吞(重复键 / 半角 `: ` / Tab)→ 见 `skill-publish-preflight`
- Git Bash 里 `taskkill /PID` 被路径转换吃掉 → `MSYS_NO_PATHCONV=1 taskkill /PID <pid> /F`
- node 里 `/tmp/x.js` 会解析成 `C:\tmp\x.js` → 临时脚本放 workspace 目录,别放 `/tmp`
- 打包上传前**必跑脱敏扫描**,且扫**实际产物**(zip 内容),不是源目录;再用已知脏文件反向验证扫描器没假阴性
- 杀无头前先验明正身:`GET /json/version` 返回 `HeadlessChrome/...` 才是自己起的实例

使用说明

## 解决什么问题

需要登录态的网站要批量操作时,AI 拿不到 cookie 也扫不了码。本技能复用你**正在使用的浏览器登录会话**,用无头实例接管,完成表单填写、文件上传、批量发布、抓取等动作,全程不打断你正常用浏览器。

## 核心认知

- **登录态只信接口,不信页面文字**:页面写"已登录"、按钮存在都不算数,必须请求 `auth/me` 类接口拿到非空 user 才算,否则极易进错账号还以为成功。
- **两种模式 API 完全不同**:浏览器正开着且已登录 → `connectOverCDP`(接管已有实例);需要长期持久会话 → `launchPersistentContext`(复用 profile)。注意 `launch()` 不接受 userDataDir。
- **别默认装的是 Chrome**:国内机器常见 Edge / 360 / 华为,先探测再动手。
- **复制 profile 取登录态多数失败**:国产浏览器会校验 Local State 与本机环境的绑定关系,副本 profile 里的 cookie 会被直接丢弃。改用 DPAPI 解密原 profile 的 cookie 再注入新实例。
- **只杀无头,不杀用户浏览器**:按"命令行含 --headless + 调试端口"定位主 PID,绝不按 exe 名 taskkill。
- **批量操作时别截断响应体**:判定发布结果时响应截得太短会让 JSON.parse 失败,把成功误判成失败。

## 使用方式

1. 跑 `scripts/detect_browsers.cjs` 探测本机浏览器、user-data-dir 与调试端口占用(零依赖)。
2. 换机器后先跑 `scripts/bootstrap.cjs` 自检四项:node 版本 / playwright-core / 浏览器探测 / 端口占用者是不是无头实例。
3. 起无头实例并开调试端口,DPAPI 解出目标站 cookie 后 `addCookies` 注入。
4. `connectOverCDP` 连上,导航、填表、用 DOM click 提交(坐标点击会误关弹窗)。
5. 抓提交接口响应确认结果;收尾只杀无头主 PID 并确认端口释放。

## 适用场景

批量发布到已登录的平台、往后台连续传文件、抓取需登录的页面结构、微信扫码登录;只要浏览器能登进去的站点都能接管。

如何安装此技能?

访问技能市场,点击「安装」按钮,按提示将技能包放入 AI 编程助手的 skills 目录即可。

浏览技能市场

支持平台:Qoder · QoderWork · Claude · Codex 等 AI 编程助手