From f133deb5399367cdadd2901fbb29032d39bb5d36 Mon Sep 17 00:00:00 2001 From: walker1211 <13750528578@163.com> Date: Sat, 18 Apr 2026 23:15:04 +0800 Subject: [PATCH] =?UTF-8?q?feat(browser):=20=E5=A2=9E=E5=8A=A0=20Phase=206?= =?UTF-8?q?B=20mock=20response=20=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在现有拦截规则之上补齐 fulfill 动作,支持最小状态码、响应头和文本响应体。 Co-Authored-By: Claude Opus 4.7 --- lib/mcp/ccs-browser-server.cjs | 93 ++++++++++++++++++++++++++++++++-- 1 file changed, 89 insertions(+), 4 deletions(-) diff --git a/lib/mcp/ccs-browser-server.cjs b/lib/mcp/ccs-browser-server.cjs index a7516eed..5627d2b8 100755 --- a/lib/mcp/ccs-browser-server.cjs +++ b/lib/mcp/ccs-browser-server.cjs @@ -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 || ''}`, `method: ${rule.method || ''}`, `action: ${rule.action}`, + `statusCode: ${rule.action === 'fulfill' ? rule.statusCode : 'n/a'}`, + `contentType: ${rule.action === 'fulfill' ? rule.contentType || '' : '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);