Showing Posts From

Chrome

Chrome 扩展 Local Extension Settings 目录:.log 文件是 LevelDB WAL,不是文本日志

目录结构 在 Windows 上,Chrome/Edge/指纹浏览器的扩展本地存储位于: C:\Users\用户名\AppData\Local\Google\Chrome\User Data\Default\ └── Local Extension Settings\ └── mcohilncbfahbmgdjkbpemcciiolgcge\ ├── 000001.log ├── 008803.log ├── CURRENT ├── LOCK ├── LOG └── MANIFEST-000001这里的 mcohilncbfahbmgdjkbpemcciiolgcge 是扩展的 ID。 .log 文件不是文本日志 008803.log 这类文件是 LevelDB WAL(Write-Ahead Log) 文件,不是普通文本日志。 LevelDB 是 Chrome 用于存储扩展数据(chrome.storage.local、IndexedDB 等)的键值数据库。WAL 文件以二进制格式存储事务日志,内容通常是 protobuf 或 JSON 序列化后的数据。 用 xxd 或十六进制编辑器查看文件头: xxd -l 256 008803.log如果看到 JSON 结构或可读字符串,说明是纯文本 JSON 存储(部分插件会明文存)。如果是乱码二进制,则是 protobuf 等序列化格式。 如何确认扩展 ID 对应哪个插件 方法一:浏览器内查找(最快) chrome://extensions/开启右上角"开发者模式",每个插件卡片下方会显示 ID,搜索目标 ID 即可。 方法二:读取 manifest.json 同一个扩展 ID 目录下找扩展的安装目录: C:\...\Extensions\mcohilncbfahbmgdjkbpemcciiolgcge\1.x.x\manifest.json{ "name": "OKX Wallet", "description": "...", "version": "6.x.x" }方法三:查看 WAL 文件中的可读字符串 部分钱包插件(如 MetaMask、OKX Wallet)的数据会以 JSON 存储,可以直接在 WAL 文件里搜索关键词: strings 008803.log | grep -i "wallet\|token\|address"或者 Python 读取: with open("008803.log", "rb") as f: data = f.read() # 找 JSON 开始位置 start = data.find(b'{') if start != -1: print(data[start:start+500])常见扩展 ID 对应扩展 ID 扩展名nkbihfbeogaeaoehlefnkodbefgpgknn MetaMaskmcohilncbfahbmgdjkbpemcciiolgcge OKX Wallet(Web3)bfnaelmomeimhlpmgjnjophhpkkoljpa Phantomfhbohimaelbohpjbbldcngcnapndodjp Binance Wallet确认后,如果数据目录很大(几十 MB),说明插件本地存了大量数据,可能是交易历史、密钥材料或缓存。

Chromium 扩展的 Local Extension Settings 目录里是什么

