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

190 lines
11 KiB
JavaScript

// 让本机真酒馆发一次真实请求,落到 capture-server 上。
// 用法: node drive-st.mjs --preset "C:/Users/zed/Downloads/linux_1.3.json" --message 你好 --capture-port 8801 --st-port 8123
// 用独立 dataRoot,不改 D:\game\sillytavern\data 里的东西。
// 预设走页面上的「导入预设」文件输入(纯导入 json),和本仓库那边保持同一条链路。
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 { captureConsole, evaluate, launchBrowser, openPage, sleep } from "./cdp.mjs";
import { listCaptures, makeLogger, parseArgs, waitForNewCapture, waitHttp, waitStReady, writeCharacter } from "./lib.mjs";
const HERE = path.dirname(fileURLToPath(import.meta.url));
const log = makeLogger("drive-st");
const DEFAULTS = {
preset: "",
message: "你好",
capturePort: 8801,
stPort: 8123,
stRoot: "D:/game/sillytavern",
dataRoot: path.join(HERE, ".st-data"),
out: path.join(HERE, "captures"),
card: "tokens",
browser: "",
headful: false,
keep: false,
timeoutMs: 180000,
};
const CLOSE_POPUPS = `(() => {
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 (!args.preset || !existsSync(args.preset)) throw new Error("预设文件不存在: " + args.preset);
if (!existsSync(args.stRoot)) throw new Error("酒馆目录不存在: " + args.stRoot);
mkdirSync(args.out, { recursive: true });
const presetName = path.basename(args.preset, ".json");
const presetText = readFileSync(args.preset, "utf8");
log("预设: " + args.preset + " (名字按酒馆规则取文件名: " + presetName + ")");
const cardName = "ProbeCard";
writeCharacter(args.dataRoot, cardName, args.card);
log("角色卡就位: " + cardName + ".png (card=" + args.card + ")");
const staleFiles = [path.join(args.dataRoot, "default-user", "OpenAI Settings", presetName + ".json")]; for (const stale of staleFiles) { try { rmSync(stale, { force: true }); } catch (e) {} } const known = new Set(listCaptures(args.out, "st"));
const st = spawn(process.execPath, ["server.js", "--port", String(args.stPort), "--dataRoot", args.dataRoot, "--browserLaunchEnabled=false"], { cwd: args.stRoot, stdio: ["ignore", "pipe", "pipe"] });
const stLog = [];
st.stdout.on("data", (d) => stLog.push(String(d)));
st.stderr.on("data", (d) => stLog.push(String(d)));
let browser;
let page;
try {
await waitHttp("http://127.0.0.1:" + args.stPort + "/", args.timeoutMs, "酒馆服务起来");
log("酒馆已启动");
browser = await launchBrowser({ exe: args.browser || undefined, port: args.stPort + 100, headless: !args.headful });
page = await openPage(browser, "http://127.0.0.1:" + args.stPort + "/");
captureConsole(page);
log("浏览器已打开页面");
await waitStReady(page, args.timeoutMs, log);
log("前端就绪");
const importExpr = `(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;
})()`;
await evaluate(page, importExpr);
log("已把预设丢进酒馆的导入框,等它落盘...");
let importState = null;
for (let i = 0; i < 120; i++) {
importState = await evaluate(page, `(() => {
const ctx = window.SillyTavern.getContext();
const names = ctx.getPresetManager("openai").getPresetList().preset_names || {};
const popup = document.querySelector(".popup-button-cancel");
if (popup) popup.click();
return { has: Object.keys(names).includes(${JSON.stringify(presetName)}), keys: Object.keys(names), popup: Boolean(document.querySelector(".popup")) };
})()`);
if (importState.has) break;
await sleep(500);
}
if (!importState || !importState.has) throw new Error("预设导入后没出现在列表里: " + JSON.stringify(importState));
log("预设已导入: " + presetName);
const presetState = await evaluate(page, `(async () => {
const ctx = window.SillyTavern.getContext();
const names = ctx.getPresetManager("openai").getPresetList().preset_names || {};
const idx = names[${JSON.stringify(presetName)}];
if (idx === undefined) return { error: "preset-not-found", keys: Object.keys(names) };
ctx.chatCompletionSettings.bind_preset_to_connection = false;
const sel = window.jQuery("#settings_preset_openai");
sel.val(String(idx));
sel.trigger("change");
const s = ctx.chatCompletionSettings;
return {
preset: s.preset_settings_openai,
temp: s.temp_openai,
topP: s.top_p_openai,
maxTokens: s.openai_max_tokens,
stream: s.stream_openai,
squash: s.squash_system_messages,
useSysprompt: s.use_sysprompt, maxContext: s.openai_max_context, functionCalling: s.function_calling, unlocked: s.max_context_unlocked,
};
})()`);
if (presetState && presetState.error) throw new Error("预设没被酒馆识别: " + JSON.stringify(presetState));
const presetJson = JSON.parse(presetText); const expect = { temp: presetJson.temperature, topP: presetJson.top_p, maxTokens: presetJson.openai_max_tokens }; log("预设已选中: " + JSON.stringify(presetState) + " , 预设文件里: " + JSON.stringify(expect)); if (expect.temp !== undefined && Math.abs(Number(expect.temp) - Number(presetState.temp)) > 1e-6) throw new Error("导入后参数没生效: temp 期望 " + expect.temp + " 实得 " + presetState.temp);
const connUrl = "http://127.0.0.1:" + args.capturePort + "/v1";
const connState = await evaluate(page, `(async () => {
const ctx = window.SillyTavern.getContext();
const jq = window.jQuery;
jq("#main_api").val("openai").trigger("change"); jq("#chat_completion_source").val("custom").trigger("change");
const s = ctx.chatCompletionSettings;
s.chat_completion_source = "custom";
s.reverse_proxy = "";
s.proxy_password = "";
s.custom_url = ${JSON.stringify(connUrl)};
s.custom_model = "mock-model";
jq("#custom_url").val(s.custom_url);
jq("#custom_model").val(s.custom_model); s.openai_max_context = 2000000; s.max_context_unlocked = true; jq("#openai_max_context").val(2000000).trigger("input"); jq("#oai_max_context_unlocked").prop("checked", true).trigger("input"); s.function_calling = false; jq("#openai_function_calling").prop("checked", false).trigger("input");
return { source: s.chat_completion_source, url: s.custom_url, model: s.custom_model, mainApi: window.SillyTavern.getContext().mainApi, maxContext: s.openai_max_context, functionCalling: s.function_calling };
})()`);
log("连接已指向 mock: " + connState.url + " (source=" + connState.source + ")");
const charState = await evaluate(page, `(async () => {
const ctx = window.SillyTavern.getContext();
await ctx.getCharacters();
const list = ctx.characters || [];
const idx = list.findIndex((c) => c.name === ${JSON.stringify(cardName)});
const other = list.findIndex((c) => c.name !== ${JSON.stringify(cardName)});
if (idx < 0) return { error: "character-not-found", names: list.map((c) => c.name) };
if (other >= 0) await ctx.selectCharacterById(other);
for (let t = 0; t < 40 && window.SillyTavern.getContext().name2 !== ${JSON.stringify(cardName)}; t++) { if (t === 0 && other >= 0) await ctx.selectCharacterById(other); await ctx.selectCharacterById(idx); await new Promise((r) => setTimeout(r, 500)); }
await ctx.clearChat({ clearData: true });
const fresh = window.SillyTavern.getContext(); return { ok: true, chatLen: fresh.chat.length, charName: fresh.name2, chatId: fresh.chatId, chid: fresh.characterId };
})()`);
log("角色状态: " + JSON.stringify(charState));
if (!charState || charState.charName !== cardName) throw new Error("角色没选中: " + JSON.stringify(charState));
await evaluate(page, CLOSE_POPUPS);
await evaluate(page, `(() => {
const jq = window.jQuery;
const ta = jq("#send_textarea");
ta.val(${JSON.stringify(args.message)}).trigger("input");
window.__genErr = null; window.SillyTavern.getContext().generate("normal").catch((e) => { window.__genErr = String((e && e.stack) || e); });
return true;
})()`);
log("已点发送, 等 mock 落盘..."); for (let i = 0; i < 20; i++) { const sendState = await evaluate(page, `(() => { const c = window.SillyTavern.getContext(); const ta = document.getElementById("send_textarea"); return { chatLen: c.chat.length, err: window.__genErr, api: c.mainApi, txt: String(ta.value).slice(0, 20), ro: ta.readOnly, extCount: c.extensionSettings ? Object.keys(c.extensionSettings).length : -1 }; })()`); log("发送状态: " + JSON.stringify(sendState)); if (sendState.chatLen || sendState.err) break; await sleep(500); } log("页面日志尾: " + JSON.stringify(page.logs.slice(-12)));
const captureFile = await waitForNewCapture(args.out, "st", known, args.timeoutMs);
log("抓到请求: " + captureFile);
await sleep(1500);
console.log(JSON.stringify({ side: "st", preset: presetName, capture: path.join(args.out, captureFile) }));
} catch (err) {
console.error("[drive-st] 失败: " + err.message);
if (stLog.length) console.error(stLog.join("").slice(-2500));
if (page) {
const dump = await evaluate(page, `(() => ({
url: location.href,
ready: document.readyState,
toast: Array.from(document.querySelectorAll(".toast-message, .toast")).map((e) => e.textContent).slice(0, 6),
splash: Array.from(document.querySelectorAll(".splash-message")).map((e) => e.textContent),
popups: Array.from(document.querySelectorAll(".popup")).map((e) => e.innerText.slice(0, 300)),
chatLen: window.SillyTavern ? window.SillyTavern.getContext().chat.length : -1,
chid: window.SillyTavern ? window.SillyTavern.getContext().characterId : -1,
name2: window.SillyTavern ? window.SillyTavern.getContext().name2 : "",
sendDisabled: document.querySelector("#send_but") ? document.querySelector("#send_but").className : "",
}))()`).catch((e) => ({ error: e.message }));
console.error("页面状态: " + JSON.stringify(dump, null, 2));
if (page.logs) console.error("页面日志: " + JSON.stringify(page.logs.slice(-15), null, 2));
}
process.exitCode = 1;
} finally {
if (!args.keep) {
try { if (browser) browser.child.kill(); } catch (e) {}
try { st.kill(); } catch (e) {}
await sleep(500);
try { st.kill("SIGKILL"); } catch (e) {}
}
}
}
await main();