在使用 Google Antigravity IDE 或其他基于 Playwright 的浏览器自动化工具时,遇到一个困扰的问题:下载的文件被自动重命名为 UUID 格式,并保存到临时目录,这导致在下载东西时无法自动化,反而降低了效率。本文深入分析这个问题的原因,并提供详细的解决方案。

问题现象

当使用 Antigravity IDE 控制 Chrome 浏览器下载文件时,出现以下异常行为:

  1. 文件名变成 UUID:如 f6746de9-95f6-4c06-b636-3ac7c2fb73bc,而非原始文件名
  2. 保存位置异常:文件被保存到 %LOCALAPPDATA%\Temp\playwright-artifacts-xxxxx 临时目录
  3. 每次会话独立:每次启动浏览器都会创建新的临时目录

问题原因分析

Playwright 的下载拦截机制

Playwright 是一个强大的浏览器自动化框架,被许多 IDE 和测试工具采用(如 Google Antigravity)。它默认会拦截所有下载操作,这是其核心设计特性。

CDP Browser.setDownloadBehavior 参数解析

Playwright 使用 Chrome DevTools Protocol (CDP) 的 Browser.setDownloadBehavior 命令来控制下载行为:

behavior 值 行为描述 文件名 downloadPath
deny 拒绝所有下载 N/A 不需要
allow 允许下载,使用原始文件名 原始名 必须指定
allowAndName 允许下载,使用 GUID 命名 UUID 必须指定
default 浏览器默认行为 原始名 可省略

问题根源:Playwright 硬编码使用了 allowAndName 模式,目的是防止多次下载时文件名冲突、确保测试隔离性。对于通过 CDP 连接已有 Chrome 的使用场景(如 Antigravity),这个机制会把下载重定向到临时目录并改名。

两代代码位置

随着 Playwright 版本演进,触发 Browser.setDownloadBehavior 的逻辑位置发生了变化:

Playwright 版本 文件 修改点
较早版本 crBrowser.js behavior: 'allowAndName' + downloadPath
较新版本 coreBundle.js _connectOverCDPImpl 中的 persistent 对象

两处都需要对症下药。

解决方案

crBrowser.js 的修改

找到文件(路径形如):

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,

[!WARNING] 千万不要使用 allow 且不指定路径:CDP 规定 behavior: 'allow' 时必须提供 downloadPath,否则 Playwright 启动浏览器时会报错或卡住。

coreBundle.js 的修改

较新版本里,_connectOverCDPImpl 函数构造 persistent 对象时,用了一个三元展开:

1
2
3
4
5
// 修改前
const persistent = {
  noDefaultViewport: true,
  ...options.noDefaults ? { acceptDownloads: "internal-browser-default" } : {}
};

这意味着只有 options.noDefaults 为真时才设置 acceptDownloads,否则不设置,Playwright 就会调用 setDownloadBehavior 进行拦截。改为始终直接赋值即可:

1
2
3
4
5
// 修改后
const persistent = {
  noDefaultViewport: true,
  acceptDownloads: "internal-browser-default"
};

这样 Playwright 在通过 CDP 连接时,acceptDownloads 始终为 internal-browser-default,不再触发 setDownloadBehavior,Chrome 扩展的下载行为就不会被重定向到临时目录。

不知道修改哪个路径下的文件?

有时候不清楚用到了哪个路径下的 crBrowser.jscoreBundle.js,可以通过以下步骤定位:

先打开 Everything,搜索 crBrowser.js 路径:

image-20260416182014517

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

image-20260416181954393

image-20260416182053561

然后模拟实际操作:打开 Antigravity 里的 Chrome,并打开 Antigravity 里的 Claude Code 或自带的 AI Chat,下发指令让 AI 打开 chrome

这时可以在 Process Monitor 里看到访问了哪些路径的文件:

image-20260416182100074

然后逐个修改逐个验证,或者一次性全部修改完。

一键批量修改:脚本工具(Windows 11)

不想手动逐个找文件 vim 进去改,可以用下面的脚本组合一键搞定:借助 Everything 的 es.exe 全盘检索 → 自动备份 → 正则替换 → 干跑 / 实跑双模式,同时支持 crBrowser.jscoreBundle.js 两个目标。