Chromium 系浏览器(Chrome、Edge、Brave、指纹浏览器)的用户目录里经常能看到: Default/Local Extension Settings/<extension-id>/ ├── 000003.log ├── 008803.log ├── CURRENT ├── LOCK ├── LOG └── MANIFEST-000002看到 .log 会以为是普通日志文件——其实不是,这是 chrome.storage.local API 底层的 LevelDB 数据文件。 什么是 LevelDB WAL LevelDB 是 Google 出的嵌入式 KV 库,Chromium 用它存扩展的持久数据。文件角色:文件 含义000xxx.log 当前写入的 WAL(Write-Ahead Log)000xxx.ldb 已 compact 完毕的 SSTableCURRENT 指向最新的 MANIFESTMANIFEST-xxx 版本元信息LOG 真正的运行日志LOCK 进程锁.log 是二进制格式的 KV 追加写。用文本编辑器打开会看到一堆乱码 + 部分可见的 JSON 片段——那些片段是扩展存进去的数据。 扩展 ID 反查扩展名 看到目录名是一串 32 字符: mcohilncbfahbmgdjkbpemcciiolgcge想知道是哪个扩展,几个方法: 方法 1:浏览器里查 chrome://extensions/ 打开开发者模式,右上角就能看到每个扩展的 ID。搜刚才那串 ID 就找到了。 方法 2:读 manifest.json 扩展本体在: <UserDataDir>/Default/Extensions/<extid>/<version>/manifest.json打开 manifest: { "name": "OKX Wallet", "version": "3.x.x", "description": "..." }方法 3:看 .log 里的可见字符串 Windows: findstr /C:"name" 000003.logLinux/macOS: strings 000003.log | head -50有些扩展会把 name / apiUrl / host 直接明文写进 storage,能顺出线索。 用代码正经读它 不建议手工解析二进制。装 levelup + leveldown(Node),或者用 Python 的 plyvel: # pip install plyvel import plyveldb = plyvel.DB( r"C:\Users\me\AppData\Local\Google\Chrome\User Data\Default\Local Extension Settings\mcohilncbfahbmgdjkbpemcciiolgcge", create_if_missing=False, ) for k, v in db: print(k, "=", v[:200]) db.close()注意 Chrome 必须关掉,不然 LOCK 拿不到,Python 会报: plyvel._plyvel.IOError: Lock file existsNode 版: const level = require("level");const db = level("./Local Extension Settings/mcohilncbfahbmgdjkbpemcciiolgcge"); db.createReadStream() .on("data", ({ key, value }) => console.log(key.toString(), value.toString())) .on("end", () => db.close());里面存的是什么 看到的 JSON 值大多是扩展自己 chrome.storage.local.set 存的东西。举例:密码管理器 → 加密后的密码库 广告拦截器 → 规则订阅、白名单 翻译扩展 → 用户偏好、缓存 Web3 钱包(MetaMask / OKX / Phantom 等)→ 加密后的助记词、地址簿、dApp 授权列表Web3 钱包一般用 AES/PBKDF2 之类加密助记词,密码不知道也解不出。但指纹浏览器同步这些数据的时候,就是在拷贝这个目录里的 .log。 迁移扩展数据 想把一个 profile 里的扩展数据搬到另一个 profile,直接关掉 Chrome 后整目录拷过去: xcopy /E /I ^ "C:\Users\A\AppData\Local\Google\Chrome\User Data\Default\Local Extension Settings\<extid>" ^ "C:\Users\B\AppData\Local\Google\Chrome\User Data\Default\Local Extension Settings\<extid>"Extensions/<extid> 目录(扩展本体)也要一起拷才能真正生效。 一句话总结 Local Extension Settings/<extid>/000xxx.log = LevelDB 的 WAL 文件,不是文本日志。想看内容用 plyvel/level 打开数据库;想知道是哪个扩展就查 ID 或读 manifest.json。

Permissions Policy Violation: unload is not allowed 是什么

Chrome 控制台偶尔出现: [Violation] Permissions policy violation: unload is not allowed in this document.不是代码报错,是一条警告。 原因 Chrome 正在逐步废弃 unload 事件,因为注册了 unload 的页面无法进入 BFCache(Back/Forward Cache),会降低导航性能。某些页面策略或 iframe 设置下,unload 会被直接禁止,此时任何注册 unload 的代码都会打印这条 Violation。 常见触发场景:第三方 SDK(Sentry、埋点、广告脚本)注册了 unload 旧版 Citrix/Enterprise 插件 页面在 <iframe> 里,父页面设置了 Permissions-Policy: unload=()是否影响功能 通常不影响。[Violation] 只是警告,页面照常运行,unload 回调不会执行而已。如果你的业务逻辑依赖 unload(如发送离开事件),需要改用 visibilitychange 或 pagehide。 查看当前页面的 Permissions Policy 方法一:Network 响应头 打开 DevTools → Network,刷新页面,点击 HTML 文档请求 → Response Headers,查找: Permissions-Policy: unload=()方法二:控制台查询 // 查看 unload 是否被策略禁止 document.permissionsPolicy?.allowsFeature("unload")// 查看所有允许的特性 document.permissionsPolicy.allowedFeatures()// 抓取当前页面响应头 fetch(location.href) .then(r => console.log(r.headers.get("Permissions-Policy")))定位是谁注册了 unload // 查看 window 上注册了哪些 unload 监听器 getEventListeners(window).unload// 查看 beforeunload getEventListeners(window).beforeunload如果有结果,点击 listener 里的函数名可以跳转到 Sources 对应代码。 也可以在 Sources 全局搜索: addEventListener("unload" window.onunload beforeunload推荐替代方案 // 替代 unload:页面对用户不可见时触发 document.addEventListener("visibilitychange", () => { if (document.visibilityState === "hidden") { // 发送最后的数据 navigator.sendBeacon("/api/leave", payload); } });// 替代 beforeunload(不阻塞 BFCache) window.addEventListener("pagehide", (e) => { if (e.persisted) { // 页面进入 BFCache,不是真正关闭 } });navigator.sendBeacon 在页面隐藏时异步发送请求,不阻塞页面关闭,是发送离开事件的首选方案。 Chrome Extension 的本地存储 Chrome 扩展用 chrome.storage.local 存储的数据保存在: Windows: %LOCALAPPDATA%\Google\Chrome\User Data\Default\Local Extension Settings\<扩展ID> macOS: ~/Library/Application Support/Google/Chrome/Default/Local Extension Settings/<扩展ID>目录内容是 LevelDB 格式(.log、.ldb 文件),不是纯文本。查看方法: # 在扩展页面控制台读取(需要 Inspect views 权限) chrome.storage.local.get(null, console.log)或用 Python plyvel 解析: import plyveldb = plyvel.DB("/path/to/extension/storage", create_if_missing=False) for k, v in db: print(k.decode(errors="replace")) print(v.decode(errors="replace")) db.close()注意:读取时浏览器需要关闭,否则 LevelDB 文件被锁。

