豆豆友情提示:这是一个非官方 GitHub 代理镜像,主要用于网络测试或访问加速。请勿在此进行登录、注册或处理任何敏感信息。进行这些操作请务必访问官方网站 github.com。 Raw 内容也通过此代理提供。
Skip to content

Commit 1d3179e

Browse files
committed
feat: ensure extensions for file outputs
1 parent 429e0ca commit 1d3179e

File tree

11 files changed

+114
-20
lines changed

11 files changed

+114
-20
lines changed

src/McpContext.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,11 @@ import {Locator} from './third_party/index.js';
3333
import {PredefinedNetworkConditions} from './third_party/index.js';
3434
import {listPages} from './tools/pages.js';
3535
import {CLOSE_PAGE_ERROR} from './tools/ToolDefinition.js';
36-
import type {Context, DevToolsData} from './tools/ToolDefinition.js';
36+
import type {
37+
Context,
38+
DevToolsData,
39+
SupportedExtensions,
40+
} from './tools/ToolDefinition.js';
3741
import type {TraceResult} from './trace-processing/parse.js';
3842
import type {
3943
EmulationSettings,
@@ -46,7 +50,7 @@ import {
4650
ExtensionRegistry,
4751
type InstalledExtension,
4852
} from './utils/ExtensionRegistry.js';
49-
import {saveTemporaryFile} from './utils/files.js';
53+
import {ensureExtension, saveTemporaryFile} from './utils/files.js';
5054
import {getNetworkMultiplierFromString} from './WaitForHelper.js';
5155

5256
interface McpContextOptions {
@@ -801,10 +805,14 @@ export class McpContext implements Context {
801805
}
802806
async saveFile(
803807
data: Uint8Array<ArrayBufferLike>,
804-
filename: string,
808+
clientProvidedFilePath: string,
809+
extension: SupportedExtensions,
805810
): Promise<{filename: string}> {
806811
try {
807-
const filePath = path.resolve(filename);
812+
const filePath = ensureExtension(
813+
path.resolve(clientProvidedFilePath),
814+
extension,
815+
);
808816
await fs.mkdir(path.dirname(filePath), {recursive: true});
809817
await fs.writeFile(filePath, data);
810818
return {filename: filePath};

src/McpResponse.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -403,11 +403,12 @@ export class McpResponse implements Response {
403403
if (textSnapshot) {
404404
const formatter = new SnapshotFormatter(textSnapshot);
405405
if (this.#snapshotParams.filePath) {
406-
await context.saveFile(
406+
const result = await context.saveFile(
407407
new TextEncoder().encode(formatter.toString()),
408408
this.#snapshotParams.filePath,
409+
'.txt',
409410
);
410-
snapshot = this.#snapshotParams.filePath;
411+
snapshot = result.filename;
411412
} else {
412413
snapshot = formatter;
413414
}
@@ -429,7 +430,8 @@ export class McpResponse implements Response {
429430
fetchData: true,
430431
requestFilePath: this.#attachedNetworkRequestOptions?.requestFilePath,
431432
responseFilePath: this.#attachedNetworkRequestOptions?.responseFilePath,
432-
saveFile: (data, filename) => context.saveFile(data, filename),
433+
saveFile: (data, filename, extension) =>
434+
context.saveFile(data, filename, extension),
433435
redactNetworkHeaders: this.#redactNetworkHeaders,
434436
});
435437
detailedNetworkRequest = formatter;
@@ -573,7 +575,8 @@ export class McpResponse implements Response {
573575
context.getNetworkRequestStableId(request) ===
574576
this.#networkRequestsOptions?.networkRequestIdInDevToolsUI,
575577
fetchData: false,
576-
saveFile: (data, filename) => context.saveFile(data, filename),
578+
saveFile: (data, filename, extension) =>
579+
context.saveFile(data, filename, extension),
577580
redactNetworkHeaders: this.#redactNetworkHeaders,
578581
}),
579582
),

src/formatters/NetworkFormatter.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export interface NetworkFormatterOptions {
2424
saveFile?: (
2525
data: Uint8Array<ArrayBufferLike>,
2626
filename: string,
27+
extension: '.network-request' | '.network-response',
2728
) => Promise<{filename: string}>;
2829
redactNetworkHeaders: boolean;
2930
}
@@ -88,11 +89,12 @@ export class NetworkFormatter {
8889
throw new Error('saveFile is not provided');
8990
}
9091
if (data) {
91-
await this.#options.saveFile(
92+
const result = await this.#options.saveFile(
9293
Buffer.from(data),
9394
this.#options.requestFilePath,
95+
'.network-request',
9496
);
95-
this.#requestBodyFilePath = this.#options.requestFilePath;
97+
this.#requestBodyFilePath = result.filename;
9698
} else {
9799
this.#requestBody = requestBodyNotAvailableMessage;
98100
}
@@ -119,8 +121,12 @@ export class NetworkFormatter {
119121
if (!this.#options.saveFile) {
120122
throw new Error('saveFile is not provided');
121123
}
122-
await this.#options.saveFile(buffer, this.#options.responseFilePath);
123-
this.#responseBodyFilePath = this.#options.responseFilePath;
124+
const result = await this.#options.saveFile(
125+
buffer,
126+
this.#options.responseFilePath,
127+
'.network-response',
128+
);
129+
this.#responseBodyFilePath = result.filename;
124130
} catch {
125131
// Flatten error handling for buffer() failure and save failure
126132
}

src/tools/ToolDefinition.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,18 @@ export interface Response {
136136
setListInPageTools(): void;
137137
}
138138

139+
export type SupportedExtensions =
140+
| '.png'
141+
| '.jpeg'
142+
| '.webp'
143+
| '.json'
144+
| '.network-response'
145+
| '.network-request'
146+
| '.html'
147+
| '.txt'
148+
| '.csv'
149+
| '.json.gz';
150+
139151
/**
140152
* Only add methods required by tools/*.
141153
*/
@@ -170,7 +182,8 @@ export type Context = Readonly<{
170182
): Promise<{filepath: string}>;
171183
saveFile(
172184
data: Uint8Array<ArrayBufferLike>,
173-
filename: string,
185+
clientProvidedFilePath: string,
186+
extension: SupportedExtensions,
174187
): Promise<{filename: string}>;
175188
waitForTextOnPage(
176189
text: string[],

src/tools/lighthouse.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,12 @@ export const lighthouseAudit = definePageTool({
107107
const report = generateReport(lhr, format);
108108
const data = encoder.encode(report);
109109
if (outputDirPath) {
110-
const reportPath = path.join(outputDirPath, `report.${format}`);
111-
const {filename} = await context.saveFile(data, reportPath);
110+
const reportPath = path.join(outputDirPath, `report`);
111+
const {filename} = await context.saveFile(
112+
data,
113+
reportPath,
114+
`.${format}`,
115+
);
112116
reportPaths.push(filename);
113117
} else {
114118
const {filepath} = await context.saveTemporaryFile(

src/tools/performance.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,11 @@ async function stopTracingAndAppendOutput(
197197
});
198198
});
199199
}
200-
const file = await context.saveFile(dataToWrite, filePath);
200+
const file = await context.saveFile(
201+
dataToWrite,
202+
filePath,
203+
filePath.endsWith('.gz') ? '.json.gz' : '.json',
204+
);
201205
response.appendResponseLine(
202206
`The raw trace data was saved to ${file.filename}.`,
203207
);

src/tools/screencast.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import path from 'node:path';
1010

1111
import {zod} from '../third_party/index.js';
1212
import type {ScreenRecorder} from '../third_party/index.js';
13+
import {ensureExtension} from '../utils/files.js';
1314

1415
import {ToolCategory} from './categories.js';
1516
import {definePageTool} from './ToolDefinition.js';
@@ -46,7 +47,7 @@ export const startScreencast = definePageTool({
4647
}
4748

4849
const filePath = request.params.path ?? (await generateTempFilePath());
49-
const resolvedPath = path.resolve(filePath);
50+
const resolvedPath = ensureExtension(path.resolve(filePath), '.mp4');
5051

5152
const page = request.page;
5253

src/tools/screenshot.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,12 @@ export const screenshot = definePageTool({
8787
}
8888

8989
if (request.params.filePath) {
90-
const file = await context.saveFile(screenshot, request.params.filePath);
91-
response.appendResponseLine(`Saved screenshot to ${file.filename}.`);
90+
const result = await context.saveFile(
91+
screenshot,
92+
request.params.filePath,
93+
`.${format}`,
94+
);
95+
response.appendResponseLine(`Saved screenshot to ${result.filename}.`);
9296
} else if (screenshot.length >= 2_000_000) {
9397
const {filepath} = await context.saveTemporaryFile(
9498
screenshot,

src/utils/files.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,11 @@ export async function saveTemporaryFile(
2424
throw new Error('Could not save a file', {cause: err});
2525
}
2626
}
27+
28+
export function ensureExtension(filepath: string, extension: string): string {
29+
const normalizedExtension = extension.startsWith('.')
30+
? extension
31+
: `.${extension}`;
32+
const ext = path.extname(filepath);
33+
return filepath.slice(0, filepath.length - ext.length) + normalizedExtension;
34+
}

tests/McpResponse.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ describe('McpResponse', () => {
156156
});
157157

158158
it('saves snapshot to file and returns structured content', async t => {
159-
const filePath = join(tmpdir(), 'test-screenshot.png');
159+
const filePath = join(tmpdir(), 'test-snapshot.txt');
160160
try {
161161
await withMcpContext(async (response, context) => {
162162
const page = context.getSelectedPptrPage();

0 commit comments

Comments
 (0)