mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 08:17:11 +00:00
feat(browser): 增加 Phase 6B mock response 能力
在现有拦截规则之上补齐 fulfill 动作,支持最小状态码、响应头和文本响应体。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
1fbb53c4b1
commit
f133deb539
@@ -477,7 +477,22 @@ function getTools() {
|
|||||||
pageId: { type: 'string' },
|
pageId: { type: 'string' },
|
||||||
urlIncludes: { type: 'string' },
|
urlIncludes: { type: 'string' },
|
||||||
method: { 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'],
|
required: ['action'],
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
@@ -801,12 +816,56 @@ function parseOptionalPageId(toolArgs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function parseInterceptAction(value) {
|
function parseInterceptAction(value) {
|
||||||
if (value !== 'continue' && value !== 'fail') {
|
if (value !== 'continue' && value !== 'fail' && value !== 'fulfill') {
|
||||||
throw new Error('action must be one of: continue, fail');
|
throw new Error('action must be one of: continue, fail, fulfill');
|
||||||
}
|
}
|
||||||
return value;
|
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) {
|
function parseOptionalMethod(value) {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return '';
|
return '';
|
||||||
@@ -862,6 +921,8 @@ function formatInterceptRules(rules) {
|
|||||||
`urlIncludes: ${rule.urlIncludes || '<any>'}`,
|
`urlIncludes: ${rule.urlIncludes || '<any>'}`,
|
||||||
`method: ${rule.method || '<any>'}`,
|
`method: ${rule.method || '<any>'}`,
|
||||||
`action: ${rule.action}`,
|
`action: ${rule.action}`,
|
||||||
|
`statusCode: ${rule.action === 'fulfill' ? rule.statusCode : 'n/a'}`,
|
||||||
|
`contentType: ${rule.action === 'fulfill' ? rule.contentType || '<none>' : 'n/a'}`,
|
||||||
].join('\n')
|
].join('\n')
|
||||||
)
|
)
|
||||||
.join('\n---\n');
|
.join('\n---\n');
|
||||||
@@ -881,6 +942,7 @@ function formatRecentRequests(entries) {
|
|||||||
`resourceType: ${entry.resourceType || ''}`,
|
`resourceType: ${entry.resourceType || ''}`,
|
||||||
`matchedRuleId: ${entry.matchedRuleId || 'none'}`,
|
`matchedRuleId: ${entry.matchedRuleId || 'none'}`,
|
||||||
`action: ${entry.action}`,
|
`action: ${entry.action}`,
|
||||||
|
`statusCode: ${entry.action === 'fulfill' ? entry.statusCode : 'n/a'}`,
|
||||||
].join('\n')
|
].join('\n')
|
||||||
)
|
)
|
||||||
.join('\n---\n');
|
.join('\n---\n');
|
||||||
@@ -2574,6 +2636,19 @@ async function ensureInterceptSession(page) {
|
|||||||
params: { requestId: paused.requestId, errorReason: FETCH_FAIL_ERROR_REASON },
|
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 {
|
} else {
|
||||||
ws.send(
|
ws.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
@@ -2591,6 +2666,7 @@ async function ensureInterceptSession(page) {
|
|||||||
resourceType: String(paused.resourceType || ''),
|
resourceType: String(paused.resourceType || ''),
|
||||||
matchedRuleId: matchedRule ? matchedRule.ruleId : '',
|
matchedRuleId: matchedRule ? matchedRule.ruleId : '',
|
||||||
action,
|
action,
|
||||||
|
statusCode: action === 'fulfill' ? matchedRule.statusCode : 0,
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
activityChain = activityChain.catch(() => {}).then(() => activity).catch(() => {});
|
activityChain = activityChain.catch(() => {}).then(() => activity).catch(() => {});
|
||||||
@@ -2696,10 +2772,15 @@ async function handleAddInterceptRule(toolArgs) {
|
|||||||
}
|
}
|
||||||
const urlIncludes = parseOptionalUrlIncludes(toolArgs.urlIncludes);
|
const urlIncludes = parseOptionalUrlIncludes(toolArgs.urlIncludes);
|
||||||
const method = parseOptionalMethod(toolArgs.method);
|
const method = parseOptionalMethod(toolArgs.method);
|
||||||
|
const action = parseInterceptAction(toolArgs.action);
|
||||||
if (!urlIncludes && !method) {
|
if (!urlIncludes && !method) {
|
||||||
throw new Error('urlIncludes or method is required');
|
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 = {
|
const rule = {
|
||||||
ruleId: createInterceptRuleId(),
|
ruleId: createInterceptRuleId(),
|
||||||
pageId: page.id,
|
pageId: page.id,
|
||||||
@@ -2707,6 +2788,10 @@ async function handleAddInterceptRule(toolArgs) {
|
|||||||
urlIncludes,
|
urlIncludes,
|
||||||
method,
|
method,
|
||||||
action,
|
action,
|
||||||
|
statusCode: action === 'fulfill' ? statusCode : 0,
|
||||||
|
contentType: action === 'fulfill' ? contentType : '',
|
||||||
|
headers: action === 'fulfill' ? headers : [],
|
||||||
|
body: action === 'fulfill' ? body : '',
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
interceptRules.push(rule);
|
interceptRules.push(rule);
|
||||||
|
|||||||
Reference in New Issue
Block a user