feat(browser): 增加 Phase 6B mock response 能力

在现有拦截规则之上补齐 fulfill 动作,支持最小状态码、响应头和文本响应体。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
walker1211
2026-04-18 23:15:04 +08:00
co-authored by Claude Opus 4.7
parent 1fbb53c4b1
commit f133deb539
+89 -4
View File
@@ -477,7 +477,22 @@ function getTools() {
pageId: { type: 'string' },
urlIncludes: { type: 'string' },
method: { type: 'string' },
action: { type: 'string', enum: ['continue', 'fail'] },
action: { type: 'string', enum: ['continue', 'fail', 'fulfill'] },
statusCode: { type: 'integer', minimum: 100, maximum: 599 },
headers: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string' },
value: { type: 'string' },
},
required: ['name', 'value'],
additionalProperties: false,
},
},
body: { type: 'string' },
contentType: { type: 'string' },
},
required: ['action'],
additionalProperties: false,
@@ -801,12 +816,56 @@ function parseOptionalPageId(toolArgs) {
}
function parseInterceptAction(value) {
if (value !== 'continue' && value !== 'fail') {
throw new Error('action must be one of: continue, fail');
if (value !== 'continue' && value !== 'fail' && value !== 'fulfill') {
throw new Error('action must be one of: continue, fail, fulfill');
}
return value;
}
function parseOptionalStatusCode(value) {
if (value === undefined) {
return 200;
}
if (!Number.isInteger(value) || value < 100 || value > 599) {
throw new Error('statusCode must be an integer between 100 and 599');
}
return value;
}
function parseOptionalHeaders(value, contentType) {
const headers = [];
if (Array.isArray(value)) {
for (const entry of value) {
if (!entry || typeof entry !== 'object') {
throw new Error('headers entries must be objects with name and value');
}
const name = requireNonEmptyString(entry.name, 'headers.name');
const headerValue = String(entry.value ?? '');
headers.push({ name, value: headerValue });
}
} else if (value !== undefined) {
throw new Error('headers must be an array');
}
if (contentType && !headers.some((header) => header.name.toLowerCase() === 'content-type')) {
headers.push({ name: 'Content-Type', value: contentType });
}
return headers;
}
function parseOptionalBody(value) {
if (value === undefined) {
return '';
}
if (typeof value !== 'string') {
throw new Error('body must be a string');
}
return value;
}
function encodeFulfillBody(body) {
return Buffer.from(body, 'utf8').toString('base64');
}
function parseOptionalMethod(value) {
if (value === undefined) {
return '';
@@ -862,6 +921,8 @@ function formatInterceptRules(rules) {
`urlIncludes: ${rule.urlIncludes || '<any>'}`,
`method: ${rule.method || '<any>'}`,
`action: ${rule.action}`,
`statusCode: ${rule.action === 'fulfill' ? rule.statusCode : 'n/a'}`,
`contentType: ${rule.action === 'fulfill' ? rule.contentType || '<none>' : 'n/a'}`,
].join('\n')
)
.join('\n---\n');
@@ -881,6 +942,7 @@ function formatRecentRequests(entries) {
`resourceType: ${entry.resourceType || ''}`,
`matchedRuleId: ${entry.matchedRuleId || 'none'}`,
`action: ${entry.action}`,
`statusCode: ${entry.action === 'fulfill' ? entry.statusCode : 'n/a'}`,
].join('\n')
)
.join('\n---\n');
@@ -2574,6 +2636,19 @@ async function ensureInterceptSession(page) {
params: { requestId: paused.requestId, errorReason: FETCH_FAIL_ERROR_REASON },
})
);
} else if (action === 'fulfill') {
ws.send(
JSON.stringify({
id: ++requestCounter,
method: 'Fetch.fulfillRequest',
params: {
requestId: paused.requestId,
responseCode: matchedRule.statusCode,
responseHeaders: matchedRule.headers,
body: encodeFulfillBody(matchedRule.body),
},
})
);
} else {
ws.send(
JSON.stringify({
@@ -2591,6 +2666,7 @@ async function ensureInterceptSession(page) {
resourceType: String(paused.resourceType || ''),
matchedRuleId: matchedRule ? matchedRule.ruleId : '',
action,
statusCode: action === 'fulfill' ? matchedRule.statusCode : 0,
});
})();
activityChain = activityChain.catch(() => {}).then(() => activity).catch(() => {});
@@ -2696,10 +2772,15 @@ async function handleAddInterceptRule(toolArgs) {
}
const urlIncludes = parseOptionalUrlIncludes(toolArgs.urlIncludes);
const method = parseOptionalMethod(toolArgs.method);
const action = parseInterceptAction(toolArgs.action);
if (!urlIncludes && !method) {
throw new Error('urlIncludes or method is required');
}
const action = parseInterceptAction(toolArgs.action);
const statusCode = parseOptionalStatusCode(toolArgs.statusCode);
const contentType =
toolArgs.contentType === undefined ? '' : requireNonEmptyString(toolArgs.contentType, 'contentType');
const body = parseOptionalBody(toolArgs.body);
const headers = parseOptionalHeaders(toolArgs.headers, contentType);
const rule = {
ruleId: createInterceptRuleId(),
pageId: page.id,
@@ -2707,6 +2788,10 @@ async function handleAddInterceptRule(toolArgs) {
urlIncludes,
method,
action,
statusCode: action === 'fulfill' ? statusCode : 0,
contentType: action === 'fulfill' ? contentType : '',
headers: action === 'fulfill' ? headers : [],
body: action === 'fulfill' ? body : '',
createdAt: new Date().toISOString(),
};
interceptRules.push(rule);