用远程调试端口自动化 Chrome 应用商店发布:做法、三个真坑,以及你必须知道的风险

起因:常规浏览器自动化在这个域名下直接失效

我平时驱动浏览器用的是一套基于扩展的方案,底层走 chrome.debugger API。它在绝大多数站点都好用,但在 Chrome 应用商店开发者后台上,第一条命令就被拒了:

复制代码
CDP error -32000: Not allowed

域名是 chrome.google.comchromewebstore.google.com

这里要说清楚一件事,否则很容易得出错误结论:这不是"这个站点禁止自动化",而是 Chrome 禁止扩展的 chrome.debugger API 附加到 Google 自己的这几个高权限域名上。 这是对扩展 API 的域名级限制,防的是恶意扩展提权。换一条不经过扩展的路径------从外部用裸 CDP 连一个独立启动的 Chrome------完全正常。

所以方案就确定了:单独起一个带远程调试端口的 Chrome,用原始 CDP 协议驱动它。

一、启动:一个独立进程 + 一个全新的空 profile

bash 复制代码
"C:\Program Files\Google\Chrome\Application\chrome.exe" \
  --remote-debugging-port=9333 \
  --user-data-dir="D:\tmp\cdp-profile"

两个参数都是必须的,而且 --user-data-dir 必须指向一个全新的、空的目录。

很多教程会教你把它指向自己日常 Chrome 的 profile 目录,好处是"省得重新登录"。不要这么做,两个原因:

  1. 两个 Chrome 进程同时操作同一个 profile 目录,有损坏 profile 的风险。 你的书签、密码库、扩展状态都在里面,为了省一次登录去赌这个,不划算。
  2. 更重要的是安全边界。 后面会展开:调试端口等于把这个浏览器的完全控制权敞开在本机。敞开一个只登录了一个开发者账号的空浏览器,和敞开一个装着你全部身份的日常浏览器,是两件完全不同的事。

顺带说一句,较新版本的 Chrome 已经直接禁止对默认用户数据目录启用远程调试了------你把 --remote-debugging-port 指向默认 profile,它会静默忽略这个参数。这个改动本身就说明了 Google 怎么看待这个风险。

代价是新窗口什么都没登录。登录这一步交给人做,别自己去填账号密码------这是原则问题,不是能力问题。等用户在那个窗口登录完开发者账号,再导航到后台继续。

二、下手之前先确认端口是谁的

9333 这种顺手的端口很容易撞车:上一次会话残留的进程、别的自动化工具管理的浏览器,都可能已经在监听。这时候你以为在操作自己的窗口,实际在操作别人的。

bash 复制代码
curl http://127.0.0.1:9333/json/list

看返回里 tab 的 titleurl 是不是你预期的那个,确认了再动手。这一步只花两秒,但省掉的是"我刚才到底改了谁的页面"这种问题。

三、裸 CDP 的最小骨架

CDP 就是一个 JSON-RPC over WebSocket:GET /json/list 拿到目标列表,挑出你要的那个 tab,连它的 webSocketDebuggerUrl,然后发消息、按 id 收响应。

js 复制代码
// cdp.mjs  ---  node cdp.mjs <eval|nav|text> [arg]
const PORT  = process.env.CDP_PORT  || 9333;
const MATCH = process.env.CDP_MATCH || 'devconsole';

const list = await (await fetch(`http://127.0.0.1:${PORT}/json/list`)).json();
const t = list.find(x => x.type === 'page' && x.url.includes(MATCH))
       || list.find(x => x.type === 'page');
if (!t) { console.log('NO TARGET'); process.exit(1); }

const ws = new WebSocket(t.webSocketDebuggerUrl);
let id = 0; const pend = new Map();
const send = (m, p = {}) => new Promise((res, rej) => {
  const i = ++id; pend.set(i, { res, rej });
  ws.send(JSON.stringify({ id: i, method: m, params: p }));
});
ws.onmessage = e => {
  const m = JSON.parse(e.data);
  if (m.id && pend.has(m.id)) {
    const p = pend.get(m.id); pend.delete(m.id);
    m.error ? p.rej(new Error(JSON.stringify(m.error))) : p.res(m.result);
  }
};
const evalx = async (expr) => {
  const r = await send('Runtime.evaluate', {
    expression: expr, returnByValue: true, awaitPromise: true, userGesture: true
  });
  if (r.exceptionDetails) throw new Error(r.exceptionDetails.exception?.description || 'eval error');
  return r.result.value;
};

ws.onopen = async () => {
  const [cmd, ...rest] = process.argv.slice(2);
  const arg = rest.join(' ');
  try {
    if (cmd === 'nav')       { await send('Page.navigate', { url: arg });
                               await new Promise(r => setTimeout(r, 4000));
                               console.log('url:', await evalx('location.href')); }
    else if (cmd === 'text') { console.log(await evalx(`document.body.innerText.slice(0,${arg || 4000})`)); }
    else if (cmd === 'eval') { const v = await evalx(arg);
                               console.log(typeof v === 'string' ? v : JSON.stringify(v, null, 1)); }
  } catch (e) { console.log('ERR:', e.message); }
  ws.close(); process.exit(0);
};
setTimeout(() => { console.log('timeout'); process.exit(1); }, 40000);

一个实践建议:把要执行的 JS 写成 .mjs 脚本用 node 跑,不要在 shell 里拼几百个字符的表达式。 所有用户提供的文本(描述、隐私说明)都用 JSON.stringify() 序列化进去,别手工转义引号------手工转义是这类脚本最常见的翻车点。

userGesture: true 值得单独说:有些按钮的处理逻辑要求"来自用户手势",加上这个参数 CDP 才会把 Runtime.evaluate 标记成用户触发。

四、Web Store 表单的三个真坑

坑 1:标题和摘要不是输入框,是从 manifest 读的

控制台里那两个字段标着 "Title from package" / "Summary from package"。它们不可编辑。

要改标题或短描述的 SEO 措辞,只能改 manifest.json、重新打包、走 "Upload new package" 重传。别在页面上翻找输入框------没有。

坑 2:React 输入框必须用原生 setter

直接 el.value = text 对 React 受控组件是无效的:DOM 上看着变了,框架的状态没变,保存时写回去的还是旧值。而且这个失败是静默的,页面上看起来完全正常。

js 复制代码
const setter = Object.getOwnPropertyDescriptor(
  window.HTMLTextAreaElement.prototype, 'value'
).set;
setter.call(textarea, text);
textarea.dispatchEvent(new Event('input',  { bubbles: true }));
textarea.dispatchEvent(new Event('change', { bubbles: true }));
textarea.dispatchEvent(new Event('blur',   { bubbles: true }));

input 是给 React 的,changeblur 是给各种校验/自动保存逻辑的,三个都要发。

mat-select 这类下拉:点开 → 等大约 1.2 秒让选项列表渲染 → 按选项文本精确或包含匹配点击 mat-option / [role=option]。第一次跑一个陌生下拉时,让脚本在匹配不到的时候把所有可选项打印出来,比截图找选项快得多。

坑 3:单选按钮必须按 label 文本匹配

隐私页有个"是否使用远程代码"的 Yes/No。用一个宽泛的 input[type=radio] 选择器去点第 N 个------我这边连续点错过好几次,本意选 No,实际选中了 Yes。

正确做法是按可见 label 文本匹配(比如包含 "No, I am not using remote code"),点完之后再读回 {value, checked} 验证,而不是相信"点击返回成功"。

这条其实可以推广成一条通则:凡是"选错了也不会报错"的控件,都必须点完回读。

五、隐私页的四件事

  • Single purpose descriptionhost permission justification :各 ≤1000 字符,用上面的原生 setter 写。页面上通常正好两个 textarea[maxlength="1000"],顺序是单一用途在前、权限说明在后------但先按数量确认一遍再按下标写。
  • Remote code 单选:见坑 3。
  • 12 个数据收集类型复选框 :零数据收集的扩展全部不勾。别默认它们本来就是空的------数一遍已勾选数量是否为 0
  • 隐私政策 URL:普通文本框,正常填。

六、硬停止:最后三个复选框和 Submit 不要自动点

表单末尾有三个认证复选框(不出售数据 / 不用于无关用途 / 不用于信用评估)和一个 Submit for review

我的规则是:这四个控件一律不由脚本点。

原因不是技术上做不到,是这一步的性质变了。点下去之后,条目进入 Google 的审核队列;而且提交弹窗里那个"审核通过后自动发布"的复选框默认是勾上的------也就是说,一次随手的提交会把"上架前最后一次人工确认"这个环节直接跳过去。

把其他所有字段填满,让条目停在 Draft 状态,然后明确告诉用户还剩哪几步需要他手动完成。填完表单不等于获得了提交授权。

另外:控制台里没有删除草稿条目的功能,只有溢出菜单里的 "Archive..."。如果不小心点 "New item" 建出了重复条目(我干过),归档是唯一的清理方式,而且归档前也该问一句。

七、风险清单:开一个调试端口到底意味着什么

这部分才是这篇文章真正想说的。

