mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
perf: reduce MCP launcher and browser capture overhead (#4350)
* perf(browser): 为单次捕获复用脚本资源索引 * docs(setup): 说明多会话直接启动以减少 npm 驻留
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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<string>;
|
||||
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<CapturedScripts> => {
|
||||
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,
|
||||
|
||||
@@ -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],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+34
-20
@@ -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<string, (typeof rawResources)[number] | null>();
|
||||
for (const resource of rawResources)
|
||||
rawByUrl.set(
|
||||
resource.rawUrl,
|
||||
rawByUrl.has(resource.rawUrl) ? null : resource,
|
||||
);
|
||||
const keysByUrl = new Map<string, string[]>();
|
||||
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. */
|
||||
|
||||
Reference in New Issue
Block a user