OpenCode Browser 扩展能做什么:与 Chrome DevTools MCP 的差异

两种方案的架构对比 OpenCode Browser(扩展方式) Chrome DevTools MCP(CDP 方式)OpenCode OpenCode ↓ ↓ Native Messaging MCP Server ↓ ↓ Chrome Extension Chrome DevTools Protocol (CDP) ↓ ↓ DOM / Tab 操作 完整 DevTools 能力OpenCode Browser 扩展连接方式更轻量,不需要打开 --remote-debugging-port,官方说明"No DevTools Protocol, no security prompts"。 OpenCode Browser 能做什么能力 支持导航(跳转 URL) ✅点击元素 ✅输入文本 ✅获取页面 DOM / 文本 ✅截图 ✅读取 <script> 标签内容 ✅读取 window.localStorage / sessionStorage 取决于扩展实现监听 Network 请求(XHR/fetch/WebSocket) ❌设置 JS 断点 ❌查看 Webpack/闭包内变量 ❌Memory Snapshot / Performance Profile ❌拦截/修改请求 ❌Chrome 扩展标准 API(chrome.scripting.executeScript)可以在页面上下文执行脚本,所以凡是挂在 window 上的对象(window.__NEXT_DATA__、window.__INITIAL_STATE__ 等)都可以读取。 但 webpack 闭包内的局部变量: ;(() => { const token = "abc123"; // ← 扩展拿不到这个 })();无论是 OpenCode Browser 还是普通扩展都无法直接访问,必须通过 DevTools Protocol 的 Runtime.evaluate 或断点能力才能拿到。 什么时候用 Chrome DevTools MCP 如果目标是:分析混淆 JS、逆向加密参数 Hook XHR / fetch / WebSocket 请求 查看 React/Vue 组件状态树 抓取动态签名、Token 设置断点、单步执行应该改用支持 CDP 的 MCP 方案,例如通过 --remote-debugging-port=9222 启动 Chrome,再连接 CDP: google-chrome --remote-debugging-port=9222 --user-data-dir=/tmp/debug-profile然后 MCP Server 通过 WebSocket 连接 ws://localhost:9222 调用 CDP 接口。 CDP 可以做到: Sources → 设置断点 Network → 查看所有请求/响应 Console → 执行任意 JS Memory → Heap Snapshot如果目标是指纹浏览器(Adspower 等) OpenCode Browser 扩展控制 Chrome 不能隐藏浏览器指纹(Canvas、WebGL、TLS、WebRTC 等),这些由浏览器本身决定。 需要多账号防关联的场景通常走另一条路: OpenCode / Playwright ↓ Adspower / MultiLogin 本地 API ↓ 指纹浏览器实例通过 Adspower 提供的 REST API 创建/启动指纹浏览器,再用 Playwright 连接其 CDP 端口操作页面。 小结 OpenCode Browser 扩展的定位是自动化操作真实浏览器(点击、填表、截图、爬内容),适合不需要深层 JS 调试的 RPA 和 AI Agent 场景。需要分析 JS 执行过程或拦截网络请求时,应切换到 CDP 方案。

Chrome 扩展数据存放位置:LevelDB 与 IndexedDB

Chrome 扩展的 chrome.storage.local 数据并不存在普通的 localStorage 里,而是存在用户数据目录下以 LevelDB 格式保存的独立数据库中。 数据存放路径 Windows # Chrome C:\Users\用户名\AppData\Local\Google\Chrome\User Data\Default\Local Extension Settings\# Edge C:\Users\用户名\AppData\Local\Microsoft\Edge\User Data\Default\Local Extension Settings\每个扩展一个子目录,目录名是扩展 ID: Local Extension Settings\ ├── nkbihfbeogaeaoehlefnkodbefgpgknn\ ← MetaMask │ ├── LOG │ ├── CURRENT │ ├── MANIFEST-000001 │ └── 000003.ldb └── cjpalhdlnbpafiamejdnhcphjbkeiagm\ ← uBlock Origin里面是 LevelDB 格式,不能直接用文本编辑器读取。 macOS ~/Library/Application Support/Google/Chrome/Default/Local Extension Settings/找到扩展 ID 打开 chrome://extensions/,开启右上角开发者模式,即可看到每个扩展的 ID(32 位小写字母)。 在 DevTools 中读取扩展数据 打开扩展的背景页(Service Worker):chrome://extensions/ → 点击扩展的"检查视图:Service Worker"或"background page" 在 DevTools Console 中:// 读取所有 storage.local 数据 chrome.storage.local.get(null, console.log);// 或 const data = await chrome.storage.local.get(); console.log(data);其他存储类型路径 Default\ ├── Local Extension Settings\ ← chrome.storage.local(LevelDB) ├── Extension State\ ← chrome.storage.session ├── IndexedDB\ ← IndexedDB │ └── chrome-extension_xxx.indexeddb.leveldb └── Session Storage\ ← sessionStorageAdsPower 多配置文件批量同步扩展数据 AdsPower 的每个浏览器配置文件存在各自的 cache 目录下,批量同步扩展 LevelDB 数据: import json import shutil import sys from pathlib import Path from datetime import datetime from tqdm import tqdmCONFIG_FILE = Path("config.json") EXT_DIR = "mcohilncbfahbmgdjkbpemcciiolgcge" # 扩展 IDDEFAULT_CONFIG = { "src": r"D:\.ADSPOWER_GLOBAL\cache", "dst": r"D:\ads\.ADSPOWER_GLOBAL\cache" }if not CONFIG_FILE.exists(): with open(CONFIG_FILE, "w", encoding="utf-8") as f: json.dump(DEFAULT_CONFIG, f, indent=4, ensure_ascii=False) print("已创建 config.json,请修改后重新运行") sys.exit(0)with open(CONFIG_FILE, "r", encoding="utf-8") as f: config = json.load(f)src_root = Path(config["src"]) dst_root = Path(config["dst"])# 找出所有包含目标扩展数据的配置文件目录 tasks = [ d for d in src_root.iterdir() if d.is_dir() and (d / "Default" / "Local Extension Settings" / EXT_DIR).exists() ]print(f"发现 {len(tasks)} 个配置需要同步")for index, cache_dir in enumerate(tasks, 1): src = cache_dir / "Default" / "Local Extension Settings" / EXT_DIR dst = dst_root / cache_dir.name / "Default" / "Local Extension Settings" / EXT_DIR print(f"\n[{index}/{len(tasks)}] {cache_dir.name}") # 备份旧目录(加时间戳,不直接删除) if dst.exists(): backup = dst.parent / f"{dst.name}_backup_{datetime.now():%Y%m%d_%H%M%S}" dst.rename(backup) # 只保留最近 5 个备份 backups = sorted(dst.parent.glob(f"{dst.name}_backup_*"), key=lambda p: p.stat().st_mtime, reverse=True) for old in backups[5:]: shutil.rmtree(old) dst.mkdir(parents=True, exist_ok=True) files = [f for f in src.rglob("*") if f.is_file()] with tqdm(total=len(files), desc=cache_dir.name, unit="file", ncols=100) as pbar: for file in files: target = dst / file.relative_to(src) target.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(file, target) pbar.update(1)print("\n全部同步完成")关键点:shutil.rmtree 太暴力,改用 rename 加时间戳备份 保留最近 5 个备份,自动清理旧版本 tqdm 显示每个目录的文件级复制进度打包成独立 exe: pip install pyinstaller tqdm pyinstaller -F sync_ext.py

Playwright 接管本地 Chrome:四种方式对比

Playwright 默认下载并使用自带的 Chromium。想用本机装的 Google Chrome(自动化登录时保留 cookie、复用插件),有四种方式,各自适用场景不同。 方式一:channel = "chrome" 最省事——channel: "chrome" 告诉 Playwright 用系统装的稳定版 Chrome: from playwright.sync_api import sync_playwrightwith sync_playwright() as p: browser = p.chromium.launch(channel="chrome", headless=False) page = browser.new_page() page.goto("https://example.com") input("回车退出") browser.close()优点:跨平台一行搞定 局限:不会用你日常那个 profile,不带 cookie、不带插件其它可选 channel:msedge、chrome-beta、chrome-dev、msedge-beta。 方式二:executable_path 指定路径 Chrome 装在非默认路径,或者想用 Chromium 兼容分发(Brave、Edge、Vivaldi): Windows: browser = p.chromium.launch( executable_path=r"C:\Program Files\Google\Chrome\Application\chrome.exe", headless=False, )macOS: browser = p.chromium.launch( executable_path="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", headless=False, )Linux: browser = p.chromium.launch( executable_path="/usr/bin/google-chrome", headless=False, )跟方式一一样,不带 cookie / 插件——只是换了浏览器可执行文件。 方式三:launchPersistentContext 复用 profile 想让 Playwright 用日常那个 Chrome 用户目录(复用登录态、书签、扩展): browser = p.chromium.launch_persistent_context( user_data_dir=r"C:\Users\<你>\AppData\Local\Google\Chrome\User Data", channel="chrome", headless=False, ) page = browser.pages[0] if browser.pages else browser.new_page() page.goto("https://example.com")注意:这个方式要求 Chrome 没在运行,否则报: Failed to launch: browser closed. Profile is already in use解法:关掉所有 Chrome 窗口再跑 或者复制一份 profile 出来单独用:xcopy "C:\...\User Data" "D:\pw-profile" /E /I想只用某个 Profile(不是 Default): browser = p.chromium.launch_persistent_context( user_data_dir=r"D:\pw-profile", args=["--profile-directory=Profile 1"], channel="chrome", headless=False, )方式四:CDP 连接已启动的 Chrome(推荐) 方式三最大的痛点是"Chrome 必须关闭"。想在 Chrome 正常开着的情况下让 Playwright 接管它,走 CDP(Chrome DevTools Protocol)。 第一步:以 debug 模式启动 Chrome,指定端口: "C:\Program Files\Google\Chrome\Application\chrome.exe" ^ --remote-debugging-port=9222 ^ --user-data-dir="D:\pw-chrome-profile"--user-data-dir 指向一个专用目录,避免和日常 Chrome 抢占用。 第二步:Playwright 连上去: from playwright.sync_api import sync_playwrightwith sync_playwright() as p: browser = p.chromium.connect_over_cdp("http://127.0.0.1:9222") ctx = browser.contexts[0] page = ctx.pages[0] if ctx.pages else ctx.new_page() page.goto("https://example.com")优势:Chrome 一直开着,做完自动化 Playwright 断开就行 保留登录态、插件、cookie 可以边脚本操作边人工插手(做半自动化)生产爬虫、账号自动登录场景基本用这个。 四种方式对比方式 Chrome 需关闭 保留 profile 使用场景channel="chrome" 否 否 简单自动化,无需登录态executable_path=... 否 否 用其它 Chromium 分发launch_persistent_context 是 是 一次性用现有 profileconnect_over_cdp (CDP) 否 是 推荐:常驻 Chrome + 脚本联动顺带:无痕、隐身、下载路径 无痕模式: browser = p.chromium.launch(channel="chrome", args=["--incognito"])指定下载目录: context = browser.new_context(accept_downloads=True) page = context.new_page() async with page.expect_download() as info: await page.click("a#download") download = await info.value await download.save_as("D:/downloads/file.zip")忽略证书错误(内网自签证书): context = browser.new_context(ignore_https_errors=True)一句话总结 普通自动化用 channel="chrome"、要复用登录态用 CDP 连接(先手动启动 Chrome + --remote-debugging-port=9222)。launch_persistent_context 只在 Chrome 关闭时才能用。

Chrome CDP 远程调试:--remote-debugging-port 与 Node.js Hook 框架

CDP 远程调试 启动浏览器并开放调试端口 # Chrome chrome.exe --remote-debugging-port=9222# Edge(建议指定独立 user-data-dir) msedge.exe --remote-debugging-port=9222 --user-data-dir=D:\edge_debug获取 WebSocket 调试地址 浏览器启动后,访问: http://127.0.0.1:9222/json返回当前所有 Tab 的信息,其中 webSocketDebuggerUrl 是关键字段: { "id": "ABC123", "title": "test page", "webSocketDebuggerUrl": "ws://127.0.0.1:9222/devtools/page/ABC123" }连接 DevTools 前端 devtools://devtools/bundled/devtools_app.html?ws=127.0.0.1:9222/devtools/page/ABC123整体链路:--remote-debugging-port 开放 CDP 端口 → /json 获取 ws 地址 → DevTools 前端通过 ws 接入。 Node.js Hook 三件套 在 Node 程序里注入以下 hook 可以拦截运行时数据流。 Hook JSON.parse(记录解析数据) const fs = require('fs'); const _parse = JSON.parse;JSON.parse = function (text, reviver) { const result = _parse.call(this, text, reviver); // 只保存含 "raw" key 的对象 if (result && typeof result === 'object' && 'raw' in result) { fs.appendFileSync('./json_log.txt', JSON.stringify({ time: Date.now(), data: result }) + '\n'); } return result; };过滤条件也可以改为 /"raw"\s*:/.test(text) 在原始字符串层面筛选,减少反序列化开销。 Hook TextDecoder.decode(截获二进制转字符串) const { TextDecoder } = require('util'); const _decode = TextDecoder.prototype.decode;TextDecoder.prototype.decode = function (input, options) { const result = _decode.call(this, input, options); console.log('[TextDecoder]', result); return result; };适合抓 WebSocket 二进制帧、CDP 协议数据等场景。注意 Buffer.toString('utf-8') 走的是另一条路径,需要单独 hook Buffer.prototype.toString。 Hook AES 加密(抓密钥与明文) const crypto = require('crypto'); const _createCipheriv = crypto.createCipheriv;crypto.createCipheriv = function (algorithm, key, iv, options) { console.log('[AES key]', { algorithm, key: key?.toString?.('hex'), iv: iv?.toString?.('hex') }); const cipher = _createCipheriv.call(this, algorithm, key, iv, options); const _update = cipher.update; cipher.update = function (data) { console.log('[AES plaintext]', data?.toString?.() ?? data); return _update.apply(this, arguments); }; return cipher; };Hook createDecipheriv 的结构完全对称,可以抓解密后的明文。 注意事项JSON.parse 被局部引用绕过时(const p = JSON.parse; p(...)),改 global.JSON.parse 更彻底 大流量场景建议加过滤条件(text.length < 5000)避免日志刷屏 Node.js 版本 >= 11 才有全局 TextDecoder,旧版用 require('util').TextDecoder

Python 自动安装 Chrome 插件:策略强制 vs 开发者模式加载

写脚本自动帮机器装个 Chrome 插件——听着简单。Chrome 团队故意不让你静默装本地 crx(安全策略,不是技术问题)。想绕开有几条现实可行的路。 路线一:企业策略强制安装(Web Store 上架的插件) Chrome 支持通过 Windows 注册表 / macOS plist / Linux JSON 策略,指定"必须自动装某扩展"。这是 IT 部门批量部署的标准做法。 前提是扩展在 Chrome Web Store 上架——策略只接受 update URL 拉取,不认本地 crx。 Windows 注册表方式 import winregEXT_ID = "aapocclcgogkmnckokdopfmhonfmgoek" # 举例:Google Docs Offline UPDATE_URL = "https://clients2.google.com/service/update2/crx"path = r"SOFTWARE\Policies\Google\Chrome\ExtensionInstallForcelist"key = winreg.CreateKey(winreg.HKEY_LOCAL_MACHINE, path) winreg.SetValueEx(key, "1", 0, winreg.REG_SZ, f"{EXT_ID};{UPDATE_URL}") winreg.CloseKey(key)写完重启 Chrome,扩展会自动安装、无法卸载。 注意:需要管理员权限 ExtensionInstallForcelist 的编号("1"、"2" ...)要不重复 只支持 Web Store 上架的扩展;企业内部扩展需要自建 update.xml配套删除: import winreg key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, path, 0, winreg.KEY_WRITE) winreg.DeleteValue(key, "1")路线二:Selenium 启动时加载本地插件 不做正式安装,只是启动 Chrome 时把插件目录加载进来——最适合自动化脚本、爬虫、批量测试。 加载解压后的目录(源码形式): from selenium import webdriver from selenium.webdriver.chrome.options import Optionsopts = Options() opts.add_argument(r"--load-extension=D:\extensions\my-plugin")driver = webdriver.Chrome(options=opts) driver.get("https://example.com")加载已打包的 crx: opts = Options() opts.add_extension(r"D:\extensions\my-plugin.crx")局限:不会真的"安装",关闭浏览器就没了 需要 Selenium 控制的 Chrome,不影响用户日常浏览器 Chrome 每次启动会有"以开发者模式使用未打包扩展"警告条路线三:Playwright 加载扩展 Playwright 加载扩展要用 persistent context,且必须用 Chromium(Chrome 分发版不能通过所有测试): from playwright.sync_api import sync_playwrightwith sync_playwright() as p: ctx = p.chromium.launch_persistent_context( user_data_dir="D:/tmp/pw-profile", headless=False, args=[ "--disable-extensions-except=D:\\extensions\\my-plugin", "--load-extension=D:\\extensions\\my-plugin", ], ) page = ctx.new_page() page.goto("https://example.com")不推荐的路线 UI 自动化拖 crx 到 chrome://extensions/:Chrome 会弹确认框、快捷键会变、chrome 版本升级后经常挂。别在这个方向上死磕。 改本地 Extensions 目录里的 Preferences JSON:Chrome 会检测校验和,第二次启动直接把你写进去的扩展禁用掉。 场景对应目的 用什么公司/网吧批量部署 Web Store 扩展 策略强制安装爬虫 / 自动化脚本用自己写的扩展 Selenium 加载需要长期挂着扩展,用户不能卸载 策略强制只是测试自己开发中的插件 Chrome 开发者模式手动一句话总结 要"真安装"就上企业策略(Web Store 扩展),要"临时加载"就 Selenium/Playwright --load-extension。Chrome 就是不让你静默装本地 crx,别硬撞。

JS 动态注入 CSS 样式:setStyle 函数

在油猴脚本或 Chrome 扩展里需要动态修改页面样式时,直接写 element.style.xxx 只能改行内样式,无法用 CSS 选择器批量设置。更好的方案是动态创建 <style> 标签。 setStyle 函数 function setStyle(id, css) { let style = document.getElementById(id); if (!style) { style = document.createElement("style"); style.id = id; document.head.appendChild(style); } style.textContent = css; }用 id 做唯一标识——如果该 id 的 <style> 已存在就直接更新内容,不会重复插入。 使用示例 // 隐藏广告 setStyle("ad-blocker", ` .ad-banner, .sidebar-ad, [class*="advertisement"] { display: none !important; } `);// 强制暗色模式 setStyle("dark-mode", ` body { background: #1a1a1a !important; color: #e0e0e0 !important; } a { color: #7eb8f7 !important; } `);// 动态更新(复用同一个 style 元素) setStyle("custom-theme", `body { font-size: ${fontSize}px; }`);为什么不用 innerHTML // ❌ 安全风险:可能被 XSS 利用 style.innerHTML = css;// ✅ 推荐:textContent 不会解析 HTML style.textContent = css;textContent 只设置文本内容,不会解析 HTML 标签,比 innerHTML 更安全。 移除样式 function removeStyle(id) { const el = document.getElementById(id); if (el) el.remove(); }removeStyle("dark-mode");油猴脚本中的注意事项 油猴脚本默认在 document-end 注入,此时 document.head 已存在。如果设置了 @run-at document-start,需要等待 head 可用: // @run-at document-start function setStyle(id, css) { const target = document.head || document.documentElement; let style = document.getElementById(id); if (!style) { style = document.createElement("style"); style.id = id; target.appendChild(style); } style.textContent = css; }document.documentElement(即 <html>)在 document-start 阶段就已存在,可以作为 fallback。 GM_addStyle(油猴内置 API) Tampermonkey 提供了 GM_addStyle 作为快捷方式: // @grant GM_addStyle GM_addStyle(` .target-element { color: red; } `);不需要去重处理,但无法动态更新——每次调用都会新增一个 <style> 标签。频繁更新时还是用自定义的 setStyle。

Plasmo 浏览器扩展项目 tsconfig 与 `~` 别名

用 Plasmo 写 Chrome 扩展项目,脚手架给的 tsconfig.json 是这样: { "extends": "plasmo/templates/tsconfig.base", "exclude": ["node_modules"], "include": [ ".plasmo/index.d.ts", "./**/*.ts", "./**/*.tsx" ], "compilerOptions": { "baseUrl": ".", "paths": { "~*": ["./*"] } } }一份不到 20 行,但每一行都干活。 每行在做什么extends: "plasmo/templates/tsconfig.base" — 继承 Plasmo 的严格模式基线(strict、esModuleInterop、jsx: react 等),只覆盖必要项 exclude: ["node_modules"] — 不给 TS 检查依赖 include: [".plasmo/index.d.ts", ...] — 包含 Plasmo 生成的类型声明和项目源码 baseUrl: "." — 相对根目录解析 paths: { "~*": ["./*"] } — ~utils 会被解析成 ./utils想把代码搬进 src/ Plasmo 默认允许代码散在根目录(popup.tsx、background.ts 直接扔外面)。你想集中放 src/ 下,tsconfig 需要相应改: { "extends": "plasmo/templates/tsconfig.base", "exclude": ["node_modules"], "include": [ "src/**/*.ts", "src/**/*.tsx", ".plasmo/index.d.ts" ], "compilerOptions": { "baseUrl": "src", "paths": { "~*": ["./*"], "@*": ["./*"] } } }三处变化:include 从 ./** 改成 src/** baseUrl 从 . 改成 src paths 里 ~ 和 @ 都指到 src 根之后代码里: import Button from "~/components/Button"; import { fetchUser } from "@/api/user";会被解析为 src/components/Button.tsx 和 src/api/user.ts。 Plasmo 的 url: 前缀 Plasmo 基于 Parcel 打包,导入 HTML 或图片资源有个特殊语法: import fontPickerHTML from "url:./panels/font-picker/index.html";url: 前缀告诉 Parcel"不要把这个当代码解析、当静态资源处理,导出打包后的 URL"。编译产物大致是: const fontPickerHTML = "/assets/font-picker.abc123.html";用途:chrome.windows.create({ url: fontPickerHTML }) 打开一个扩展页面。 代码搬进 src 后要配合修改: import fontPickerHTML from "url:~/panels/font-picker/index.html";因为 ~ 现在指向 src/。 除了 url: 还有 Parcel 支持多个前缀,Plasmo 都能用:url: — 当资源,返回 URL 字符串 data-url: — 编码成 base64 data URL raw: — 读文件原始内容为字符串 bundle: — 把整个 JS 打成单独 bundle data-text: — 直接嵌入文本例如把某个 css 内联进背景 script: import css from "raw:~/styles/inject.css"; chrome.scripting.insertCSS({ target: { tabId }, css });Chrome 扩展 tsconfig 常见字段 Plasmo base 已经配好,但了解一下: { "compilerOptions": { "target": "ES2020", "module": "ESNext", "moduleResolution": "Bundler", "jsx": "react", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "types": ["chrome"] // 让 chrome.* API 有类型 } }types: ["chrome"] 是关键——扩展项目里 chrome.storage、chrome.runtime 得靠它。Plasmo base 里默认带了。 一句话总结 Plasmo 的 ~ 别名跟着 baseUrl 走:搬到 src/ 就把 baseUrl 改成 src、include 换成 src/**。url: 前缀是 Parcel 特有的静态资源导入,改路径时别忘了同步。