在使用 Google Antigravity IDE 或其他基于 Playwright 的浏览器自动化工具时,遇到一个困扰的问题:下载的文件被自动重命名为 UUID 格式,并保存到临时目录,这导致博主在下载东西时无法自动化,反而降低了效率。本文深入分析这个问题的原因,并提供详细的解决方案。
问题现象
当使用 Antigravity IDE 控制 Chrome 浏览器下载文件时,出现以下异常行为:
- 文件名变成 UUID:如
f6746de9-95f6-4c06-b636-3ac7c2fb73bc,而非原始文件名 - 保存位置异常:文件被保存到
%LOCALAPPDATA%\Temp\playwright-artifacts-xxxxx临时目录 - 每次会话独立:每次启动浏览器都会创建新的临时目录
问题原因分析
Playwright 的下载拦截机制
Playwright 是一个强大的浏览器自动化框架,被许多 IDE 和测试工具采用(如 Google Antigravity)。它默认会拦截所有下载操作,这是其核心设计特性。
通过分析 Playwright 源码 crBrowser.ts,可以找到问题的根源:
1
2
3
4
5
6
7
8
9
// CRBrowserContext._initialize() 中的代码
if (this._options.acceptDownloads !== 'internal-browser-default') {
promises.push(this._browser._session.send('Browser.setDownloadBehavior', {
behavior: this._options.acceptDownloads === 'accept' ? 'allowAndName' : 'deny',
browserContextId: this._browserContextId,
downloadPath: this._browser.options.downloadsPath,
eventsEnabled: true,
}));
}
CDP Browser.setDownloadBehavior 参数解析
Playwright 使用 Chrome DevTools Protocol (CDP) 的 Browser.setDownloadBehavior 命令来控制下载行为:
| behavior 值 | 行为描述 | 文件名 | downloadPath |
|---|---|---|---|
deny |
拒绝所有下载 | N/A | 不需要 |
allow |
允许下载,使用原始文件名 | 原始名 ✅ | 必须指定 |
allowAndName |
允许下载,使用 GUID 命名 | UUID ❌ | 必须指定 |
default |
浏览器默认行为 | 原始名 ✅ | 可省略 ✅ |
关键区别:
allow:虽然保留原始文件名,但必须指定downloadPath,文件仍会保存到指定目录default:使用浏览器原生下载行为,可以省略downloadPath,文件保存到浏览器默认下载目录
问题根源:Playwright 硬编码使用了 allowAndName 模式,这是有意设计,目的是:
- 防止多次下载时的文件名冲突
- 确保自动化测试的隔离性
- 便于 Playwright 追踪和管理下载
解决方案
修改 Playwright 源码
找到并修改 Playwright 核心文件:
文件路径:
1
%LOCALAPPDATA%\ms-playwright-go\1.50.1\package\lib\server\chromium\crBrowser.js
修改内容:将 allowAndName 改为 default,并移除 downloadPath 参数
1
2
3
4
5
- behavior: this._options.acceptDownloads === 'accept' ? 'allowAndName' : 'deny',
- browserContextId: this._browserContextId,
- downloadPath: this._browser.options.downloadsPath,
+ behavior: this._options.acceptDownloads === 'accept' ? 'default' : 'deny',
+ browserContextId: this._browserContextId,
说明:使用
default行为(浏览器默认行为),Playwright 将不再拦截下载,文件会直接保存到浏览器配置的默认下载目录。[!WARNING] 千万不要使用
allow且不指定路径:CDP 协议规定behavior: 'allow'时必须提供downloadPath。如果移除downloadPath但仍使用'allow',会导致 CDP 命令参数校验失败,进而导致 Playwright 无法启动浏览器(报错或卡在 invoke 阶段)。
重要提示:修改后需要重启 Antigravity IDE(或相关应用程序)才能生效。验证步骤:
- 关闭所有由 Antigravity 控制的 Chrome 浏览器窗口
- 完全退出 Antigravity IDE
- 重新启动 Antigravity
- 测试下载功能
相关 GitHub Issues
| Issue | 描述 | 状态 |
|---|---|---|
| #32692 | 请求支持用户指定的下载文件名 | Open |
| #7464 | UUID 文件名是否是预期行为 | Closed |
| #2058 | 允许使用自然浏览器文件名 | Closed |
不知道修改哪个路径的 crBrowser.js?
有时候,需要从 Claude Code 通过 playwright 来访问 Chrome,不清楚用到了哪个路径下的 crBrowser.js 文件,则可以通过下述步骤找到生效的 crBrowser.js 文件,然后对它定制。
先打开 Everything,搜索 crBrowser.js 路径:

打开 Process Monitor,点击Filter,然后将刚才出现的路径全部加进来,监控它们:


然后模拟实际的操作:打开 Antigravity 里的 Chrome,并打开 Antigravity 里的 Claude Code 或者自带的 AI Chat,下发指令让 AI 打开 chrome。
这时可以在 Process Monitor 里看到:访问了哪些路径的 crBrowser.js 文件。

然后就可以逐个修改逐个验证,不行再修改另一个;或者一次性全部修改完即可。
一键批量修改:Python 脚本(Windows 11)
如果你和博主一样懒,不想手动逐个找 crBrowser.js 文件并 vim 进去改,可以用下面的 Python 脚本一键搞定:借助 Everything 的 es.exe 全盘检索 → 自动备份 → 正则替换 → 干跑 / 实跑双模式。
前置条件
- 已安装 Everything 并保持后台运行(脚本通过 IPC 与之通信,不需要重新建索引)
-
es.exe可用,默认路径为:1
%LOCALAPPDATA%\Microsoft\WindowsApps\es.exe
如果你装到了别处(例如
C:\Tools\Everything\es.exe),运行脚本时通过--es显式指定即可。 - Python 3.8+,标准库即可,无第三方依赖。
脚本内容
将下面的脚本保存为 C:\Users\<你的用户名>\Documents\patch_crbrowser_download.py:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Patch Playwright crBrowser.js download behavior on Windows 11.
- Uses Everything es.exe to locate every crBrowser.js on disk.
- Replaces 'allowAndName' with 'default'.
- Removes the downloadPath line so the browser default download dir is used.
- Defaults to dry-run; pass --apply to actually modify files.
- Creates a timestamped .bak.<YYYYmmdd_HHMMSS> backup next to each patched file.
"""
import argparse
import os
import re
import shutil
import subprocess
import sys
import tempfile
from datetime import datetime
from pathlib import Path
from typing import List, Optional, Tuple
DEFAULT_ES_EXE = Path(
os.path.expandvars(r"%LOCALAPPDATA%\Microsoft\WindowsApps\es.exe")
)
DEFAULT_TARGET = "crBrowser.js"
BEHAVIOR_PATTERN = re.compile(
r"""^(\s*)behavior:\s*this\._options\.acceptDownloads\s*===\s*['"]accept['"]\s*\?\s*['"]allowAndName['"]\s*:\s*['"]deny['"],\s*$""",
re.MULTILINE,
)
DOWNLOAD_PATH_PATTERN = re.compile(
r"""^\s*downloadPath:\s*this\._browser\.options\.downloadsPath,\s*\r?\n?""",
re.MULTILINE,
)
def read_text_guess(path: Path) -> Tuple[str, str]:
raw = path.read_bytes()
for encoding in ("utf-8-sig", "utf-8", "cp936"):
try:
return raw.decode(encoding), encoding
except UnicodeDecodeError:
continue
raise UnicodeError("Cannot decode file: {}".format(path))
def write_text_keep_encoding(path: Path, text: str, encoding: str) -> None:
path.write_text(text, encoding=encoding, newline="")
def make_backup(path: Path) -> Path:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_path = path.with_name("{}.bak.{}".format(path.name, timestamp))
shutil.copy2(path, backup_path)
return backup_path
def run_everything_search(
es_exe: Path,
target_name: str,
max_results: int,
search_path: Optional[str] = None,
) -> List[Path]:
if not es_exe.exists():
raise FileNotFoundError("es.exe not found: {}".format(es_exe))
tmp_path = None
try:
with tempfile.NamedTemporaryFile(
suffix=".txt", delete=False, mode="w", encoding="utf-8",
) as tmp:
tmp_path = Path(tmp.name)
cmd = [str(es_exe), "-a-d", "-n", str(max_results),
"-export-txt", str(tmp_path)]
if search_path:
cmd.extend(["-path", search_path])
cmd.append(target_name)
result = subprocess.run(
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
text=True, encoding="utf-8", errors="replace",
)
if result.returncode != 0:
raise RuntimeError(
"Everything es.exe search failed.\n"
"Return code: {}\nCommand: {}\nSTDOUT:\n{}\nSTDERR:\n{}".format(
result.returncode, " ".join(cmd),
result.stdout, result.stderr,
)
)
content = tmp_path.read_text(encoding="utf-8-sig", errors="replace")
paths = []
for line in content.splitlines():
line = line.strip().strip('"')
if not line:
continue
paths.append(Path(line))
return paths
finally:
if tmp_path is not None:
try:
tmp_path.unlink()
except Exception:
pass
def patch_crbrowser_content(text: str) -> Tuple[str, bool, bool]:
new_text, behavior_count = BEHAVIOR_PATTERN.subn(
r"\1behavior: this._options.acceptDownloads === \"accept\" ? \"default\" : \"deny\",",
text,
)
new_text, download_path_count = DOWNLOAD_PATH_PATTERN.subn("", new_text)
return new_text, behavior_count > 0, download_path_count > 0
def dedupe_paths(paths: List[Path]) -> List[Path]:
seen, result = set(), []
for path in paths:
key = str(path).lower()
if key in seen:
continue
seen.add(key)
result.append(path)
return result
def is_target_file(path: Path, target_name: str) -> bool:
return path.name.lower() == target_name.lower()
def patch_one_file(
path: Path, target_name: str,
apply_changes: bool, create_backup: bool,
) -> Tuple[str, str]:
if not path.exists():
return "MISSING", "[MISSING] {}".format(path)
if not path.is_file():
return "SKIP", "[SKIP] Not a file: {}".format(path)
if not is_target_file(path, target_name):
return "SKIP", "[SKIP] Not target file: {}".format(path)
text, encoding = read_text_guess(path)
new_text, behavior_changed, download_path_removed = patch_crbrowser_content(text)
if new_text == text:
return "UNCHANGED", "[UNCHANGED] {}".format(path)
lines = [
"[CHANGE] {}".format(path),
" behavior allowAndName -> default : {}".format(behavior_changed),
" remove downloadPath line : {}".format(download_path_removed),
]
if apply_changes:
if create_backup:
backup_path = make_backup(path)
lines.append(" backup: {}".format(backup_path))
write_text_keep_encoding(path, new_text, encoding)
lines.append(" status: patched")
else:
lines.append(" status: dry-run only")
return "CHANGE", "\n".join(lines)
def main() -> int:
parser = argparse.ArgumentParser(
description="Search crBrowser.js by Everything es.exe and patch Playwright download behavior."
)
parser.add_argument("--es", default=str(DEFAULT_ES_EXE),
help="Path to es.exe. Default: {}".format(DEFAULT_ES_EXE))
parser.add_argument("--target", default=DEFAULT_TARGET,
help="Target file name. Default: crBrowser.js")
parser.add_argument("--path", default=None,
help=r"Optional search root, e.g. C:\Users\xxx\AppData\Local")
parser.add_argument("--max-results", type=int, default=10000,
help="Max Everything search results. Default: 10000")
parser.add_argument("--apply", action="store_true",
help="Actually modify files. Without this flag: dry-run only.")
parser.add_argument("--no-backup", action="store_true",
help="Do not create timestamped backup files when applying.")
args = parser.parse_args()
es_exe = Path(os.path.expandvars(args.es))
target_name = args.target
create_backup = not args.no_backup
print("[INFO] es.exe : {}".format(es_exe))
print("[INFO] target : {}".format(target_name))
if args.path:
print("[INFO] path : {}".format(args.path))
print("[INFO] mode : {}".format("APPLY" if args.apply else "DRY-RUN"))
if args.apply:
print("[INFO] backup : {}".format("enabled" if create_backup else "disabled"))
print()
try:
found_paths = run_everything_search(
es_exe=es_exe, target_name=target_name,
max_results=args.max_results, search_path=args.path,
)
except Exception as exc:
print("[ERROR] {}".format(exc), file=sys.stderr)
print("\nPlease check:\n"
" 1. Everything is running.\n"
" 2. es.exe path is correct.\n"
" 3. Current user can access matched files.\n"
" 4. Everything index contains the target file.")
return 2
candidates = [p for p in dedupe_paths(found_paths)
if is_target_file(p, target_name)]
print("[INFO] Found {} candidate files\n".format(len(candidates)))
changed = unchanged = missing = skipped = failed = 0
for path in candidates:
try:
status, message = patch_one_file(
path=path, target_name=target_name,
apply_changes=args.apply, create_backup=create_backup,
)
print(message); print()
if status == "CHANGE": changed += 1
elif status == "UNCHANGED": unchanged += 1
elif status == "MISSING": missing += 1
elif status == "SKIP": skipped += 1
except PermissionError as exc:
print("[FAILED] {}\n PermissionError: {}\n"
" Try running PowerShell or CMD as Administrator.\n".format(path, exc))
failed += 1
except Exception as exc:
print("[FAILED] {}\n {}: {}\n".format(path, type(exc).__name__, exc))
failed += 1
print("[SUMMARY]")
print(" candidates : {}".format(len(candidates)))
print(" changed : {}".format(changed))
print(" unchanged : {}".format(unchanged))
print(" missing : {}".format(missing))
print(" skipped : {}".format(skipped))
print(" failed : {}".format(failed))
if not args.apply:
print("\n[NOTE] Dry-run only. No file was modified.")
print(" Run again with --apply after reviewing the output.")
return 0 if failed == 0 else 1
if __name__ == "__main__":
raise SystemExit(main())
一键运行步骤
Step 1:干跑(Dry-run),先看影响范围,不会修改任何文件
1
python3 C:\Users\wanlinwang\Documents\patch_crbrowser_download.py
输出示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
[INFO] mode : DRY-RUN
[INFO] Found 3 candidate files
[CHANGE] C:\Users\wanlinwang\AppData\Local\ms-playwright-go\1.50.1\package\lib\server\chromium\crBrowser.js
behavior allowAndName -> default : True
remove downloadPath line : True
status: dry-run only
...
[SUMMARY]
candidates : 3
changed : 3
unchanged : 0
...
Step 2:确认无误后实跑(Apply),自动备份并修改
1
python3 C:\Users\wanlinwang\Documents\patch_crbrowser_download.py --apply
每个被修改的文件旁边会生成一个时间戳备份,格式为:
1
crBrowser.js.bak.20260510_154233
Step 3:重启 Antigravity / Claude Code,验证下载文件名恢复正常。
常用变体
-
指定
es.exe路径(没装在默认位置时):1
python3 patch_crbrowser_download.py --es "C:\Tools\Everything\es.exe" --apply
-
缩小搜索范围(只在某个目录下找,速度更快、影响更可控):
1
python3 patch_crbrowser_download.py --path "C:\Users\wanlinwang\AppData\Local" --apply
-
不创建备份(不推荐,除非你已经全局做了快照):
1
python3 patch_crbrowser_download.py --apply --no-backup
万一改坏了:一键回滚
每个备份文件都带时间戳,直接拷回原文件名即可:
1
copy /Y "crBrowser.js.bak.20260510_154233" "crBrowser.js"
或者用 PowerShell 批量回滚某次修改:
1
2
3
4
5
Get-ChildItem -Recurse -Filter "crBrowser.js.bak.20260510_154233" |
ForEach-Object {
$orig = $_.FullName -replace '\.bak\.\d{8}_\d{6}$',''
Copy-Item $_.FullName $orig -Force
}
脚本设计说明
- 正则只匹配硬编码的
'allowAndName' : 'deny'这一行,对 Playwright 后续版本如果换了字符串或重构成函数,会落到UNCHANGED而不是误改。 - 保留原文件编码(
utf-8-sig/utf-8/cp936自动嗅探),避免 BOM 与换行符被改坏。 - 默认 dry-run:除非显式
--apply,永远不会动文件。 - 去重路径(小写比对),防止 Everything 把同一个文件以不同大小写返回多次。
总结
Playwright 的下载拦截行为是有意设计的,目的是确保自动化测试的隔离性。对于 Antigravity 用户,需要修改 crBrowser.js:
- 将
allowAndName改为default - 移除
downloadPath参数
修改后关闭浏览器并重启 Antigravity 即可恢复正常下载行为,文件将保存到浏览器默认下载目录。
参考资料: