加一套抓真实请求做预设对比的工具,并把三份预设的机制差异记进 README。
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
/** 抓包对比工具共用件:参数、日志、等等,以及造一张最小角色卡。 */
|
||||
import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import zlib from "node:zlib";
|
||||
import { waitFor } from "./cdp.mjs";
|
||||
|
||||
export function parseArgs(argv, defaults) {
|
||||
const out = { ...defaults };
|
||||
const map = {
|
||||
"--preset": "preset",
|
||||
"--message": "message",
|
||||
"--capture-port": "capturePort",
|
||||
"--st-port": "stPort",
|
||||
"--st-root": "stRoot",
|
||||
"--data-root": "dataRoot",
|
||||
"--out": "out",
|
||||
"--card": "card",
|
||||
"--browser": "browser",
|
||||
"--port": "port",
|
||||
"--timeout": "timeoutMs",
|
||||
};
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const key = argv[i];
|
||||
if (key === "--headful") {
|
||||
out.headful = true;
|
||||
continue;
|
||||
}
|
||||
if (key === "--keep") {
|
||||
out.keep = true;
|
||||
continue;
|
||||
}
|
||||
const name = map[key];
|
||||
if (!name) continue;
|
||||
const raw = argv[i + 1];
|
||||
i++;
|
||||
if (name === "preset" || name === "stRoot" || name === "dataRoot" || name === "out") {
|
||||
out[name] = path.resolve(String(raw));
|
||||
} else if (["capturePort", "stPort", "port", "timeoutMs"].includes(name)) {
|
||||
out[name] = Number(raw);
|
||||
} else {
|
||||
out[name] = String(raw);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function makeLogger(tag) {
|
||||
return (...parts) => console.log(`[${tag}]`, ...parts);
|
||||
}
|
||||
|
||||
const CRC_TABLE = (() => {
|
||||
const table = new Uint32Array(256);
|
||||
for (let n = 0; n < 256; n++) {
|
||||
let c = n;
|
||||
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
||||
table[n] = c >>> 0;
|
||||
}
|
||||
return table;
|
||||
})();
|
||||
|
||||
function crc32(buf) {
|
||||
let c = 0xffffffff;
|
||||
for (const byte of buf) c = CRC_TABLE[(c ^ byte) & 0xff] ^ (c >>> 8);
|
||||
return (c ^ 0xffffffff) >>> 0;
|
||||
}
|
||||
|
||||
function pngChunk(type, data) {
|
||||
const typeBuf = Buffer.from(type, "ascii");
|
||||
const len = Buffer.alloc(4);
|
||||
len.writeUInt32BE(data.length, 0);
|
||||
const crc = Buffer.alloc(4);
|
||||
crc.writeUInt32BE(crc32(Buffer.concat([typeBuf, data])), 0);
|
||||
return Buffer.concat([len, typeBuf, data, crc]);
|
||||
}
|
||||
|
||||
/** 1x1 透明 PNG + chara(tEXt) 文本块,酒馆认这个当角色卡 */
|
||||
export function buildCharacterPng(card) {
|
||||
const sig = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
||||
const ihdr = Buffer.alloc(13);
|
||||
ihdr.writeUInt32BE(1, 0);
|
||||
ihdr.writeUInt32BE(1, 4);
|
||||
ihdr[8] = 8;
|
||||
ihdr[9] = 6;
|
||||
const idat = zlib.deflateSync(Buffer.from([0x00, 0xff, 0xff, 0xff, 0x00]));
|
||||
const b64 = Buffer.from(JSON.stringify(card), "utf8").toString("base64");
|
||||
const text = Buffer.concat([Buffer.from("chara\0", "latin1"), Buffer.from(b64, "latin1")]);
|
||||
return Buffer.concat([
|
||||
sig,
|
||||
pngChunk("IHDR", ihdr),
|
||||
pngChunk("tEXt", text),
|
||||
pngChunk("IDAT", idat),
|
||||
pngChunk("IEND", Buffer.alloc(0)),
|
||||
]);
|
||||
}
|
||||
|
||||
const CARD_TOKENS = {
|
||||
description: "DESC_TOKEN 这是一张测试角色卡的描述,用来确认酒馆把它放在哪一段。",
|
||||
personality: "PERS_TOKEN 冷静、话少、只答问题。",
|
||||
scenario: "SCEN_TOKEN 一间安静的书房,只有两个人在说话。",
|
||||
first_mes: "GREET_TOKEN 你好,我在。",
|
||||
mes_example: "<START>\n{{user}}: EX_USER_TOKEN 今天做什么?\n{{char}}: EX_CHAR_TOKEN 读书。",
|
||||
};
|
||||
|
||||
const CARD_EMPTY = { description: "", personality: "", scenario: "", first_mes: "", mes_example: "" };
|
||||
|
||||
export function buildCard(kind, name) {
|
||||
const body = kind === "empty" ? CARD_EMPTY : CARD_TOKENS;
|
||||
return {
|
||||
spec: "chara_card_v2",
|
||||
spec_version: "2.0",
|
||||
data: {
|
||||
name,
|
||||
...body,
|
||||
creator_notes: "",
|
||||
system_prompt: "",
|
||||
post_history_instructions: "",
|
||||
alternate_greetings: [],
|
||||
character_book: null,
|
||||
tags: [],
|
||||
creator: "",
|
||||
character_version: "",
|
||||
extensions: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function writeCharacter(dataRoot, name, kind) {
|
||||
const dir = path.join(dataRoot, "default-user", "characters");
|
||||
mkdirSync(dir, { recursive: true });
|
||||
const file = path.join(dir, `${name}.png`);
|
||||
writeFileSync(file, buildCharacterPng(buildCard(kind, name)));
|
||||
return file;
|
||||
}
|
||||
|
||||
export async function waitHttp(url, timeoutMs, label) {
|
||||
return waitFor(async () => {
|
||||
const res = await fetch(url).catch(() => null);
|
||||
return res && res.ok ? res : null;
|
||||
}, { timeoutMs, intervalMs: 500, label });
|
||||
}
|
||||
|
||||
/** 等 captures 目录里出现一个没见过的 <label>-*.json */
|
||||
export async function waitForNewCapture(outDir, prefix, known, timeoutMs) {
|
||||
return waitFor(() => {
|
||||
const files = readdirSync(outDir).filter((f) => f.startsWith(`${prefix}-`) && f.endsWith(".json"));
|
||||
const fresh = files.filter((f) => !known.has(f));
|
||||
return fresh.length ? fresh.sort().at(-1) : null;
|
||||
}, { timeoutMs, intervalMs: 300, label: `mock 收到 ${prefix} 的请求` });
|
||||
}
|
||||
|
||||
export function listCaptures(outDir, prefix) {
|
||||
return existsSync(outDir)
|
||||
? readdirSync(outDir).filter((f) => f.startsWith(`${prefix}-`) && f.endsWith(".json"))
|
||||
: [];
|
||||
}
|
||||
|
||||
import { evaluate } from "./cdp.mjs";
|
||||
|
||||
const ST_READY = `Boolean(window.SillyTavern && window.SillyTavern.getContext && document.querySelector("#send_textarea") && !document.querySelector(".splash-message") && Object.keys(window.SillyTavern.getContext().getPresetManager("openai").getPresetList().preset_names || {}).length > 0)`;
|
||||
const ST_PROGRESS = `(() => ({
|
||||
readyState: document.readyState,
|
||||
splash: Array.from(document.querySelectorAll(".splash-message")).map((e) => e.textContent),
|
||||
loaders: Array.from(document.querySelectorAll("[data-slug]")).map((e) => e.getAttribute("data-slug")).slice(0, 20),
|
||||
api: performance.getEntriesByType("resource").map((e) => e.name).filter((n) => n.indexOf("/api/") >= 0).slice(-10),
|
||||
}))()`;
|
||||
|
||||
/** 等酒馆前端初始化完;卡住时每 5 秒打一次进度,方便判断停在哪一步。 */
|
||||
export async function waitStReady(page, timeoutMs, log) {
|
||||
const start = Date.now();
|
||||
let ticks = 0;
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
const ready = await evaluate(page, ST_READY).catch(() => false);
|
||||
if (ready) return true;
|
||||
if (ticks % 5 === 4) {
|
||||
const progress = await evaluate(page, ST_PROGRESS).catch((e) => ({ error: e.message }));
|
||||
if (log) log("等前端就绪: " + JSON.stringify(progress));
|
||||
}
|
||||
ticks++;
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
}
|
||||
throw new Error("等酒馆前端就绪超时");
|
||||
}
|
||||
Reference in New Issue
Block a user