远程调试端口是一个没有任何认证的完全控制接口。 任何能访问 127.0.0.1:9333 的本地进程,不需要密码、不需要授权、不需要用户点确认,就可以:

  • 在任意已打开页面里执行任意 JavaScript
  • 读取这些页面的 DOM、localStorage、sessionStorage
  • 通过 Network 域读取请求和响应,包括 Cookie 头
  • 任意导航、开新标签页、下载文件
  • 以你的登录身份发起任何站内操作

换句话说,端口开着的这段时间,这个浏览器里所有登录态都等于是公开的------对本机任何进程而言。

具体到这个场景,那个浏览器里登录的是 Chrome Web Store 开发者账号:一个能修改、能发布扩展给成千上万用户的账号。这是个高价值目标。

几条具体的:

风险 说明 怎么办
本地进程完全接管 端口无认证,任何本地程序可连 只在任务期间开,任务结束立刻关进程
日常 profile 暴露 如果指向真实 profile,暴露的是你全部身份 永远用全新空目录;新版 Chrome 也已禁止对默认目录开调试
profile 损坏 两个 Chrome 进程共用一个目录 同上
端口撞车 你以为在操作自己的窗口 动手前 GET /json/list 核对 title/url
残留 profile 目录 目录里存着开发者账号的会话 Cookie 任务结束后删掉整个 cdp-profile 目录
端口忘了关 任务结束进程还在跑,端口一直开着 收尾时确认进程已退出、端口已释放
网络暴露 默认只绑 127.0.0.1 绝不要--remote-debugging-address=0.0.0.0,那等于把浏览器控制权开到局域网

还有一点容易被忽略:调试端口并不受同源策略约束。 页面里的 JS 访问不到它(Chrome 对 Host 头做了校验来防 DNS rebinding),但本机上的任何普通程序都能直连。所以威胁模型里的"攻击者"不是网页,是本机上你没审计过的那些进程。

一套最小化的收尾动作

bash 复制代码
# 1. 确认进程退出
taskkill /F /IM chrome.exe /FI "COMMANDLINE eq *9333*"   # 或直接关掉那个窗口

# 2. 确认端口释放
curl http://127.0.0.1:9333/json/list    # 应该连不上

# 3. 删掉带登录态的临时 profile
rmdir /S /Q D:\tmp\cdp-profile

第 3 步最容易忘,但它是三步里最重要的:那个目录里躺着一份你的开发者账号会话。

小结

  • 扩展方案在 Google 自家高权限域名上被封,是对 chrome.debugger 的域名级限制,不是"此处禁止自动化";裸 CDP 可以绕开。
  • 独立进程 + 全新空 profile,不要碰日常 profile。
  • 动手前核对端口归属,动手时用脚本文件而不是 shell 拼串。
  • 三个真坑:标题来自 manifest、React 输入要原生 setter、单选按钮按 label 匹配并回读。
  • 最后的认证复选框和 Submit 不自动点------因为"自动发布"默认勾选,这一步跳过的是人工确认。
  • 调试端口 = 无认证的完全控制权。用完就关,profile 目录记得删。

自动化能省掉的是重复劳动,省不掉的是判断。把不可逆的那一下留给人,是这套流程里唯一不能妥协的设计。

相关推荐
懂软件的胡子个哥2 小时前
微信机器人为什么需要“回复候选”而不是所有 AI 内容直接发送
运维·微信·自动化·wechatapi·个人微信号二次开发
jingli93 小时前
多平台多店铺客服怎么统一接待?千牛/拼多多/抖音/京东/闲鱼/微信/快手切窗口太累,一个后台集中接管的七步落地方案(2026 实操版)
人工智能·自动化·用户运营
坚定信念,勇往无前3 小时前
Playwright 的本质
自动化·网络爬虫
乐橙开放平台21 小时前
从「多套客户端」到一套开放能力:乐橙视频监控能力复盘
笔记·物联网·自动化·音视频·智能家居
志栋智能1 天前
集成是关键:让巡检超自动化融入现有工具链
运维·服务器·数据库·架构·自动化
AI智能从业者1 天前
数码家电客服机器人怎么选?客户问参数问题机器人能不能接住
运维·人工智能·自动化
骑着蜗牛撵大象3271 天前
微信远程指挥电脑:用Hermes Agent构建本地AI执行引擎实战指南
机器人·自动化·es
Liaiyang661 天前
# 自研 AST 容错初筛工具:横向评测 Django、PyTorch、TensorFlow 三大开源 Python 框架异常收容风险
python·测试工具·自动化·开源软件·代码规范·devops·代码复审