bsk upload 为什么传不进文件:一次跨系统路径排查
现象
用浏览器自动化往一个上传页塞文件,命令返回 upload ok,页面也确实收到了 change 事件,看起来一切正常。但接下来所有命令都开始超时——截图超时、执行 JS 超时、连换个会话都连不上。只能杀掉浏览器重来。
一开始的判断是「文件进入 input[type=file] 后页面渲染进程被卡死」,于是试了隐藏 input、显形 input、JS 点按钮、物理鼠标点击、页面内 fetch 塞 DataTransfer、往原生文件对话框 SendKeys 填路径,六种做法全部失败,症状一模一样。
后来搭了个合成测试页,把这件事拆开,才发现方向从头就是错的。
把问题拆成两半
测试页很简单:一个 display:none 的 input[type=file],一个按钮,按钮 onclick 里调 input.click(),change 事件把文件名和字节数打到页面上。
<input id="f" type="file" style="display:none">
<button id="btn">上传</button>
<script>
document.getElementById('btn').onclick = () => document.getElementById('f').click();
document.getElementById('f').addEventListener('change', e => {
const f = e.target.files[0];
log(f ? f.name + ' ' + f.size + 'B' : '无文件');
});
</script>
四个实验,结果如下:
| 传法 | 结果 |
|---|---|
| 直接指向隐藏 input | error: target element has no visible geometry |
指向可见按钮,传 /tmp/x.zip |
回 upload ok,页面收到 change,size = 0 |
指向可见按钮,传 /mnt/c/x.zip |
同上,仍然 0 字节 |
指向可见按钮,传 C:\x.zip |
open upload file: No such file or directory |
文件根本没进浏览器,页面拿到的是一个 0 字节的空壳。
根因:路径字符串跨不过系统边界
这套自动化工具的进程模型是:控制端(daemon)跑在 WSL,浏览器跑在 Windows。上传时会做两件事——先在 daemon 侧打开文件,再把文件路径交给浏览器的 DOM.setFileInputFiles。
问题就在第二步:路径是以字符串形式传过去的。/tmp/x.zip 在 WSL 里存在,在 Windows 上就是个不存在的路径;Chrome 打不开,于是给出一个 size = 0 的 File,而且不报错。
Windows 路径呢?daemon 侧直接 No such file or directory——它只能看见 Linux 命名空间。
两个方向都堵死:daemon 读得懂的路径 Chrome 读不懂,Chrome 读得懂的路径 daemon 读不懂。
为什么看起来像「全链路超时」
这是最有迷惑性的一点。上传命令并没有真的失败,它在等一个永远不会到来的文件句柄,于是占着会话的命令槽不放。之后每条命令都被拒绝,报的是:
error: previous session command is still running
details: session already has an unfinished command
实测:upload ok 之后第一条求值还能用,第二条起全被拒。想 session stop 也常被这句挡住,偶尔还会抛 Background execution cleanup timed out。
所以「截图超时、evaluate 超时、重连不上」这些现象,根源只有一个:一个卡住的命令堵住了整条通道。排查时如果只看超时的表象,会一路往网络、往页面性能上找,永远找不到答案。
解法:不传路径,传字节
既然路径跨不过去,就别传路径。改成把文件内容 base64 分片注入页面,在页面里还原成 Uint8Array,构造 File,塞进 input.files:
// 1. 分片注入(每片 40KB,多次调用累加)
window.__buf += 'UEsDBBQAAAAI...';
// 2. 页面里还原成 File 并触发 change
const bin = atob(window.__buf);
const bytes = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
const file = new File([bytes], 'x.zip', { type: 'application/octet-stream' });
const dt = new DataTransfer();
dt.items.add(file);
const input = document.querySelector('input[type=file]');
input.files = dt.files;
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
这样做的好处是不经过浏览器所在的文件系统,路径问题自然消失;隐藏的 input 也能塞,不需要先让它可见。
实测:379 KB 的 zip 分 13 片注入,页面侧 file.size 与原文件字节数完全一致,change 正常触发。
顺手排除的一条路:CDP 直连
另一个思路是给浏览器加上 --remote-debugging-port=9222,用 CDP 直接调 DOM.setFileInputFiles,或者用 Playwright 的 set_input_files 传字节。
方向对,但环境不允许:Chrome 只把调试端口绑在 Windows 的 127.0.0.1,且会忽略 --remote-debugging-address=0.0.0.0。WSL 到宿主的端口被防火墙拦住,写了个 TCP 转发脚本也连不通,没有管理员权限加规则就是走不通。如果换成在浏览器所在的系统上跑控制脚本,这条路是可行的。
几条可复用的经验
- 跨系统自动化,任何以路径形式传递的文件都要警惕,改传内容。
- 命令「成功返回」不等于动作成功。这次
upload ok是真的返回了,文件也是真的 0 字节。 - 出现「所有命令都超时」时,先怀疑有命令卡在会话里,而不是网络或页面。去查命令槽状态,比抓包快得多。
- 搭一个合成测试页复现,比在真实页面上盲试六种方法省时间得多——真实页面的干扰因素太多。
- 点赞
- 收藏
- 关注作者
评论(0)