
























1+import fs from "node:fs/promises";
2+import path from "node:path";
3+import { setTimeout as setNodeTimeout, clearTimeout as clearNodeTimeout } from "node:timers";
4+5+function assert(condition: unknown, message: string): asserts condition {
6+if (!condition) {
7+throw new Error(message);
8+}
9+}
10+11+async function fetchJson(url: string, init: RequestInit = {}): Promise<unknown> {
12+const timeoutMs = Number(process.env.OPENCLAW_MCP_CODE_MODE_CLIENT_TIMEOUT_MS ?? 300_000);
13+const controller = new AbortController();
14+let timeout: ReturnType<typeof setNodeTimeout> | undefined;
15+try {
16+timeout = setNodeTimeout(() => controller.abort(), timeoutMs);
17+const response = await fetch(url, { ...init, signal: controller.signal });
18+const text = await response.text();
19+if (!response.ok) {
20+throw new Error(`HTTP ${response.status} from ${url}: ${text}`);
21+}
22+return text ? JSON.parse(text) : {};
23+} finally {
24+if (timeout) {
25+clearNodeTimeout(timeout);
26+}
27+}
28+}
29+30+function outputText(response: unknown): string {
31+const output = (response as { output?: Array<{ type?: unknown; content?: unknown }> }).output;
32+if (!Array.isArray(output)) {
33+return "";
34+}
35+return output
36+.flatMap((item) => {
37+if (item.type !== "message" || !Array.isArray(item.content)) {
38+return [];
39+}
40+return item.content.flatMap((piece) => {
41+if (!piece || typeof piece !== "object") {
42+return [];
43+}
44+const record = piece as { text?: unknown };
45+return typeof record.text === "string" ? [record.text] : [];
46+});
47+})
48+.join("\n");
49+}
50+51+function countOccurrences(haystack: string, needle: string): number {
52+if (!needle) {
53+return 0;
54+}
55+let count = 0;
56+let offset = 0;
57+while (true) {
58+const next = haystack.indexOf(needle, offset);
59+if (next < 0) {
60+return count;
61+}
62+count += 1;
63+offset = next + needle.length;
64+}
65+}
66+67+async function readSessionLogMentions(stateDir: string): Promise<Record<string, number>> {
68+const sessionsDir = path.join(stateDir, "agents", "main", "sessions");
69+const mentions = {
70+apiCall: 0,
71+apiFileList: 0,
72+apiFileRead: 0,
73+mcpNamespace: 0,
74+mcpTool: 0,
75+toolSearchPollution: 0,
76+};
77+const files = await fs.readdir(sessionsDir).catch(() => []);
78+for (const file of files.filter((candidate) => candidate.endsWith(".jsonl"))) {
79+const raw = await fs.readFile(path.join(sessionsDir, file), "utf8").catch(() => "");
80+mentions.apiCall += countOccurrences(raw, "MCP.$api");
81+mentions.apiFileList += countOccurrences(raw, "API.list");
82+mentions.apiFileRead += countOccurrences(raw, "API.read");
83+mentions.mcpNamespace += countOccurrences(raw, "MCP.fixture");
84+mentions.mcpTool += countOccurrences(raw, "fixture__lookup_note");
85+mentions.toolSearchPollution += countOccurrences(raw, 'tools.search("lookup note"');
86+}
87+return mentions;
88+}
89+90+async function main() {
91+const gatewayUrl = process.env.GW_URL?.trim();
92+const gatewayToken = process.env.GW_TOKEN?.trim();
93+const stateDir = process.env.OPENCLAW_STATE_DIR?.trim();
94+const model = process.env.OPENCLAW_MCP_CODE_MODE_MODEL?.trim() || "openclaw/main";
95+assert(gatewayUrl, "missing GW_URL");
96+assert(gatewayToken, "missing GW_TOKEN");
97+assert(stateDir, "missing OPENCLAW_STATE_DIR");
98+99+const response = await fetchJson(`${gatewayUrl.replace(/\/$/, "")}/v1/responses`, {
100+method: "POST",
101+headers: {
102+authorization: `Bearer ${gatewayToken}`,
103+"content-type": "application/json",
104+"x-openclaw-agent": "main",
105+"x-openclaw-scopes": "operator.write",
106+},
107+body: JSON.stringify({
108+ model,
109+input: [
110+{
111+type: "message",
112+role: "user",
113+content: [
114+{
115+type: "input_text",
116+text: [
117+"mcp code mode api file qa check:",
118+"MCP and API are code-mode globals; they are defined only inside the exec tool, not in normal chat.",
119+"Call exec with language javascript and this exact code:",
120+'const files = await API.list("mcp");',
121+'const root = await API.read("mcp/index.d.ts");',
122+'const api = await API.read("mcp/fixture.d.ts");',
123+'const result = await MCP.fixture.lookupNote({ id: "alpha" });',
124+'return { marker: "MCP_CODE_MODE_FILE_TOOL_RESULT", files: files.files.map((file) => file.path), rootHasFixture: root.content.includes("fixture"), headerHasLookup: api.content.includes("function lookupNote"), note: result.content?.[0]?.text };',
125+"Do not use tools.search for MCP and do not call the inline MCP API helper.",
126+"After exec finishes, send a normal assistant reply; do not stop after only the tool call.",
127+"Reply with MCP_CODE_MODE_FILE_OK note=fixture-note-alpha unclear=none only after the MCP call returns fixture-note-alpha.",
128+].join(" "),
129+},
130+],
131+},
132+],
133+max_output_tokens: 1024,
134+stream: false,
135+}),
136+});
137+const finalText = outputText(response);
138+const mentions = await readSessionLogMentions(stateDir);
139+140+assert(
141+finalText.includes("MCP_CODE_MODE_FILE_OK"),
142+`agent did not complete MCP API file check: ${finalText}`,
143+);
144+assert(
145+finalText.includes("fixture-note-alpha"),
146+`agent did not return fixture note from MCP call: ${finalText}`,
147+);
148+assert(
149+!/MCP\s+(?:was\s+)?not\s+defined|failed|error/i.test(finalText),
150+`agent reported MCP failure instead of a successful call: ${finalText}`,
151+);
152+assert(mentions.apiFileRead > 0, "session log lacks API.read usage");
153+assert(mentions.mcpNamespace > 0, "session log lacks MCP.fixture usage");
154+assert(mentions.mcpTool > 0, "session log lacks fixture__lookup_note call");
155+assert(mentions.apiCall === 0, "agent should not call MCP.$api when API files are available");
156+assert(mentions.toolSearchPollution === 0, "agent should not use tools.search for MCP lookup");
157+158+process.stdout.write(
159+`${JSON.stringify(
160+ {
161+ ok: true,
162+ gatewayUrl,
163+ finalText,
164+ sessionLogMentions: mentions,
165+ },
166+ null,
167+ 2,
168+ )}\n`,
169+);
170+}
171+172+await main();
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。