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

152 lines
7.2 KiB
JavaScript

// 手动挡:我把环境起好(假上游 + 酒馆 + 有头浏览器 + 预设导入 + 连接 + 角色 + 空历史),
// 你只管在页面里点发送,脚本只监控抓包,不自己反复重试。
// 用法: node manual-st.mjs --preset "C:/Users/zed/Downloads/linux_1.3.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, waitHttp, waitStReady, writeCharacter } from "./lib.mjs";
const HERE = path.dirname(fileURLToPath(import.meta.url));
const log = makeLogger("manual-st");
const DEFAULTS = {
preset: "C:/Users/zed/Downloads/linux_1.3.json",
capturePort: 8801,
stPort: 8123,
stRoot: "D:/game/sillytavern",
dataRoot: path.join(HERE, ".st-data"),
out: path.join(HERE, "captures"),
card: "tokens",
browser: "",
timeoutMs: 180000,
};
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", "st", "--port", String(args.capturePort), "--out", args.out], { cwd: HERE, stdio: "inherit" });
await waitHttp("http://127.0.0.1:" + args.capturePort + "/v1/models", 15000, "假上游起来");
const cardName = "ProbeCard";
writeCharacter(args.dataRoot, cardName, args.card);
const stale = path.join(args.dataRoot, "default-user", "OpenAI Settings", presetName + ".json");
try { rmSync(stale, { 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"] });
const stLog = [];
st.stdout.on("data", (d) => stLog.push(String(d)));
st.stderr.on("data", (d) => stLog.push(String(d)));
let browser;
try {
await waitHttp("http://127.0.0.1:" + args.stPort + "/", args.timeoutMs, "酒馆服务起来");
browser = await launchBrowser({ exe: args.browser || undefined, port: args.stPort + 100, headless: false });
const page = await openPage(browser, "http://127.0.0.1:" + args.stPort + "/");
captureConsole(page);
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);
for (let i = 0; i < 120; i++) {
const s = 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)}) };
})()`);
if (s.has) break;
await sleep(500);
}
log("预设已导入并选中: " + presetName);
const connUrl = "http://127.0.0.1:" + args.capturePort + "/v1";
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.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);
jq("#openai_max_context").val(2000000).trigger("input");
const list = ctx.characters || [];
const ti = list.findIndex((c) => c.name === ${JSON.stringify(cardName)});
const other = list.findIndex((c) => c.name !== ${JSON.stringify(cardName)});
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(ti);
await new Promise((r) => setTimeout(r, 400));
}
await window.SillyTavern.getContext().clearChat({ clearData: true });
const f = window.SillyTavern.getContext();
return {
preset: f.chatCompletionSettings.preset_settings_openai,
temp: f.chatCompletionSettings.temp_openai,
maxTokens: f.chatCompletionSettings.openai_max_tokens,
maxContext: f.chatCompletionSettings.openai_max_context,
functionCalling: f.chatCompletionSettings.function_calling,
squash: f.chatCompletionSettings.squash_system_messages,
stream: f.chatCompletionSettings.stream_openai,
charName: f.name2,
chatLen: f.chat.length,
url: f.chatCompletionSettings.custom_url,
};
})()`);
log("环境已就位: " + JSON.stringify(state));
log("=================================================");
log("浏览器已打开酒馆,你只需要:在输入框写内容 -> 点发送(或回车)。");
log("发送后我会把抓到的请求打出来。要改设置(比如关掉 function calling)也直接在页面里改。");
log("=================================================");
const seen = new Set(listCaptures(args.out, "st"));
log("开始监控抓包(Ctrl+C 结束)...");
for (;;) {
await sleep(1500);
const files = listCaptures(args.out, "st").filter((f) => !seen.has(f));
for (const f of files) {
seen.add(f);
log("抓到新请求: " + f);
const info = await evaluate(page, `(() => ({ name2: window.SillyTavern.getContext().name2, chatLen: window.SillyTavern.getContext().chat.length }))()`).catch(() => ({}));
log("页面当前: " + JSON.stringify(info));
log("→ 用 `node inspect-capture.mjs captures/" + f + "` 看逐条消息");
}
const err = await evaluate(page, "window.__lastErr || null").catch(() => null);
if (err) log("页面报错: " + err);
}
} catch (err) {
console.error("[manual-st] 失败: " + err.message);
if (stLog.length) console.error(stLog.join("").slice(-2000));
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();