






















@@ -496,9 +496,11 @@ const fs = require("node:fs");
496496const jsonl = process.argv[2];
497497const required = new Set(process.argv.slice(3));
498498499-const raw = fs.readFileSync(jsonl, "utf8");
500-const lines = raw.split("\n").map((l) => l.trim()).filter(Boolean);
501499const seen = new Set();
500+const head = [];
501+let scannedBytes = 0;
502+let truncated = false;
503+let skippedOversizedLines = 0;
502504503505const toolTypes = new Set([
504506 "tool_use",
@@ -511,10 +513,30 @@ const toolTypes = new Set([
511513 "toolresult",
512514 "tool-result",
513515]);
514-function walk(node, parent) {
516+function readPositiveIntEnv(name, fallback) {
517+ const raw = process.env[name];
518+ if (raw === undefined || raw === "") return fallback;
519+ if (!/^[1-9][0-9]*$/.test(raw)) {
520+ console.error(`${name} must be a positive integer`);
521+ process.exit(2);
522+ }
523+ return Number(raw);
524+}
525+const maxBytes = readPositiveIntEnv("OPENCLAW_INSTALL_E2E_SESSION_SCAN_BYTES", 16 * 1024 * 1024);
526+const maxLineBytes = readPositiveIntEnv("OPENCLAW_INSTALL_E2E_SESSION_LINE_BYTES", 1024 * 1024);
527+const maxDepth = readPositiveIntEnv("OPENCLAW_INSTALL_E2E_SESSION_SCAN_DEPTH", 64);
528+const maxNodes = readPositiveIntEnv("OPENCLAW_INSTALL_E2E_SESSION_SCAN_NODES", 100000);
529+530+function missingTools() {
531+ return [...required].filter((t) => !seen.has(t));
532+}
533+534+function walk(node, depth, state) {
515535 if (!node) return;
536+ if (depth > maxDepth || state.nodes >= maxNodes) return;
537+ state.nodes += 1;
516538 if (Array.isArray(node)) {
517- for (const item of node) walk(item, node);
539+ for (const item of node) walk(item, depth + 1, state);
518540 return;
519541 }
520542 if (typeof node !== "object") return;
@@ -542,26 +564,113 @@ function walk(node, parent) {
542564 }
543565 }
544566 if (obj.function && typeof obj.function.name === "string") seen.add(obj.function.name);
545- for (const v of Object.values(obj)) walk(v, obj);
567+ for (const v of Object.values(obj)) walk(v, depth + 1, state);
546568}
547569548-for (const line of lines) {
570+function processLine(lineBuffer) {
571+ const line = lineBuffer.toString("utf8").trim();
572+ if (!line) return;
573+ if (head.length < 5) head.push(line.slice(0, maxLineBytes));
549574 try {
550575 const entry = JSON.parse(line);
551- walk(entry, null);
576+ walk(entry, 0, { nodes: 0 });
552577 } catch {
553578 // ignore unparsable lines
554579 }
555580}
556581557-const missing = [...required].filter((t) => !seen.has(t));
558-if (missing.length > 0) {
559- console.error(`Missing tools in transcript: ${missing.join(", ")}`);
560- console.error(`Seen tools: ${[...seen].sort().join(", ")}`);
561- console.error("Transcript head:");
562- console.error(lines.slice(0, 5).join("\n"));
563- process.exit(1);
582+async function scan() {
583+ const stream = fs.createReadStream(jsonl, { highWaterMark: 64 * 1024 });
584+ let lineParts = [];
585+ let lineBytes = 0;
586+ let skippingLine = false;
587+588+ function resetLine() {
589+ lineParts = [];
590+ lineBytes = 0;
591+ skippingLine = false;
592+ }
593+594+ function appendLineChunk(chunk) {
595+ if (skippingLine) return;
596+ if (lineBytes + chunk.length > maxLineBytes) {
597+ skippedOversizedLines += 1;
598+ lineParts = [];
599+ lineBytes = 0;
600+ skippingLine = true;
601+ return;
602+ }
603+ lineParts.push(chunk);
604+ lineBytes += chunk.length;
605+ }
606+607+ function finishLine() {
608+ if (!skippingLine && lineBytes > 0) {
609+ processLine(Buffer.concat(lineParts, lineBytes));
610+ }
611+ resetLine();
612+ }
613+614+ for await (const rawChunk of stream) {
615+ let chunk = rawChunk;
616+ let stopAfterChunk = false;
617+ if (scannedBytes + rawChunk.length > maxBytes) {
618+ const remaining = Math.max(0, maxBytes - scannedBytes);
619+ chunk = rawChunk.subarray(0, remaining);
620+ truncated = true;
621+ stopAfterChunk = true;
622+ }
623+ scannedBytes += chunk.length;
624+ let offset = 0;
625+ while (offset < chunk.length) {
626+ const newline = chunk.indexOf(10, offset);
627+ const end = newline === -1 ? chunk.length : newline;
628+ if (end > offset) {
629+ appendLineChunk(chunk.subarray(offset, end));
630+ }
631+ if (newline === -1) {
632+ break;
633+ }
634+ finishLine();
635+ if (missingTools().length === 0) {
636+ stream.destroy();
637+ return;
638+ }
639+ offset = newline + 1;
640+ }
641+ if (stopAfterChunk) {
642+ stream.destroy();
643+ break;
644+ }
645+ }
646+ if (!truncated && lineBytes > 0) {
647+ finishLine();
648+ }
564649}
650+651+scan()
652+ .then(() => {
653+ const missing = missingTools();
654+ if (missing.length > 0) {
655+ console.error(`Missing tools in transcript: ${missing.join(", ")}`);
656+ console.error(`Seen tools: ${[...seen].sort().join(", ")}`);
657+ if (truncated) {
658+ console.error(`Transcript scan stopped after ${maxBytes} bytes`);
659+ }
660+ if (skippedOversizedLines > 0) {
661+ console.error(`Skipped ${skippedOversizedLines} oversized transcript line(s)`);
662+ }
663+ console.error("Transcript head:");
664+ console.error(head.join("\n"));
665+ process.exit(1);
666+ }
667+ })
668+ .catch((error) => {
669+ console.error(
670+ `Could not scan transcript ${jsonl}: ${error instanceof Error ? error.message : String(error)}`,
671+ );
672+ process.exit(1);
673+ });
565674NODE
566675}
567676此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。