/** 抓包对比工具共用件:参数、日志、等等,以及造一张最小角色卡。 */ 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: "\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 目录里出现一个没见过的