190 lines
11 KiB
JavaScript
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();
|