modelfunction-cli 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +118 -2
- package/fixtures/models.json +39 -0
- package/fixtures/suite.json +150 -0
- package/fixtures/tools/resume_score.json +37 -0
- package/lib/ablate.js +120 -0
- package/lib/adapter-import.js +24 -0
- package/lib/adapters/mock.js +143 -0
- package/lib/adapters/openai.js +95 -0
- package/lib/cases.js +188 -0
- package/lib/compare.js +220 -0
- package/lib/eval-exec.js +140 -0
- package/lib/exec.js +265 -0
- package/lib/fingerprint.js +52 -0
- package/lib/fsutil.js +7 -0
- package/lib/lint.js +110 -0
- package/lib/mcp-probe.js +152 -0
- package/lib/mcp-server.js +222 -0
- package/lib/report.js +119 -0
- package/lib/runtime/harness.mjs +145 -0
- package/lib/runtime/harness.py +373 -0
- package/lib/sandbox-check.js +166 -0
- package/lib/scan.js +510 -0
- package/lib/score.js +137 -0
- package/lib/suite.js +117 -0
- package/mf.js +638 -0
- package/package.json +38 -4
- package/skills/mf-cases/SKILL.md +59 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { loadSuite } from "./suite.js";
|
|
9
|
+
import { scanProject, addTools, buildSuiteDraft, redactReport } from "./scan.js";
|
|
10
|
+
import { writeFileAtomic } from "./fsutil.js";
|
|
11
|
+
import { probeStdioServer } from "./mcp-probe.js";
|
|
12
|
+
import { addCases, computeCoverage } from "./cases.js";
|
|
13
|
+
|
|
14
|
+
const MF_PATH = fileURLToPath(new URL("../mf.js", import.meta.url));
|
|
15
|
+
const PACKAGE_JSON = new URL("../package.json", import.meta.url);
|
|
16
|
+
const MAX_CHILD_OUTPUT = 5_000_000;
|
|
17
|
+
|
|
18
|
+
function json(result) {
|
|
19
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function suitePath(input) {
|
|
23
|
+
return resolve(input ?? "mf.suite.json");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function startMcpServer() {
|
|
27
|
+
const pkg = JSON.parse(await readFile(PACKAGE_JSON, "utf8"));
|
|
28
|
+
const server = new McpServer({ name: "modelfunction", version: pkg.version });
|
|
29
|
+
|
|
30
|
+
server.registerTool(
|
|
31
|
+
"scan_tools",
|
|
32
|
+
{
|
|
33
|
+
title: "Scan tools",
|
|
34
|
+
description:
|
|
35
|
+
"扫描仓库里的 function.json / OpenAPI / MCP 配置 / SKILL.md / 代码内定义。probe=true 时连接 stdio MCP server 拉取 tools/list。不执行工具。默认只读;传 writeSuite 时在该路径写出套件草稿(cases 为空,需补用例并复核)。",
|
|
36
|
+
inputSchema: {
|
|
37
|
+
root: z.string().optional().describe("仓库目录,默认进程当前目录"),
|
|
38
|
+
probe: z.boolean().optional().describe("是否连接 MCP server 拉取工具清单"),
|
|
39
|
+
timeoutMs: z.number().optional().describe("单个 MCP server 超时(毫秒,默认 5000)"),
|
|
40
|
+
writeSuite: z.string().optional().describe("套件草稿输出路径,如 mf.suite.json")
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
async ({ root, probe, timeoutMs, writeSuite }) => {
|
|
44
|
+
const report = await scanProject(root ?? process.cwd());
|
|
45
|
+
if (probe) {
|
|
46
|
+
for (const mcpServer of [...report.mcpServers]) {
|
|
47
|
+
if (mcpServer.transport !== "stdio") {
|
|
48
|
+
report.probes.push({ server: mcpServer.name, ok: false, error: "http transport 暂不支持" });
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const result = await probeStdioServer(mcpServer, { timeoutMs: timeoutMs ?? 5000 });
|
|
52
|
+
if (!result.ok) {
|
|
53
|
+
report.probes.push({ server: mcpServer.name, ok: false, error: result.error });
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const added = addTools(report, result.tools, {
|
|
57
|
+
source: mcpServer.source,
|
|
58
|
+
origin: "mcp-probe",
|
|
59
|
+
confidence: "high",
|
|
60
|
+
note: `server ${mcpServer.name}`
|
|
61
|
+
});
|
|
62
|
+
report.probes.push({ server: mcpServer.name, ok: true, toolCount: added.length, tools: added });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const payload = redactReport(report);
|
|
66
|
+
if (!writeSuite) return json(payload);
|
|
67
|
+
const target = resolve(writeSuite);
|
|
68
|
+
await writeFileAtomic(target, JSON.stringify(buildSuiteDraft(report), null, 2));
|
|
69
|
+
return json({ ...payload, suiteDraftPath: target });
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
server.registerTool(
|
|
74
|
+
"list_suite",
|
|
75
|
+
{
|
|
76
|
+
title: "List suite",
|
|
77
|
+
description: "读取套件概览:工具、用例数、复核状态分布。",
|
|
78
|
+
inputSchema: { suite: z.string().optional().describe("套件路径,默认 ./mf.suite.json") }
|
|
79
|
+
},
|
|
80
|
+
async ({ suite }) => {
|
|
81
|
+
const loaded = await loadSuite(suitePath(suite));
|
|
82
|
+
const sources = {};
|
|
83
|
+
for (const testCase of loaded.cases) {
|
|
84
|
+
sources[testCase.source] = (sources[testCase.source] ?? 0) + 1;
|
|
85
|
+
}
|
|
86
|
+
return json({
|
|
87
|
+
name: loaded.name,
|
|
88
|
+
path: loaded.path,
|
|
89
|
+
tools: loaded.tools.map((tool) => ({
|
|
90
|
+
name: tool.name,
|
|
91
|
+
description: tool.description,
|
|
92
|
+
parameters: Object.keys(tool.parameters?.properties ?? {})
|
|
93
|
+
})),
|
|
94
|
+
cases: loaded.cases.length,
|
|
95
|
+
reviewed: loaded.cases.filter((testCase) => testCase.reviewed !== false).length,
|
|
96
|
+
sources
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
server.registerTool(
|
|
102
|
+
"coverage",
|
|
103
|
+
{
|
|
104
|
+
title: "Case coverage",
|
|
105
|
+
description: "报告每个工具的用例数、参数覆盖、负例比例、疑似混淆的工具对与缺口。",
|
|
106
|
+
inputSchema: { suite: z.string().optional().describe("套件路径,默认 ./mf.suite.json") }
|
|
107
|
+
},
|
|
108
|
+
async ({ suite }) => json(computeCoverage(await loadSuite(suitePath(suite))))
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
server.registerTool(
|
|
112
|
+
"add_cases",
|
|
113
|
+
{
|
|
114
|
+
title: "Add cases",
|
|
115
|
+
description:
|
|
116
|
+
"把编码 Agent 撰写的用例写入套件。默认 source=agent、reviewed=false;未复核用例不计入正式分数。",
|
|
117
|
+
inputSchema: {
|
|
118
|
+
suite: z.string().optional().describe("套件路径,默认 ./mf.suite.json"),
|
|
119
|
+
cases: z.array(z.any()).describe("用例数组,字段 id/input/expectedTool/expectedArgs/note/tags"),
|
|
120
|
+
source: z.string().optional().describe("来源标记,默认 agent"),
|
|
121
|
+
reviewed: z.boolean().optional().describe("是否已复核,默认 false"),
|
|
122
|
+
dryRun: z.boolean().optional().describe("只校验不写入")
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
async ({ suite, cases, source, reviewed, dryRun }) =>
|
|
126
|
+
json(
|
|
127
|
+
await addCases(suitePath(suite), cases, {
|
|
128
|
+
source: source ?? "agent",
|
|
129
|
+
reviewed: reviewed ?? false,
|
|
130
|
+
dryRun: dryRun ?? false
|
|
131
|
+
})
|
|
132
|
+
)
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
server.registerTool(
|
|
136
|
+
"run_eval",
|
|
137
|
+
{
|
|
138
|
+
title: "Run evaluation",
|
|
139
|
+
description:
|
|
140
|
+
"对套件跑评分卡(调用被测模型)。返回 JSON 评分卡;耗时约 用例数 × 模型数 × 单次延迟。",
|
|
141
|
+
inputSchema: {
|
|
142
|
+
suite: z.string().optional().describe("套件路径,默认 ./mf.suite.json"),
|
|
143
|
+
models: z.string().optional().describe("逗号分隔的模型 id,如 mock,dsv4-flash"),
|
|
144
|
+
cases: z.string().optional().describe("只跑这些用例 id(逗号分隔)"),
|
|
145
|
+
failUnder: z.number().optional().describe("选择准确率阈值,低于则退出码为 2")
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
async ({ suite, models, cases, failUnder }) => {
|
|
149
|
+
const args = ["eval", "--json", "--suite", suitePath(suite)];
|
|
150
|
+
if (models) args.push("--models", models);
|
|
151
|
+
if (cases) args.push("--case", cases);
|
|
152
|
+
if (failUnder !== undefined) args.push("--fail-under", String(failUnder));
|
|
153
|
+
|
|
154
|
+
const result = await runCli(args);
|
|
155
|
+
let scorecard = null;
|
|
156
|
+
try {
|
|
157
|
+
scorecard = JSON.parse(result.stdout);
|
|
158
|
+
} catch {
|
|
159
|
+
scorecard = null;
|
|
160
|
+
}
|
|
161
|
+
return json({
|
|
162
|
+
exitCode: result.code,
|
|
163
|
+
scorecard,
|
|
164
|
+
...(result.error ? { error: result.error } : {}),
|
|
165
|
+
...(scorecard ? {} : { stdout: result.stdout.slice(-4000) }),
|
|
166
|
+
stderr: result.stderr.slice(-4000)
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const transport = new StdioServerTransport();
|
|
172
|
+
await server.connect(transport);
|
|
173
|
+
|
|
174
|
+
const shutdown = () => process.exit(0);
|
|
175
|
+
process.stdin.on("end", shutdown);
|
|
176
|
+
process.stdin.on("close", shutdown);
|
|
177
|
+
process.on("SIGINT", shutdown);
|
|
178
|
+
process.on("SIGTERM", shutdown);
|
|
179
|
+
console.error("modelfunction mcp server running on stdio");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function runCli(args, timeoutMs = 300_000) {
|
|
183
|
+
return new Promise((resolvePromise) => {
|
|
184
|
+
const child = spawn(process.execPath, [MF_PATH, ...args], {
|
|
185
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
186
|
+
windowsHide: true
|
|
187
|
+
});
|
|
188
|
+
let stdout = "";
|
|
189
|
+
let stderr = "";
|
|
190
|
+
let settled = false;
|
|
191
|
+
let error = null;
|
|
192
|
+
|
|
193
|
+
const finish = (code) => {
|
|
194
|
+
if (settled) return;
|
|
195
|
+
settled = true;
|
|
196
|
+
clearTimeout(timer);
|
|
197
|
+
try {
|
|
198
|
+
child.kill();
|
|
199
|
+
} catch {
|
|
200
|
+
// ignore
|
|
201
|
+
}
|
|
202
|
+
resolvePromise({ code, stdout, stderr, error });
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const timer = setTimeout(() => {
|
|
206
|
+
error = `timeout after ${timeoutMs}ms`;
|
|
207
|
+
finish(-1);
|
|
208
|
+
}, timeoutMs);
|
|
209
|
+
|
|
210
|
+
child.stdout.on("data", (chunk) => {
|
|
211
|
+
if (stdout.length < MAX_CHILD_OUTPUT) stdout += chunk.toString("utf8");
|
|
212
|
+
});
|
|
213
|
+
child.stderr.on("data", (chunk) => {
|
|
214
|
+
if (stderr.length < MAX_CHILD_OUTPUT) stderr += chunk.toString("utf8");
|
|
215
|
+
});
|
|
216
|
+
child.on("error", (err) => {
|
|
217
|
+
error = `spawn failed: ${err.message}`;
|
|
218
|
+
finish(-1);
|
|
219
|
+
});
|
|
220
|
+
child.on("exit", (code) => finish(code ?? -1));
|
|
221
|
+
});
|
|
222
|
+
}
|
package/lib/report.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { pct } from "./score.js";
|
|
2
|
+
import { suiteFingerprint } from "./fingerprint.js";
|
|
3
|
+
|
|
4
|
+
function money(value, estimated) {
|
|
5
|
+
if (value === 0) return "0";
|
|
6
|
+
const text = value >= 0.01 ? value.toFixed(3) : value.toFixed(4);
|
|
7
|
+
return `${estimated ? "~" : ""}${text}`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function isEstimated(summary) {
|
|
11
|
+
return summary.rows.some((row) => row.usage?.estimated);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function formatCaseLines(summary, cases) {
|
|
15
|
+
const lines = [];
|
|
16
|
+
const byId = new Map(cases.map((testCase) => [testCase.id, testCase]));
|
|
17
|
+
for (const row of summary.rows) {
|
|
18
|
+
const testCase = byId.get(row.id);
|
|
19
|
+
const expected = row.expectedTool ?? "(no tool)";
|
|
20
|
+
if (row.error) {
|
|
21
|
+
lines.push(`FAIL ${row.id} error: ${row.error}`);
|
|
22
|
+
} else if (!row.selectionCorrect) {
|
|
23
|
+
lines.push(`FAIL ${row.id} expected ${expected}, got ${row.actualTool ?? "(no tool)"}`);
|
|
24
|
+
} else if (row.args.failures.length > 0) {
|
|
25
|
+
lines.push(`FAIL ${row.id} ${row.actualTool} args mismatch: ${row.args.failures.join(", ")}`);
|
|
26
|
+
} else {
|
|
27
|
+
lines.push(`OK ${row.id} ${row.actualTool ?? "(no tool)"}`);
|
|
28
|
+
}
|
|
29
|
+
if (testCase?.note) lines.push(` note: ${testCase.note}`);
|
|
30
|
+
}
|
|
31
|
+
return lines;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function formatTable(summaries) {
|
|
35
|
+
const headers = ["model", "select", "args", "pass", "p50ms", "p95ms", "cny/call", "errors"];
|
|
36
|
+
const rows = summaries.map((summary) => [
|
|
37
|
+
summary.model,
|
|
38
|
+
pct(summary.selectionAccuracy),
|
|
39
|
+
pct(summary.argAccuracy),
|
|
40
|
+
pct(summary.passRate),
|
|
41
|
+
String(summary.p50LatencyMs),
|
|
42
|
+
String(summary.p95LatencyMs),
|
|
43
|
+
money(summary.avgCostCny, isEstimated(summary)),
|
|
44
|
+
String(summary.errors)
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
const widths = headers.map((header, index) =>
|
|
48
|
+
Math.max(header.length, ...rows.map((row) => row[index].length))
|
|
49
|
+
);
|
|
50
|
+
const pad = (value, index) => (index === 0 ? value.padEnd(widths[index]) : value.padStart(widths[index]));
|
|
51
|
+
const line = (cells) => cells.map(pad).join(" ");
|
|
52
|
+
const divider = widths.map((width) => "-".repeat(width)).join(" ");
|
|
53
|
+
|
|
54
|
+
return [line(headers), divider, ...rows.map(line)].join("\n");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function formatMarkdown(suite, cases, summaries) {
|
|
58
|
+
const fingerprint = suiteFingerprint(suite, cases);
|
|
59
|
+
const lines = [
|
|
60
|
+
`# ${suite.name} — function-calling scorecard`,
|
|
61
|
+
"",
|
|
62
|
+
(suite.description || "").trim(),
|
|
63
|
+
"",
|
|
64
|
+
`- Suite: \`${suite.path}\``,
|
|
65
|
+
`- Cases: ${cases.length}`,
|
|
66
|
+
`- Tools: ${suite.tools.map((tool) => tool.name).join(", ")}`,
|
|
67
|
+
`- Fingerprint: \`${fingerprint.hash}\``,
|
|
68
|
+
`- Tool versions: ${fingerprint.tools.map((tool) => `${tool.name}@${tool.hash}`).join(", ")}`,
|
|
69
|
+
""
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
const withExec = summaries.some((summary) => summary.exec);
|
|
73
|
+
const headers = withExec
|
|
74
|
+
? ["model", "select", "args", "pass", "exec", "run p50 ms", "max RSS MB", "p50 ms", "p95 ms", "CNY/call", "errors"]
|
|
75
|
+
: ["model", "select", "args", "pass", "p50 ms", "p95 ms", "CNY/call", "errors"];
|
|
76
|
+
lines.push(`| ${headers.join(" | ")} |`, `| ${headers.map(() => "---").join(" | ")} |`);
|
|
77
|
+
|
|
78
|
+
for (const summary of summaries) {
|
|
79
|
+
const cells = [
|
|
80
|
+
summary.model,
|
|
81
|
+
pct(summary.selectionAccuracy),
|
|
82
|
+
pct(summary.argAccuracy),
|
|
83
|
+
pct(summary.passRate)
|
|
84
|
+
];
|
|
85
|
+
if (withExec) {
|
|
86
|
+
cells.push(summary.exec ? `${summary.exec.passed}/${summary.exec.executed}` : "-");
|
|
87
|
+
cells.push(summary.exec?.p50DurationMs ?? "-");
|
|
88
|
+
cells.push(summary.exec?.maxPeakRssMB ?? "-");
|
|
89
|
+
}
|
|
90
|
+
cells.push(
|
|
91
|
+
String(summary.p50LatencyMs),
|
|
92
|
+
String(summary.p95LatencyMs),
|
|
93
|
+
money(summary.avgCostCny, isEstimated(summary)),
|
|
94
|
+
String(summary.errors)
|
|
95
|
+
);
|
|
96
|
+
lines.push(`| ${cells.join(" | ")} |`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
for (const summary of summaries) {
|
|
100
|
+
lines.push(
|
|
101
|
+
"",
|
|
102
|
+
`## ${summary.model}(reviewed ${summary.reviewedTotal}/${summary.total})`,
|
|
103
|
+
"",
|
|
104
|
+
"| case | expected | got | args | result |",
|
|
105
|
+
"| --- | --- | --- | --- | --- |"
|
|
106
|
+
);
|
|
107
|
+
for (const row of summary.rows) {
|
|
108
|
+
const result = row.error ? `error: ${row.error}` : row.passed ? "pass" : "fail";
|
|
109
|
+
const args = row.args.total
|
|
110
|
+
? `${row.args.matched}/${row.args.total}${row.args.failures.length ? ` (miss: ${row.args.failures.join(", ")})` : ""}`
|
|
111
|
+
: "-";
|
|
112
|
+
lines.push(
|
|
113
|
+
`| ${row.id} | ${row.expectedTool ?? "(none)"} | ${row.actualTool ?? "(none)"} | ${args} | ${result} |`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return lines.join("\n");
|
|
119
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { readFileSync, writeSync } from "node:fs";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
|
|
4
|
+
const args = parseArgs(process.argv.slice(2));
|
|
5
|
+
|
|
6
|
+
function parseArgs(argv) {
|
|
7
|
+
const parsed = { entry: null, input: null, exportName: "run", warmup: 0 };
|
|
8
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
9
|
+
const arg = argv[i];
|
|
10
|
+
const next = () => {
|
|
11
|
+
const value = argv[i + 1];
|
|
12
|
+
if (value === undefined) throw new Error(`missing value for ${arg}`);
|
|
13
|
+
i += 1;
|
|
14
|
+
return value;
|
|
15
|
+
};
|
|
16
|
+
if (arg === "--entry") parsed.entry = next();
|
|
17
|
+
else if (arg === "--input") parsed.input = next();
|
|
18
|
+
else if (arg === "--export") parsed.exportName = next();
|
|
19
|
+
else if (arg === "--warmup") parsed.warmup = Number(next()) || 0;
|
|
20
|
+
}
|
|
21
|
+
return parsed;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function emit(payload) {
|
|
25
|
+
const line = `${JSON.stringify(payload)}\n`;
|
|
26
|
+
try {
|
|
27
|
+
writeSync(3, line);
|
|
28
|
+
} catch {
|
|
29
|
+
process.stdout.write(line);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function metrics({ durationMs, importMs, ruBefore, muBefore, peakRss, startRss }) {
|
|
34
|
+
const ru = process.resourceUsage();
|
|
35
|
+
const mu = process.memoryUsage();
|
|
36
|
+
return {
|
|
37
|
+
durationMs: Math.round(durationMs * 100) / 100,
|
|
38
|
+
importMs: Math.round(importMs * 100) / 100,
|
|
39
|
+
cpuUserMs: Math.round((ru.userCPUTime - ruBefore.userCPUTime) / 1000),
|
|
40
|
+
cpuSystemMs: Math.round((ru.systemCPUTime - ruBefore.systemCPUTime) / 1000),
|
|
41
|
+
fsReadDelta: ru.fsRead - ruBefore.fsRead,
|
|
42
|
+
fsWriteDelta: ru.fsWrite - ruBefore.fsWrite,
|
|
43
|
+
startRssMB: Math.round((startRss / 1048576) * 10) / 10,
|
|
44
|
+
peakRssMB: Math.round((Math.max(peakRss, mu.rss) / 1048576) * 10) / 10,
|
|
45
|
+
heapUsedDeltaMB: Math.round(((mu.heapUsed - muBefore.heapUsed) / 1048576) * 100) / 100
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!args.entry || !args.input) {
|
|
50
|
+
emit({ ok: false, error: { name: "HarnessError", code: "BAD_ARGS", message: "--entry and --input are required" } });
|
|
51
|
+
process.exit(2);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const startRss = process.memoryUsage().rss;
|
|
55
|
+
const importStarted = performance.now();
|
|
56
|
+
let fn;
|
|
57
|
+
try {
|
|
58
|
+
const moduleUrl = pathToFileURL(args.entry).href;
|
|
59
|
+
const mod = await import(moduleUrl);
|
|
60
|
+
const candidate = mod[args.exportName] ?? mod.default ?? mod.run;
|
|
61
|
+
if (typeof candidate !== "function") {
|
|
62
|
+
emit({
|
|
63
|
+
ok: false,
|
|
64
|
+
error: {
|
|
65
|
+
name: "HarnessError",
|
|
66
|
+
code: "EXPORT_NOT_FOUND",
|
|
67
|
+
message: `${args.entry} 需要导出 ${args.exportName}、default 或 run 函数`
|
|
68
|
+
},
|
|
69
|
+
metrics: { importMs: Math.round((performance.now() - importStarted) * 100) / 100 }
|
|
70
|
+
});
|
|
71
|
+
process.exit(2);
|
|
72
|
+
}
|
|
73
|
+
fn = candidate;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
emit({
|
|
76
|
+
ok: false,
|
|
77
|
+
error: { name: error?.name, code: error?.code, message: error?.message, phase: "import" },
|
|
78
|
+
metrics: { importMs: Math.round((performance.now() - importStarted) * 100) / 100 }
|
|
79
|
+
});
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
const importMs = performance.now() - importStarted;
|
|
83
|
+
|
|
84
|
+
let input;
|
|
85
|
+
try {
|
|
86
|
+
input = JSON.parse(readFileSync(args.input, "utf8"));
|
|
87
|
+
} catch (error) {
|
|
88
|
+
emit({
|
|
89
|
+
ok: false,
|
|
90
|
+
error: { name: error?.name, code: error?.code, message: `input 读取失败: ${error?.message}`, phase: "input" },
|
|
91
|
+
metrics: { importMs }
|
|
92
|
+
});
|
|
93
|
+
process.exit(2);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let peakRss = process.memoryUsage().rss;
|
|
97
|
+
const sampler = setInterval(() => {
|
|
98
|
+
peakRss = Math.max(peakRss, process.memoryUsage().rss);
|
|
99
|
+
}, 10);
|
|
100
|
+
if (typeof sampler.unref === "function") sampler.unref();
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
for (let i = 0; i < args.warmup; i += 1) {
|
|
104
|
+
await fn(input);
|
|
105
|
+
}
|
|
106
|
+
} catch (error) {
|
|
107
|
+
clearInterval(sampler);
|
|
108
|
+
emit({
|
|
109
|
+
ok: false,
|
|
110
|
+
error: { name: error?.name, code: error?.code, message: error?.message, phase: "warmup" },
|
|
111
|
+
metrics: { importMs, durationMs: 0, startRssMB: Math.round((startRss / 1048576) * 10) / 10 }
|
|
112
|
+
});
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const ruBefore = process.resourceUsage();
|
|
117
|
+
const muBefore = process.memoryUsage();
|
|
118
|
+
const started = performance.now();
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const result = await fn(input);
|
|
122
|
+
const durationMs = performance.now() - started;
|
|
123
|
+
clearInterval(sampler);
|
|
124
|
+
emit({
|
|
125
|
+
ok: true,
|
|
126
|
+
result,
|
|
127
|
+
metrics: metrics({ durationMs, importMs, ruBefore, muBefore, peakRss, startRss })
|
|
128
|
+
});
|
|
129
|
+
process.exit(0);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
const durationMs = performance.now() - started;
|
|
132
|
+
clearInterval(sampler);
|
|
133
|
+
emit({
|
|
134
|
+
ok: false,
|
|
135
|
+
error: {
|
|
136
|
+
name: error?.name,
|
|
137
|
+
code: error?.code,
|
|
138
|
+
message: error?.message,
|
|
139
|
+
stack: String(error?.stack ?? "").split("\n").slice(0, 10).join("\n"),
|
|
140
|
+
phase: "call"
|
|
141
|
+
},
|
|
142
|
+
metrics: metrics({ durationMs, importMs, ruBefore, muBefore, peakRss, startRss })
|
|
143
|
+
});
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|