// 手动挡:我把环境起好(假上游 + 酒馆 + 有头浏览器 + 预设导入 + 连接 + 角色 + 空历史), // 你只管在页面里点发送,脚本只监控抓包,不自己反复重试。 // 用法: 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();