Files
writing-agent/tools/st-request-diff/capture-server.mjs
T

167 lines
5.1 KiB
JavaScript

/**
* 假上游:实现够用的 OpenAI 兼容端点,把收到的原始请求体原样落盘。
*
* 用法:
* node capture-server.mjs --label st --port 8801
*
* 落盘目录默认 ./captures(相对本文件)。
* 每个请求一个 <label>-<序号>.json,内容 { label, receivedAt, method, url, headers, body }。
* body 是原始字符串,diff 工具再解析 —— 保留原文便于核对是否有我们没建模的字段。
*/
import { createServer } from "node:http";
import { mkdirSync, writeFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const HERE = path.dirname(fileURLToPath(import.meta.url));
function readArgs(argv) {
const out = { label: "unknown", port: 8800, out: path.join(HERE, "captures") };
for (let i = 0; i < argv.length; i++) {
const key = argv[i];
const next = argv[i + 1];
if (key === "--label") out.label = String(next);
else if (key === "--port") out.port = Number(next);
else if (key === "--out") out.out = path.resolve(String(next));
}
return out;
}
const args = readArgs(process.argv.slice(2));
mkdirSync(args.out, { recursive: true });
const REPLY_TEXT = "[[mock-reply]] 收到。";
const REPLY_REASONING = "[[mock-reasoning]] 想一下。";
function capture(req, url, raw) {
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
const seq = String(globalThis.__captureSeq = (globalThis.__captureSeq ?? 0) + 1).padStart(3, "0");
const file = path.join(args.out, `${args.label}-${seq}-${stamp}.json`);
const payload = {
label: args.label,
receivedAt: new Date().toISOString(),
method: req.method,
url,
headers: {
"content-type": req.headers["content-type"] ?? null,
authorization: req.headers.authorization ? "<redacted>" : null,
},
body: raw,
};
writeFileSync(file, JSON.stringify(payload, null, 2), "utf8");
// 只写一行摘要,不把正文打到控制台
let charCount = raw.length;
try {
const parsed = JSON.parse(raw);
charCount = Array.isArray(parsed.messages)
? parsed.messages.reduce((n, m) => n + String(m.content ?? "").length, 0)
: charCount;
} catch {
/* 非 JSON 就按原长度记 */
}
console.log(`[capture:${args.label}] ${req.method} ${url} -> ${path.basename(file)} (messages 正文 ${charCount} 字)`);
return file;
}
function readBody(req) {
return new Promise((resolve, reject) => {
const chunks = [];
req.on("data", (c) => chunks.push(c));
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
req.on("error", reject);
});
}
function sseHead(res) {
res.writeHead(200, {
"Content-Type": "text/event-stream; charset=utf-8",
"Cache-Control": "no-cache",
Connection: "keep-alive",
});
}
function sseChunk(delta, finishReason = null) {
return `data: ${JSON.stringify({
id: "chatcmpl-mock",
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model: "mock-model",
choices: [{ index: 0, delta, finish_reason: finishReason }],
})}\n\n`;
}
const server = createServer(async (req, res) => {
const url = req.url ?? "/";
if (req.method === "GET" && url.includes("/models")) {
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
res.end(
JSON.stringify({
object: "list",
data: [{ id: "mock-model", object: "model", owned_by: "mock" }],
}),
);
return;
}
const raw = await readBody(req);
if (req.method !== "POST") {
res.writeHead(404).end("not found");
return;
}
let body = {};
try {
body = JSON.parse(raw);
} catch {
/* 保留原文,下面按默认处理 */
}
capture(req, url, raw);
if (url.includes("/embeddings")) {
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
res.end(JSON.stringify({ object: "list", data: [{ embedding: [0] }] }));
return;
}
if (url.includes("/completions")) {
if (body.stream) {
sseHead(res);
const chunks = [];
if (body.reasoning_effort && body.reasoning_effort !== "none") {
chunks.push(sseChunk({ role: "assistant", reasoning_content: REPLY_REASONING }));
}
chunks.push(sseChunk({ role: "assistant", content: REPLY_TEXT }));
chunks.push(sseChunk({}, "stop"));
chunks.push("data: [DONE]\n\n");
res.end(chunks.join(""));
return;
}
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
res.end(
JSON.stringify({
id: "chatcmpl-mock",
object: "chat.completion",
created: Math.floor(Date.now() / 1000),
model: "mock-model",
choices: [
{
index: 0,
message: { role: "assistant", content: REPLY_TEXT },
finish_reason: "stop",
},
],
usage: { prompt_tokens: 100, completion_tokens: 10, total_tokens: 110 },
}),
);
return;
}
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
res.end(JSON.stringify({ ok: true }));
});
server.listen(args.port, "127.0.0.1", () => {
console.log(`[capture:${args.label}] listening on http://127.0.0.1:${args.port}/v1 -> ${args.out}`);
});