From 0cd4eafd613cb7b6ab418916c21b6f0bc1f15ff3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=83=AD=E7=AB=8B=20Guo=20Li?= Date: Sat, 17 Oct 2026 15:51:23 +0800 Subject: [PATCH] perf: reduce MCP launcher and browser capture overhead (#4350) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * perf(browser): 为单次捕获复用脚本资源索引 * docs(setup): 说明多会话直接启动以减少 npm 驻留 --- docs/installation.md | 15 ++++ src/browser/CdpPageCaptureScripts.ts | 36 +++++----- src/domain/webInventory.test.ts | 101 +++++++++++++++++++++++---- src/domain/webInventory.ts | 54 ++++++++------ 4 files changed, 157 insertions(+), 49 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index 0bfd00063c..330a6b81cd 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -1236,6 +1236,21 @@ on the shell `PATH`. Use the setup command above for the guided setup journey, `npx -y rea-agents@latest` for unattended one-off commands, or install globally with `npm install --global rea-agents` for a shell-visible `rea` command. +For many concurrent agent sessions, run setup directly from a global installation +to avoid keeping a separate npm launcher process for each MCP connection: + +```bash +npm install --global rea-agents +rea setup +``` + +Review the plan, approve the selected client changes, then restart those clients. +This registers the installed entrypoint directly; installing the global package +alone does not replace existing `npx` registrations. Each client still has its own +REA process and analysis state. Direct registrations follow the installed CLI; +use `rea update` to update it. Running setup through `npx` again selects the pinned +package-runner command. Do not register paths inside npm's temporary package cache. + The optional curl wrapper installs only the global npm package: ```bash diff --git a/src/browser/CdpPageCaptureScripts.ts b/src/browser/CdpPageCaptureScripts.ts index 4108484340..6c20c75140 100644 --- a/src/browser/CdpPageCaptureScripts.ts +++ b/src/browser/CdpPageCaptureScripts.ts @@ -1,6 +1,6 @@ import type { WebPageInspection } from "../domain/browserObservationSchemas.js"; import { - reconcileCapturedWebScript, + createCapturedWebScriptReconciler, stableWebScriptKey, } from "../domain/webInventory.js"; import type { CapturedResource } from "./CdpCaptureDocuments.js"; @@ -32,8 +32,9 @@ interface ScriptDraftState { readonly context: CaptureContext; readonly events: CdpCaptureEvents; readonly frameIds: ReadonlySet; - readonly rawResources: readonly CapturedResource[]; - readonly resources: WebPageInspection["resources"]; + readonly reconcileResource: ReturnType< + typeof createCapturedWebScriptReconciler + >; } type ScriptDraftItem = Omit< @@ -45,19 +46,23 @@ export const captureScripts = async ( options: CaptureScriptsOptions, ): Promise => { const { context, events, rawResources, resources, frameIds } = options; - const state: ScriptDraftState = { - context, - events, - frameIds, - rawResources, - resources, - }; + let state: ScriptDraftState | undefined; const drafts: { readonly item: ScriptDraftItem; readonly sourceMapRawUrl: string | null; }[] = []; - for (const script of events.scripts.values()) + for (const script of events.scripts.values()) { + state ??= { + context, + events, + frameIds, + reconcileResource: createCapturedWebScriptReconciler( + rawResources, + resources, + ), + }; drafts.push(await buildScriptDraft(script, state)); + } if (!context.input.include_script_sources) events.completeness.exclude( "script_sources", @@ -91,11 +96,10 @@ const buildScriptDraft = async ( item: { ...inventoryScript, origin: script.origin, - resource_reconciliation: reconcileCapturedWebScript( - { ...inventoryScript, rawUrl: script.rawUrl }, - state.rawResources, - state.resources, - ), + resource_reconciliation: state.reconcileResource({ + ...inventoryScript, + rawUrl: script.rawUrl, + }), source, }, sourceMapRawUrl: script.sourceMapRawUrl, diff --git a/src/domain/webInventory.test.ts b/src/domain/webInventory.test.ts index 51a6b42e90..692a8ab2b9 100644 --- a/src/domain/webInventory.test.ts +++ b/src/domain/webInventory.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { - reconcileCapturedWebScript, + createCapturedWebScriptReconciler, reconcileWebScript, stableWebResources, stableWebScriptKey, @@ -11,6 +11,15 @@ import { webTextArtifactSchema, } from "./webContentArtifact.js"; +const script = { + url: "https://app.example.test/assets/app.js", + cdp_hash: "stable-cdp-hash", + length: 1_024, + is_module: true, + language: "JavaScript", + source_map_url: "https://app.example.test/assets/app.js.map", +}; + describe("web content artifacts", () => { it("rejects altered content that reuses an artifact digest", () => { const artifact = createWebTextArtifact("original", "text/javascript"); @@ -29,15 +38,6 @@ describe("web content artifacts", () => { }); describe("stable web inventory", () => { - const script = { - url: "https://app.example.test/assets/app.js", - cdp_hash: "stable-cdp-hash", - length: 1_024, - is_module: true, - language: "JavaScript", - source_map_url: "https://app.example.test/assets/app.js.map", - }; - it("excludes transient CDP and target identity from script keys", () => { const first = { ...script, @@ -134,11 +134,13 @@ describe("stable web inventory", () => { rawResources.map(({ rawUrl: _rawUrl, ...resource }) => resource), ); expect( - reconcileCapturedWebScript( - { ...script, rawUrl: app.rawUrl }, + createCapturedWebScriptReconciler( rawResources, resources, - ), + )({ + ...script, + rawUrl: app.rawUrl, + }), ).toEqual({ status: "exact", resource_key: resources.find(({ url }) => url === script.url) @@ -147,3 +149,76 @@ describe("stable web inventory", () => { } }); }); + +describe("captured web resource reconciliation", () => { + it("keeps repeated raw observations ambiguous after public deduplication", () => { + const resource = { + url: script.url, + rawUrl: `${script.url}?variant=one`, + origin: "https://app.example.test", + type: "Script", + mime_type: "text/javascript", + content_size: 1_024, + }; + const resources = stableWebResources( + [resource].map(({ rawUrl: _, ...r }) => r), + ); + const reconcile = createCapturedWebScriptReconciler( + [resource, resource, resource], + resources, + ); + expect(reconcile({ ...script, rawUrl: resource.rawUrl })).toEqual({ + status: "ambiguous", + candidate_resource_keys: resources.map( + ({ resource_key }) => resource_key, + ), + }); + }); + + it("preserves exact raw matches and ambiguous sanitized fallbacks in one capture", () => { + const rawResources = [1_024, 2_048].map((content_size, index) => ({ + url: script.url, + rawUrl: `${script.url}?variant=${index}`, + origin: "https://app.example.test", + type: "Script", + mime_type: "text/javascript", + content_size, + })); + const resources = stableWebResources( + rawResources.map(({ rawUrl: _, ...r }) => r), + ); + const reconcile = createCapturedWebScriptReconciler( + rawResources, + resources, + ); + for (const resource of rawResources) + expect(reconcile({ ...script, rawUrl: resource.rawUrl })).toEqual({ + status: "exact", + resource_key: resources.find( + (r) => r.content_size === resource.content_size, + )?.resource_key, + }); + const fallback = { ...script, rawUrl: `${script.url}?variant=unobserved` }; + const expected = { + status: "ambiguous", + candidate_resource_keys: resources.map((r) => r.resource_key).sort(), + }; + expect(reconcile(fallback)).toEqual(expected); + const first = reconcile(fallback); + if (first.status !== "ambiguous") throw new Error("Expected ambiguity"); + first.candidate_resource_keys.pop(); + expect(reconcile(fallback)).toEqual(expected); + expect(reconcile({ ...fallback, url: `${script.url}/missing` })).toEqual({ + status: "unmatched", + reason: "no_exact_sanitized_url", + }); + const nextCapture = createCapturedWebScriptReconciler( + [], + resources.slice(0, 1), + ); + expect(nextCapture(fallback)).toEqual({ + status: "ambiguous", + candidate_resource_keys: [resources[0]?.resource_key], + }); + }); +}); diff --git a/src/domain/webInventory.ts b/src/domain/webInventory.ts index 1ea051951e..2cde0a18bc 100644 --- a/src/domain/webInventory.ts +++ b/src/domain/webInventory.ts @@ -67,35 +67,49 @@ export const reconcileWebScript = ( return { status: "unmatched", reason: "no_exact_sanitized_url" }; }; -/** Prefer exact transient raw-URL matching, then report sanitized ambiguity. */ -export const reconcileCapturedWebScript = ( - script: WebInventoryScriptInput & { readonly rawUrl: string }, +/** Index one capture's observations without merging duplicate raw URLs. */ +export const createCapturedWebScriptReconciler = ( rawResources: readonly (WebInventoryResourceInput & { readonly rawUrl: string; })[], resources: readonly StableWebResource[], -): WebResourceReconciliation => { - const exact = rawResources.filter( - (resource) => resource.rawUrl === script.rawUrl, - ); - if (exact.length === 1) { - const resource = exact[0]; - if (resource !== undefined) { +): (( + script: WebInventoryScriptInput & { + readonly rawUrl: string; + }, +) => WebResourceReconciliation) => { + // null means multiple observations, even when their public facts deduplicate. + const rawByUrl = new Map(); + for (const resource of rawResources) + rawByUrl.set( + resource.rawUrl, + rawByUrl.has(resource.rawUrl) ? null : resource, + ); + const keysByUrl = new Map(); + for (const resource of resources) { + const keys = keysByUrl.get(resource.url); + if (keys === undefined) + keysByUrl.set(resource.url, [resource.resource_key]); + else keys.push(resource.resource_key); + } + for (const [url, keys] of keysByUrl) + if (keys.length > 1) keysByUrl.set(url, [...new Set(keys)].sort()); + return (script) => { + const resource = rawByUrl.get(script.rawUrl); + if (resource !== undefined && resource !== null) { const { rawUrl: _rawUrl, ...publicResource } = resource; const stable = stableWebResources([publicResource])[0]; if (stable !== undefined) return { status: "exact", resource_key: stable.resource_key }; } - } - const candidates = resources.filter( - (resource) => resource.url === script.url, - ); - if (candidates.length > 0) - return { - status: "ambiguous", - candidate_resource_keys: uniqueResourceKeys(candidates), - }; - return { status: "unmatched", reason: "no_exact_sanitized_url" }; + const candidates = keysByUrl.get(script.url); + if (candidates !== undefined) + return { + status: "ambiguous", + candidate_resource_keys: [...candidates], + }; + return { status: "unmatched", reason: "no_exact_sanitized_url" }; + }; }; /** Stable script identity excludes target IDs, CDP script IDs, and timestamps. */