前置条件

  1. 已安装 Everything 并保持后台运行
  2. es.exe 可用,默认路径为:
    1
    
    %LOCALAPPDATA%\Microsoft\WindowsApps\es.exe
    
  3. Python 3.8+,标准库即可,无第三方依赖

Python 脚本

将下面的脚本保存为 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
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
Patch Playwright crBrowser.js / coreBundle.js download behavior on Windows 11.

This script uses Everything es.exe to search target files.

--- crBrowser.js patch ---

  behavior: this._options.acceptDownloads === 'accept' ? 'allowAndName' : 'deny',

to:

  behavior: this._options.acceptDownloads === 'accept' ? 'default' : 'deny',

And removes:

  downloadPath: this._browser.options.downloadsPath,

--- coreBundle.js patch ---

In _connectOverCDPImpl, changes:

  const persistent = {
    noDefaultViewport: true,
    ...options.noDefaults ? { acceptDownloads: "internal-browser-default" } : {}
  };

to:

  const persistent = {
    noDefaultViewport: true,
    acceptDownloads: "internal-browser-default"
  };

This prevents Playwright from calling Browser.setDownloadBehavior when connecting
via CDP, so Chrome extension downloads are not redirected to a temp folder.

---

Default mode is dry-run.
Use --apply to really modify files.

Before modifying, the script creates timestamp backups, for example:

  crBrowser.js.bak.20260510_154233
  coreBundle.js.bak.20260510_154233
"""

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"


# ---------- crBrowser.js patterns ----------

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,
)


# ---------- coreBundle.js patterns ----------

# Matches:  ...options.noDefaults ? { acceptDownloads: "internal-browser-default" } : {}
# on a single line (possibly with leading whitespace).
# This is the spread expression inside _connectOverCDPImpl's persistent object.
COREBUNDLE_PERSISTENT_PATTERN = re.compile(
    r"""(\s*)\.\.\.\s*options\.noDefaults\s*\?\s*\{\s*acceptDownloads:\s*["']internal-browser-default["']\s*\}\s*:\s*\{\}""",
    re.MULTILINE,
)


# ---------- helpers ----------

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: {}\n"
                "Command: {}\n"
                "STDOUT:\n{}\n"
                "STDERR:\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


# ---------- patch logic ----------

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 patch_corebundle_content(text: str) -> Tuple[str, bool]:
    """Remove the noDefaults ternary spread and always use internal-browser-default."""
    new_text, count = COREBUNDLE_PERSISTENT_PATTERN.subn(
        r'\1acceptDownloads: "internal-browser-default"',
        text,
    )
    return new_text, count > 0


# ---------- file-level dispatch ----------

def dedupe_paths(paths: List[Path]) -> List[Path]:
    seen = set()
    result = []

    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)

    if target_name.lower() == "corebundle.js":
        new_text, persistent_changed = patch_corebundle_content(text)

        if new_text == text:
            return "UNCHANGED", "[UNCHANGED] {}".format(path)

        lines = [
            "[CHANGE] {}".format(path),
            "         noDefaults spread -> direct acceptDownloads : {}".format(persistent_changed),
        ]
    else:
        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 print_restore_hint(target_name: str) -> None:
    print()
    print("[RESTORE]")
    print("  To restore one file, copy the backup file back to original name.")
    print("  Example:")
    print()
    example_bak = "{}.bak.20260510_154233".format(target_name)
    print(r'  copy /Y "{}" "{}"'.format(example_bak, target_name))
    print()


