Skip to content

Commit 32fa61f

Browse files
FE-runneryang.li12ottomao
authored
fix(bridge): support fileChooserAccept in Bridge mode and WSL environments (#2693)
* fix(bridge): support fileChooserAccept in Bridge mode and WSL environments - chrome-extension/page.ts: call ensureDebuggerAttached() before Page.setInterceptFileChooserDialog in registerFileChooserListener, fixing -32000 Not allowed error in Bridge mode - agent.ts: convert WSL paths in normalizeFilePaths so Windows Chrome can read files via DOM.setFileInputFiles: /mnt/c/... → C:\... /home/... → \\wsl$\<distro>\... * refactor(core): extract file path normalization --------- Co-authored-by: yang.li12 <yang.li12@bluefocus.com> Co-authored-by: ottomao <ottomao@gmail.com>
1 parent 1d39b33 commit 32fa61f

4 files changed

Lines changed: 115 additions & 20 deletions

File tree

packages/core/src/agent/agent.ts

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,8 @@ import {
5252
parseYamlScript,
5353
} from '../yaml/index';
5454

55-
import { existsSync } from 'node:fs';
5655
import { readFile } from 'node:fs/promises';
57-
import { basename, resolve } from 'node:path';
56+
import { basename } from 'node:path';
5857
import type { AbstractInterface } from '@/device';
5958
import type { TaskRunner } from '@/task-runner';
6059
import {
@@ -89,6 +88,7 @@ import {
8988
commonContextParser,
9089
createScreenshotBoundUIContext,
9190
getReportFileName,
91+
normalizeFilePaths,
9292
normalizeScrollType,
9393
parsePrompt,
9494
} from './utils';
@@ -1711,25 +1711,9 @@ export class Agent<
17111711
return null;
17121712
}
17131713

1714-
private normalizeFilePaths(files: string[]): string[] {
1715-
if (ifInBrowser) {
1716-
throw new Error('File chooser is not supported in browser environment');
1717-
}
1718-
1719-
return files.map((file) => {
1720-
const absolutePath = resolve(file);
1721-
if (!existsSync(absolutePath)) {
1722-
throw new Error(
1723-
`File not found: ${file}. Resolved to: ${absolutePath}. Current working directory: ${process.cwd()}`,
1724-
);
1725-
}
1726-
return absolutePath;
1727-
});
1728-
}
1729-
17301714
private normalizeFileInput(files: string | string[]): string[] {
17311715
const filesArray = Array.isArray(files) ? files : [files];
1732-
return this.normalizeFilePaths(filesArray);
1716+
return normalizeFilePaths(filesArray);
17331717
}
17341718

17351719
/**

packages/core/src/agent/utils.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { existsSync } from 'node:fs';
2+
import { resolve } from 'node:path';
13
import { pixelBboxToRect } from '@/ai-model/workflows/inspect/locate-result-rect';
24
import type { TMultimodalPrompt, TUserPrompt } from '@/common';
35
import type { AbstractInterface } from '@/device';
@@ -27,7 +29,7 @@ import {
2729
} from '@midscene/shared/img';
2830
import { getDebug } from '@midscene/shared/logger';
2931
import { _keyDefinitions } from '@midscene/shared/us-keyboard-layout';
30-
import { assert, logMsg, uuid } from '@midscene/shared/utils';
32+
import { assert, ifInBrowser, logMsg, uuid } from '@midscene/shared/utils';
3133
import dayjs from 'dayjs';
3234
import type { TaskCache } from './task-cache';
3335
import { debug as cacheDebug } from './task-cache';
@@ -238,6 +240,51 @@ export function printReportMsg(filepath: string) {
238240
logMsg(`Midscene - report file updated: ${filepath}`);
239241
}
240242

243+
type NormalizeFilePathsOptions = {
244+
fileExists?: (path: string) => boolean;
245+
isInBrowser?: boolean;
246+
resolvePath?: (path: string) => string;
247+
wslDistroName?: string;
248+
cwd?: string;
249+
};
250+
251+
export function normalizeFilePaths(
252+
files: string[],
253+
options: NormalizeFilePathsOptions = {},
254+
): string[] {
255+
const {
256+
fileExists = existsSync,
257+
isInBrowser = ifInBrowser,
258+
resolvePath = resolve,
259+
wslDistroName = process.env.WSL_DISTRO_NAME,
260+
cwd = process.cwd(),
261+
} = options;
262+
263+
if (isInBrowser) {
264+
throw new Error('File chooser is not supported in browser environment');
265+
}
266+
267+
return files.map((file) => {
268+
const absolutePath = resolvePath(file);
269+
if (!fileExists(absolutePath)) {
270+
throw new Error(
271+
`File not found: ${file}. Resolved to: ${absolutePath}. Current working directory: ${cwd}`,
272+
);
273+
}
274+
275+
if (!wslDistroName) {
276+
return absolutePath;
277+
}
278+
279+
const wslMount = absolutePath.match(/^\/mnt\/([a-z])\//);
280+
if (wslMount) {
281+
return `${wslMount[1].toUpperCase()}:\\${absolutePath.slice(7).replace(/\//g, '\\')}`;
282+
}
283+
284+
return `\\\\wsl$\\${wslDistroName}${absolutePath.replace(/\//g, '\\')}`;
285+
});
286+
}
287+
241288
export function isPixelBbox(value: unknown): value is PixelBbox {
242289
return (
243290
Array.isArray(value) &&

packages/core/tests/unit-test/file-chooser-accept-path.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { unlinkSync, writeFileSync } from 'node:fs';
22
import { join, relative, resolve } from 'node:path';
33
import { Agent } from '@/agent';
4+
import { normalizeFilePaths } from '@/agent/utils';
45
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
56

67
const createMockInterface = () =>
@@ -11,6 +12,11 @@ const createMockInterface = () =>
1112

1213
const fixturesDir = join(__dirname, '../fixtures');
1314
const testFilePath = join(fixturesDir, 'path-test-file.txt');
15+
const hadWslDistroName = Object.prototype.hasOwnProperty.call(
16+
process.env,
17+
'WSL_DISTRO_NAME',
18+
);
19+
const originalWslDistroName = process.env.WSL_DISTRO_NAME;
1420

1521
// resolve() resolves based on process.cwd(), compute fixture path relative to cwd
1622
const relativeFromCwd = relative(process.cwd(), testFilePath);
@@ -19,6 +25,7 @@ describe('fileChooserAccept relative path support', () => {
1925
let agent: Agent;
2026

2127
beforeAll(() => {
28+
Reflect.deleteProperty(process.env, 'WSL_DISTRO_NAME');
2229
writeFileSync(testFilePath, 'path test content');
2330
agent = new Agent(createMockInterface(), {
2431
modelConfig: {
@@ -32,6 +39,12 @@ describe('fileChooserAccept relative path support', () => {
3239
try {
3340
unlinkSync(testFilePath);
3441
} catch {}
42+
43+
if (hadWslDistroName) {
44+
process.env.WSL_DISTRO_NAME = originalWslDistroName;
45+
} else {
46+
Reflect.deleteProperty(process.env, 'WSL_DISTRO_NAME');
47+
}
3548
});
3649

3750
it('should resolve relative path with ./ prefix', () => {
@@ -121,3 +134,53 @@ describe('fileChooserAccept relative path support', () => {
121134
expect(result.length).toBe(2);
122135
});
123136
});
137+
138+
describe('normalizeFilePaths', () => {
139+
it('throws in browser environments', () => {
140+
expect(() =>
141+
normalizeFilePaths(['file.txt'], { isInBrowser: true }),
142+
).toThrow('File chooser is not supported in browser environment');
143+
});
144+
145+
it('resolves relative paths and validates existence', () => {
146+
const result = normalizeFilePaths(['./upload.txt'], {
147+
fileExists: (filePath) => filePath === '/project/upload.txt',
148+
resolvePath: () => '/project/upload.txt',
149+
wslDistroName: '',
150+
});
151+
152+
expect(result).toEqual(['/project/upload.txt']);
153+
});
154+
155+
it('throws with the original and resolved paths when a file does not exist', () => {
156+
expect(() =>
157+
normalizeFilePaths(['./missing.txt'], {
158+
cwd: '/project',
159+
fileExists: () => false,
160+
resolvePath: () => '/project/missing.txt',
161+
}),
162+
).toThrow(
163+
'File not found: ./missing.txt. Resolved to: /project/missing.txt. Current working directory: /project',
164+
);
165+
});
166+
167+
it('converts WSL mounted Windows drive paths for Windows Chrome', () => {
168+
const result = normalizeFilePaths(['/mnt/c/Users/me/upload.zip'], {
169+
fileExists: () => true,
170+
resolvePath: (filePath) => filePath,
171+
wslDistroName: 'Ubuntu-22.04',
172+
});
173+
174+
expect(result).toEqual(['C:\\Users\\me\\upload.zip']);
175+
});
176+
177+
it('converts WSL distro-local paths to UNC paths for Windows Chrome', () => {
178+
const result = normalizeFilePaths(['/home/me/upload.zip'], {
179+
fileExists: () => true,
180+
resolvePath: (filePath) => filePath,
181+
wslDistroName: 'Ubuntu-22.04',
182+
});
183+
184+
expect(result).toEqual(['\\\\wsl$\\Ubuntu-22.04\\home\\me\\upload.zip']);
185+
});
186+
});

packages/web-integration/src/chrome-extension/page.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,7 @@ export default class ChromeExtensionProxyPage implements AbstractInterface {
404404
const registrationVersion = ++this.fileChooserRegistrationVersion;
405405
const tabId = await this.getTabIdOrConnectToCurrentTab();
406406

407+
await this.ensureDebuggerAttached();
407408
await this.sendCommandToDebugger('Page.enable', {});
408409
await this.sendCommandToDebugger('DOM.enable', {});
409410
await this.sendCommandToDebugger('Page.setInterceptFileChooserDialog', {

0 commit comments

Comments
 (0)