// 探针挡:起假上游 + 酒馆 + 无头浏览器,导入预设后挂 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();