183 lines
6.2 KiB
JavaScript
183 lines
6.2 KiB
JavaScript
/** 抓包对比工具共用件:参数、日志、等等,以及造一张最小角色卡。 */
|
|
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("等酒馆前端就绪超时");
|
|
}
|