def main() -> int:
    parser = argparse.ArgumentParser(
        description=(
            "Search crBrowser.js or coreBundle.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  (also supports: coreBundle.js)",
    )

    parser.add_argument(
        "--path",
        default=None,
        help=r"Optional search root path. Example: C:\Users\wanlinwang\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 option, dry-run only.",
    )

    parser.add_argument(
        "--no-backup",
        action="store_true",
        help="Do not create timestamp backup files when applying changes.",
    )

    args = parser.parse_args()

    es_exe = Path(os.path.expandvars(args.es))
    target_name = args.target
    create_backup = not args.no_backup

    print("[INFO] Everything es.exe")
    print("       {}".format(es_exe))
    print("[INFO] Search target")
    print("       {}".format(target_name))

    if args.path:
        print("[INFO] Search path")
        print("       {}".format(args.path))

    print("[INFO] Mode")
    print("       {}".format("APPLY" if args.apply else "DRY-RUN"))

    if args.apply:
        print("[INFO] Backup")
        print("       {}".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()
        print("Please check:")
        print("  1. Everything is running.")
        print("  2. es.exe path is correct.")
        print("  3. Current user can access matched files.")
        print("  4. Everything index contains the target file.")
        return 2

    candidates = [
        path for path in dedupe_paths(found_paths)
        if is_target_file(path, target_name)
    ]

    print("[INFO] Found {} candidate files".format(len(candidates)))
    print()

    changed = 0
    unchanged = 0
    missing = 0
    skipped = 0
    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] {}".format(path))
            print("         PermissionError: {}".format(exc))
            print("         Try running PowerShell or CMD as Administrator.")
            print()
            failed += 1

        except Exception as exc:
            print("[FAILED] {}".format(path))
            print("         {}: {}".format(type(exc).__name__, exc))
            print()
            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()
        print("[NOTE]")
        print("  Dry-run only. No file was modified.")
        print("  After checking the output, run again with --apply.")
        print()
        print("  Example:")
        if target_name.lower() == "corebundle.js":
            print(
                r"  python3 C:\Users\wanlinwang\Documents\patch_crbrowser_download.py"
                r" --target coreBundle.js --apply"
            )
        else:
            print(
                r"  python3 C:\Users\wanlinwang\Documents\patch_crbrowser_download.py --apply"
            )

    if args.apply and create_backup and changed > 0:
        print_restore_hint(target_name)

    return 0 if failed == 0 else 1


if __name__ == "__main__":
    raise SystemExit(main())

批处理菜单(patch_playwright_download.bat)

嫌每次敲命令麻烦,可以配套一个批处理菜单文件 patch_playwright_download.bat,双击运行选数字即可:

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
@echo off
setlocal enabledelayedexpansion

title Patch Playwright Download Behavior

set "SCRIPT=%USERPROFILE%\Documents\patch_crbrowser_download.py"
set "ES=%LOCALAPPDATA%\Microsoft\WindowsApps\es.exe"

echo ================================================
echo   Patch Playwright Download Behavior
echo ================================================
echo.
echo Python script:
echo   %SCRIPT%
echo.
echo Everything es.exe:
echo   %ES%
echo.

if not exist "%SCRIPT%" (
    echo [ERROR] Python script not found:
    echo   %SCRIPT%
    echo.
    pause
    exit /b 1
)

if not exist "%ES%" (
    echo [ERROR] es.exe not found:
    echo   %ES%
    echo.
    pause
    exit /b 1
)

where python3 >nul 2>nul
if %errorlevel%==0 (
    set "PYTHON=python3"
) else (
    where python >nul 2>nul
    if %errorlevel%==0 (
        set "PYTHON=python"
    ) else (
        echo [ERROR] Python not found. Please install Python 3 or add it to PATH.
        echo.
        pause
        exit /b 1
    )
)

:MENU
echo ================================================
echo Please select:
echo.
echo   [crBrowser.js]  behavior: allowAndName ^> default, remove downloadPath
echo   1. Dry-run, check only, do not modify files
echo   2. Apply, patch all crBrowser.js files found by Everything
echo   3. Apply, only search under %%LOCALAPPDATA%%
echo   4. Check remaining allowAndName in crBrowser.js
echo.
echo   [coreBundle.js]  persistent.acceptDownloads: noDefaults spread ^> direct
echo   5. Dry-run, check only, do not modify files
echo   6. Apply, patch all coreBundle.js files found by Everything
echo   7. Apply, only search under %%LOCALAPPDATA%%
echo   8. Check remaining noDefaults spread in coreBundle.js
echo.
echo   9. Exit
echo.
echo ================================================
set /p CHOICE=Input [1-9]:

if "%CHOICE%"=="1" goto DRYRUN
if "%CHOICE%"=="2" goto APPLY_ALL
if "%CHOICE%"=="3" goto APPLY_LOCALAPPDATA
if "%CHOICE%"=="4" goto CHECK_ALLOWANDNAME
if "%CHOICE%"=="5" goto CB_DRYRUN
if "%CHOICE%"=="6" goto CB_APPLY_ALL
if "%CHOICE%"=="7" goto CB_APPLY_LOCALAPPDATA
if "%CHOICE%"=="8" goto CHECK_CB_NODEFAULTS
if "%CHOICE%"=="9" goto END

echo.
echo [ERROR] Invalid choice.
echo.
goto MENU


rem ==================== crBrowser.js ====================

:DRYRUN
echo.
echo [INFO] Running dry-run for crBrowser.js...
echo.
%PYTHON% "%SCRIPT%" --es "%ES%"
echo.
pause
goto MENU

:APPLY_ALL
echo.
echo [WARN] This will modify matched crBrowser.js files.
echo [WARN] Backup files will be created before patching.
echo.
set /p CONFIRM=Type YES to continue:
if /I not "%CONFIRM%"=="YES" (
    echo.
    echo [INFO] Cancelled.
    echo.
    pause
    goto MENU
)
echo.
echo [INFO] Applying patch to crBrowser.js...
echo.
%PYTHON% "%SCRIPT%" --es "%ES%" --apply
echo.
pause
goto MENU

:APPLY_LOCALAPPDATA
echo.
echo [WARN] This will modify crBrowser.js files under:
echo   %LOCALAPPDATA%
echo.
echo [WARN] Backup files will be created before patching.
echo.
set /p CONFIRM=Type YES to continue:
if /I not "%CONFIRM%"=="YES" (
    echo.
    echo [INFO] Cancelled.
    echo.
    pause
    goto MENU
)
echo.
echo [INFO] Applying patch to crBrowser.js under LOCALAPPDATA...
echo.
%PYTHON% "%SCRIPT%" --es "%ES%" --path "%LOCALAPPDATA%" --apply
echo.
pause
goto MENU

:CHECK_ALLOWANDNAME
echo.
echo [INFO] Checking remaining allowAndName in crBrowser.js...
echo.
powershell -NoProfile -ExecutionPolicy Bypass -Command ^
"$es = '%ES%'; ^
$paths = & $es -a-d crBrowser.js; ^
$found = $false; ^
foreach ($p in $paths) { ^
  if (Test-Path $p) { ^
    $m = Select-String -Path $p -Pattern 'allowAndName' -SimpleMatch -ErrorAction SilentlyContinue; ^
    if ($m) { ^
      $found = $true; ^
      Write-Host 'STILL_HAS_allowAndName:' $p; ^
    } ^
  } ^
}; ^
if (-not $found) { Write-Host 'OK: no allowAndName found in crBrowser.js files returned by Everything.' }"
echo.
pause
goto MENU


rem ==================== coreBundle.js ====================

:CB_DRYRUN
echo.
echo [INFO] Running dry-run for coreBundle.js...
echo.
%PYTHON% "%SCRIPT%" --es "%ES%" --target coreBundle.js
echo.
pause
goto MENU

:CB_APPLY_ALL
echo.
echo [WARN] This will modify matched coreBundle.js files.
echo [WARN] Backup files will be created before patching.
echo.
set /p CONFIRM=Type YES to continue:
if /I not "%CONFIRM%"=="YES" (
    echo.
    echo [INFO] Cancelled.
    echo.
    pause
    goto MENU
)
echo.
echo [INFO] Applying patch to coreBundle.js...
echo.
%PYTHON% "%SCRIPT%" --es "%ES%" --target coreBundle.js --apply
echo.
pause
goto MENU

:CB_APPLY_LOCALAPPDATA
echo.
echo [WARN] This will modify coreBundle.js files under:
echo   %LOCALAPPDATA%
echo.
echo [WARN] Backup files will be created before patching.
echo.
set /p CONFIRM=Type YES to continue:
if /I not "%CONFIRM%"=="YES" (
    echo.
    echo [INFO] Cancelled.
    echo.
    pause
    goto MENU
)
echo.
echo [INFO] Applying patch to coreBundle.js under LOCALAPPDATA...
echo.
%PYTHON% "%SCRIPT%" --es "%ES%" --target coreBundle.js --path "%LOCALAPPDATA%" --apply
echo.
pause
goto MENU

:CHECK_CB_NODEFAULTS
echo.
echo [INFO] Checking remaining noDefaults spread in coreBundle.js...
echo        (presence means file has NOT been patched yet)
echo.
powershell -NoProfile -ExecutionPolicy Bypass -Command ^
"$es = '%ES%'; ^
$paths = & $es -a-d coreBundle.js; ^
$found = $false; ^
foreach ($p in $paths) { ^
  if (Test-Path $p) { ^
    $m = Select-String -Path $p -Pattern 'options\.noDefaults.*acceptDownloads' -ErrorAction SilentlyContinue; ^
    if ($m) { ^
      $found = $true; ^
      Write-Host 'STILL_HAS_noDefaults_SPREAD:' $p; ^
    } ^
  } ^
}; ^
if (-not $found) { Write-Host 'OK: no noDefaults spread found in coreBundle.js files returned by Everything.' }"
echo.
pause
goto MENU


:END
echo.
echo Bye.
echo.
endlocal
exit /b 0

使用步骤

针对 crBrowser.js(旧版 Playwright)

Step 1:dry-run,先看影响范围,不会修改任何文件:

1
python3 C:\Users\wanlinwang\Documents\patch_crbrowser_download.py

Step 2:确认无误后实跑,自动备份并修改:

1
python3 C:\Users\wanlinwang\Documents\patch_crbrowser_download.py --apply

针对 coreBundle.js(新版 Playwright)

Step 1:dry-run:

1
python3 C:\Users\wanlinwang\Documents\patch_crbrowser_download.py --target coreBundle.js

Step 2:实跑:

1
python3 C:\Users\wanlinwang\Documents\patch_crbrowser_download.py --target coreBundle.js --apply

每个被修改的文件旁边会生成时间戳备份,格式为:

1
2
crBrowser.js.bak.20260510_154233
coreBundle.js.bak.20260510_154233

Step 3:重启 Antigravity / Claude Code,验证下载文件名恢复正常。

常用变体

缩小搜索范围(只在 LOCALAPPDATA 下找,速度更快、影响更可控):

1
2
python3 patch_crbrowser_download.py --path "%LOCALAPPDATA%" --apply
python3 patch_crbrowser_download.py --target coreBundle.js --path "%LOCALAPPDATA%" --apply

万一改坏了:一键回滚

直接拷回原文件名:

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
  }

脚本设计说明

  • 正则只匹配硬编码特征字符串,后续版本如果重构,会落到 UNCHANGED 而不是误改
  • 保留原文件编码utf-8-sig / utf-8 / cp936 自动嗅探),避免 BOM 与换行符被改坏
  • 默认 dry-run:除非显式 --apply,永远不会动文件
  • 去重路径(小写比对),防止 Everything 把同一文件以不同大小写返回多次

相关 GitHub Issues

Issue 描述 状态
#32692 请求支持用户指定的下载文件名 Open
#7464 UUID 文件名是否是预期行为 Closed
#2058 允许使用自然浏览器文件名 Closed

总结

Playwright 的下载拦截行为是有意设计的,对于 Antigravity 等通过 CDP 连接 Chrome 的场景,需要根据 Playwright 版本选择修改目标:

版本 修改目标 修改内容
较早版本 crBrowser.js allowAndNamedefault,移除 downloadPath
较新版本 coreBundle.js noDefaults 三元展开 → 直接赋值 acceptDownloads

不确定版本时,两个文件都改,让脚本自动识别哪个能匹配上。修改后关闭浏览器并重启 Antigravity,文件将保存到浏览器默认下载目录。


参考资料: