Files
writing-agent/tools/st-request-diff/probe-st-items.mjs
T
chenmoranzhi 1a5ef3b2bc 抓包对比工具补一批一次性脚本:拆预设、对请求、跑 A/B 与拒答抽样。
这些脚本都是我对着酒馆真实请求手工核对用的,留在仓库里方便下次重跑;跑出来的本地数据目录含接口 key,已经写进忽略规则。
2026-10-26 01:09:56 +08:00

132 lines
6.4 KiB
JavaScript

// 探针挡:起假上游 + 酒馆 + 无头浏览器,导入预设后挂 chat_completion_prompt_ready 钩子,
// 把酒馆「最终要发出去」的消息表(含 name / identifier)原样打出来,用来定语义。
// 用法: node probe-st-items.mjs --preset "C:/Users/zed/Downloads/简单预设_V2.0.json" [--st-port 8124]
import { spawn } from "node:child_process";
import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { evaluate, launchBrowser, openPage, sleep } from "./cdp.mjs";
import { makeLogger, parseArgs, waitHttp, waitStReady, writeCharacter } from "./lib.mjs";
const HERE = path.dirname(fileURLToPath(import.meta.url));
const log = makeLogger("probe-st");
const DEFAULTS = {
preset: "",
capturePort: 8801,
stPort: 8124,
stRoot: "D:/game/sillytavern",
dataRoot: path.join(HERE, ".st-data"),
out: path.join(HERE, "captures"),
card: "tokens",
timeoutMs: 180000,
};
const closePopups = `(() => { document.querySelectorAll(".popup-button-cancel, .popup-button-ok").forEach((b) => b.click()); return true; })()`;
async function main() {
const args = parseArgs(process.argv.slice(2), DEFAULTS);
if (!existsSync(args.preset)) throw new Error("预设文件不存在: " + args.preset);
mkdirSync(args.out, { recursive: true });
const presetName = path.basename(args.preset, ".json");
const presetText = readFileSync(args.preset, "utf8");
const cap = spawn(process.execPath, [path.join(HERE, "capture-server.mjs"), "--label", "probe", "--port", String(args.capturePort), "--out", args.out], { cwd: HERE, stdio: "ignore" });
await waitHttp("http://127.0.0.1:" + args.capturePort + "/v1/models", 15000, "假上游起来");
const cardName = "ProbeCard";
writeCharacter(args.dataRoot, cardName, args.card);
try { rmSync(path.join(args.dataRoot, "default-user", "OpenAI Settings", presetName + ".json"), { force: true }); } catch (e) {}
const st = spawn(process.execPath, ["server.js", "--port", String(args.stPort), "--dataRoot", args.dataRoot, "--browserLaunchEnabled=false"], { cwd: args.stRoot, stdio: ["ignore", "pipe", "pipe"] });
let browser;
try {
await waitHttp("http://127.0.0.1:" + args.stPort + "/", args.timeoutMs, "酒馆起来");
browser = await launchBrowser({ port: args.stPort + 100, headless: true });
const page = await openPage(browser, "http://127.0.0.1:" + args.stPort + "/");
await waitStReady(page, args.timeoutMs, log);
await evaluate(page, `(async () => {
const input = document.getElementById("openai_preset_import_file");
const file = new File([${JSON.stringify(presetText)}], ${JSON.stringify(presetName + ".json")}, { type: "application/json" });
const dt = new DataTransfer();
dt.items.add(file);
input.files = dt.files;
window.jQuery(input).trigger("input");
return true;
})()`);
for (let i = 0; i < 120; i++) {
const s = await evaluate(page, `(() => { const ctx = window.SillyTavern.getContext(); const n = ctx.getPresetManager("openai").getPresetList().preset_names || {}; return Object.keys(n).includes(${JSON.stringify(presetName)}); })()`);
if (s) break;
await sleep(500);
}
await evaluate(page, closePopups);
log("预设已导入: " + presetName);
const state = await evaluate(page, `(async () => {
const ctx = window.SillyTavern.getContext();
const jq = window.jQuery;
const names = ctx.getPresetManager("openai").getPresetList().preset_names || {};
const idx = names[${JSON.stringify(presetName)}];
ctx.chatCompletionSettings.bind_preset_to_connection = false;
jq("#main_api").val("openai").trigger("change");
jq("#settings_preset_openai").val(String(idx)).trigger("change");
jq("#chat_completion_source").val("custom").trigger("change");
const s = ctx.chatCompletionSettings;
s.chat_completion_source = "custom";
s.reverse_proxy = "";
s.custom_url = "http://127.0.0.1:${args.capturePort}/v1";
s.custom_model = "mock-model";
jq("#custom_url").val(s.custom_url);
jq("#custom_model").val(s.custom_model);
const list = ctx.characters || [];
const ti = list.findIndex((c) => c.name === ${JSON.stringify(cardName)});
for (let t = 0; t < 40 && window.SillyTavern.getContext().name2 !== ${JSON.stringify(cardName)}; t++) {
await ctx.selectCharacterById(ti);
await new Promise((r) => setTimeout(r, 400));
}
await window.SillyTavern.getContext().clearChat({ clearData: true });
// 挂钩子:把最终消息表(含 name / identifier / role / 内容前 60 字)存下来
if (!window.__probeInstalled) {
window.__probeInstalled = true;
window.__probeItems = [];
ctx.eventSource.on("chat_completion_prompt_ready", (data) => {
const chat = (data && data.chat) || [];
window.__probeItems.push(chat.map((m, i) => ({
i,
role: m.role,
name: m.name ?? null,
identifier: m.identifier ?? null,
len: String(m.content ?? "").length,
head: String(m.content ?? "").slice(0, 60).replace(/\\s+/g, " "),
tail: String(m.content ?? "").slice(-40).replace(/\\s+/g, " "),
})));
});
}
const f = window.SillyTavern.getContext();
return { preset: f.chatCompletionSettings.preset_settings_openai, squash: f.chatCompletionSettings.squash_system_messages, names: f.chatCompletionSettings.names_behavior, newChat: f.chatCompletionSettings.new_chat_prompt, newExample: f.chatCompletionSettings.new_example_chat_prompt, url: f.chatCompletionSettings.custom_url, charName: f.name2 };
})()`);
log("环境就位: " + JSON.stringify(state));
await evaluate(page, `(async () => { await window.SillyTavern.getContext().generate(); return true; })()`);
await sleep(2500);
const items = await evaluate(page, "window.__probeItems || []");
const raw = JSON.stringify({ presetName, state, items }, null, 2);
const file = path.join(args.out, "probe-st-" + presetName.replace(/[^\w.-]+/g, "_") + ".json");
(await import("node:fs")).writeFileSync(file, raw, "utf8");
console.log(raw);
log("落盘: " + file);
} catch (err) {
console.error("[probe-st] 失败: " + err.message);
process.exitCode = 1;
} finally {
try { if (browser) browser.child.kill(); } catch (e) {}
try { st.kill("SIGKILL"); } catch (e) {}
try { cap.kill(); } catch (e) {}
}
}
await main();