






















@@ -11,6 +11,10 @@ const OUTPUT_CAPTURE_CHARS = readPositiveInt(
1111process.env.OPENCLAW_BUNDLED_PLUGIN_RUNTIME_OUTPUT_CHARS,
12121024 * 1024,
1313);
14+const LOG_SCAN_BYTES = readPositiveInt(
15+process.env.OPENCLAW_BUNDLED_PLUGIN_RUNTIME_LOG_SCAN_BYTES,
16+256 * 1024,
17+);
1418const WATCHDOG_MS = readPositiveInt(process.env.OPENCLAW_BUNDLED_PLUGIN_RUNTIME_WATCHDOG_MS, 1000);
1519const READY_TIMEOUT_MS = readPositiveInt(
1620process.env.OPENCLAW_BUNDLED_PLUGIN_RUNTIME_READY_MS,
@@ -29,6 +33,13 @@ const HTTP_PROBE_TIMEOUT_MS = readPositiveInt(
2933process.env.OPENCLAW_BUNDLED_PLUGIN_RUNTIME_HTTP_MS,
30345000,
3135);
36+const GATEWAY_READY_LOG_NEEDLE = Buffer.from("[gateway] ready");
37+const READY_OFFSET_LOG_NEEDLES = [
38+GATEWAY_READY_LOG_NEEDLE,
39+Buffer.from("listening on ws://"),
40+Buffer.from("[gateway] http server listening"),
41+];
42+const FORBIDDEN_POST_READY_DEPS_WORK = [/\b(?:npm|pnpm|yarn|corepack) install\b/iu];
32433344function readPositiveInt(raw, fallback) {
3445const parsed = Number.parseInt(String(raw || ""), 10);
@@ -44,6 +55,132 @@ function writeJson(file, value) {
4455fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
4556}
465758+function readFileChunk(file, startOffset, maxBytes) {
59+let stat;
60+try {
61+stat = fs.statSync(file);
62+} catch {
63+return { buffer: Buffer.alloc(0), startOffset: 0, size: 0 };
64+}
65+if (!stat.isFile() || stat.size <= 0) {
66+return { buffer: Buffer.alloc(0), startOffset: 0, size: stat.size };
67+}
68+69+const safeMaxBytes = Math.max(1, Math.floor(Number(maxBytes) || LOG_SCAN_BYTES));
70+const safeStartOffset = Math.min(Math.max(0, Math.floor(Number(startOffset) || 0)), stat.size);
71+const bytesToRead = Math.min(safeMaxBytes, stat.size - safeStartOffset);
72+if (bytesToRead <= 0) {
73+return { buffer: Buffer.alloc(0), startOffset: safeStartOffset, size: stat.size };
74+}
75+76+const buffer = Buffer.alloc(bytesToRead);
77+const fd = fs.openSync(file, "r");
78+try {
79+const bytesRead = fs.readSync(fd, buffer, 0, bytesToRead, safeStartOffset);
80+return { buffer: buffer.subarray(0, bytesRead), startOffset: safeStartOffset, size: stat.size };
81+} finally {
82+fs.closeSync(fd);
83+}
84+}
85+86+function readFileTailBuffer(file, maxBytes = LOG_SCAN_BYTES) {
87+let stat;
88+try {
89+stat = fs.statSync(file);
90+} catch {
91+return { buffer: Buffer.alloc(0), startOffset: 0, size: 0 };
92+}
93+const safeMaxBytes = Math.max(1, Math.floor(Number(maxBytes) || LOG_SCAN_BYTES));
94+const startOffset = Math.max(0, stat.size - safeMaxBytes);
95+return readFileChunk(file, startOffset, safeMaxBytes);
96+}
97+98+export function readFileTail(file, maxBytes = LOG_SCAN_BYTES) {
99+return readFileTailBuffer(file, maxBytes).buffer.toString("utf8");
100+}
101+102+function findFirstNeedleOffset(file, needles) {
103+let stat;
104+try {
105+stat = fs.statSync(file);
106+} catch {
107+return 0;
108+}
109+if (!stat.isFile() || stat.size <= 0) {
110+return 0;
111+}
112+113+const carryBytes = Math.max(0, ...needles.map((needle) => needle.length - 1));
114+const chunk = Buffer.alloc(Math.min(LOG_SCAN_BYTES, stat.size));
115+const fd = fs.openSync(file, "r");
116+let carry = Buffer.alloc(0);
117+let offset = 0;
118+try {
119+while (offset < stat.size) {
120+const bytesToRead = Math.min(chunk.length, stat.size - offset);
121+const bytesRead = fs.readSync(fd, chunk, 0, bytesToRead, offset);
122+if (bytesRead <= 0) {
123+break;
124+}
125+const view = chunk.subarray(0, bytesRead);
126+const combined = carry.length > 0 ? Buffer.concat([carry, view]) : view;
127+const combinedOffset = offset - carry.length;
128+const indexes = needles
129+.map((needle) => combined.indexOf(needle))
130+.filter((index) => index >= 0);
131+if (indexes.length > 0) {
132+return combinedOffset + Math.min(...indexes);
133+}
134+carry = combined.subarray(Math.max(0, combined.length - carryBytes));
135+offset += bytesRead;
136+}
137+} finally {
138+fs.closeSync(fd);
139+}
140+return 0;
141+}
142+143+export function createReadyLogScanner(file) {
144+const carryBytes = GATEWAY_READY_LOG_NEEDLE.length - 1;
145+let carry = Buffer.alloc(0);
146+let offset = 0;
147+let seen = false;
148+149+return () => {
150+if (seen) {
151+return true;
152+}
153+let stat;
154+try {
155+stat = fs.statSync(file);
156+} catch {
157+return false;
158+}
159+if (!stat.isFile() || stat.size <= 0) {
160+return false;
161+}
162+if (stat.size < offset) {
163+carry = Buffer.alloc(0);
164+offset = 0;
165+}
166+while (offset < stat.size) {
167+const { buffer } = readFileChunk(file, offset, LOG_SCAN_BYTES);
168+if (buffer.length === 0) {
169+break;
170+}
171+const combined = carry.length > 0 ? Buffer.concat([carry, buffer]) : buffer;
172+const matched = combined.includes(GATEWAY_READY_LOG_NEEDLE);
173+if (matched) {
174+seen = true;
175+return true;
176+}
177+carry = combined.subarray(Math.max(0, combined.length - carryBytes));
178+offset += buffer.length;
179+}
180+return false;
181+};
182+}
183+47184function manifestPath(pluginDir, pluginRoot) {
48185const candidates = [
49186 ...(isNonEmptyString(pluginRoot) ? [path.join(pluginRoot, "openclaw.plugin.json")] : []),
@@ -278,6 +415,7 @@ async function stopGateway(child) {
278415async function waitForReady(params) {
279416const started = Date.now();
280417let lastError = "";
418+const readyLogSeen = createReadyLogScanner(params.logPath);
281419while (Date.now() - started < READY_TIMEOUT_MS) {
282420if (params.child.exitCode !== null) {
283421throw new Error(`gateway exited before ready\n${tailFile(params.logPath)}`);
@@ -291,19 +429,14 @@ async function waitForReady(params) {
291429} catch (error) {
292430lastError = error instanceof Error ? error.message : String(error);
293431}
294-if (logShowsGatewayReady(params.logPath) && (await httpOk(params.port, "/healthz"))) {
432+if (readyLogSeen() && (await httpOk(params.port, "/healthz"))) {
295433return;
296434}
297435await delay(250);
298436}
299437throw new Error(`gateway did not become ready: ${lastError}\n${tailFile(params.logPath)}`);
300438}
301439302-function logShowsGatewayReady(logPath) {
303-const log = fs.existsSync(logPath) ? fs.readFileSync(logPath, "utf8") : "";
304-return log.includes("[gateway] ready");
305-}
306-307440async function fetchHttpProbeStatus(port, pathName, options = {}) {
308441const { timeoutMs = HTTP_PROBE_TIMEOUT_MS } = options;
309442const controller = new AbortController();
@@ -638,32 +771,47 @@ function assertSpeechProviderVisible(payload, provider, label) {
638771}
639772640773async function runWatchdog(options) {
641-const readyIndex = findReadyLogIndex(options.logPath);
774+const readyOffset = findReadyLogOffset(options.logPath);
642775await delay(WATCHDOG_MS);
643776if (options.child.exitCode !== null) {
644777throw new Error(
645778`gateway exited after ready for ${options.pluginId}\n${tailFile(options.logPath)}`,
646779);
647780}
648781await retryRpcCall("health", {}, options);
649-assertNoPostReadyRuntimeDepsWork(options.logPath, readyIndex);
782+assertNoPostReadyRuntimeDepsWork(options.logPath, readyOffset);
650783await assertNoPackageManagerChildren(options.child.pid);
651784}
652785653-function findReadyLogIndex(logPath) {
654-const log = fs.existsSync(logPath) ? fs.readFileSync(logPath, "utf8") : "";
655-const candidates = ["[gateway] ready", "listening on ws://", "[gateway] http server listening"];
656-const indexes = candidates.map((needle) => log.indexOf(needle)).filter((index) => index >= 0);
657-return indexes.length > 0 ? Math.min(...indexes) : 0;
786+export function findReadyLogOffset(logPath) {
787+return findFirstNeedleOffset(logPath, READY_OFFSET_LOG_NEEDLES);
658788}
659789660-function assertNoPostReadyRuntimeDepsWork(logPath, readyIndex) {
661-const log = fs.existsSync(logPath) ? fs.readFileSync(logPath, "utf8") : "";
662-const postReady = log.slice(Math.max(0, readyIndex));
663-const forbidden = [/\b(?:npm|pnpm|yarn|corepack) install\b/iu];
664-const match = forbidden.find((pattern) => pattern.test(postReady));
665-if (match) {
666-throw new Error(`post-ready runtime dependency work matched ${match}: ${tailText(postReady)}`);
790+export function assertNoPostReadyRuntimeDepsWork(logPath, readyOffset) {
791+let stat;
792+try {
793+stat = fs.statSync(logPath);
794+} catch {
795+return;
796+}
797+if (!stat.isFile() || stat.size <= 0) {
798+return;
799+}
800+801+let offset = Math.min(Math.max(0, Math.floor(Number(readyOffset) || 0)), stat.size);
802+let carry = "";
803+while (offset < stat.size) {
804+const { buffer } = readFileChunk(logPath, offset, LOG_SCAN_BYTES);
805+if (buffer.length === 0) {
806+break;
807+}
808+const text = carry + buffer.toString("utf8");
809+const match = FORBIDDEN_POST_READY_DEPS_WORK.find((pattern) => pattern.test(text));
810+if (match) {
811+throw new Error(`post-ready runtime dependency work matched ${match}: ${tailText(text)}`);
812+}
813+carry = text.slice(-256);
814+offset += buffer.length;
667815}
668816}
669817@@ -831,10 +979,7 @@ export function createIsolatedStateEnv(label) {
831979}
832980833981function tailFile(file) {
834-if (!fs.existsSync(file)) {
835-return "";
836-}
837-return tailText(fs.readFileSync(file, "utf8"));
982+return tailText(readFileTail(file));
838983}
839984840985function tailText(text) {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。