// 让本仓库发同一次真实请求,落到 capture-server 上。 // 用法: node drive-wa.mjs --preset "C:/Users/zed/Downloads/linux_1.3.json" --message 你好 --capture-port 8802 // 用独立 WRITING_AGENT_DATA_DIR,不碰 ~/.writing-agent 里的配置。 // 预设走本仓库自己的 /api/presets/import(纯导入 json),之后走直聊的 ephemeral complete 真发一次。 import { spawn } from "node:child_process"; import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { listCaptures, makeLogger, parseArgs, waitForNewCapture, waitHttp } from "./lib.mjs"; const HERE = path.dirname(fileURLToPath(import.meta.url)); const REPO = path.resolve(HERE, "../.."); const log = makeLogger("drive-wa"); const DEFAULTS = { preset: "", message: "你好", capturePort: 8802, port: 23377, out: path.join(HERE, "captures"), dataRoot: path.join(HERE, ".wa-data"), charName: "ProbeCard", toolPolicy: "adapt", exampleDialogue: "\n{{user}}: EX_USER_TOKEN 今天做什么?\n{{char}}: EX_CHAR_TOKEN 读书。", // 跟酒馆那张探针卡的三块字段一致 charDescription: "DESC_TOKEN 这是一张测试角色卡的描述,用来确认酒馆把它放在哪一段。", charPersonality: "PERS_TOKEN 冷静、话少、只答问题。", scenario: "SCEN_TOKEN 一间安静的书房,只有两个人在说话。", timeoutMs: 120000, }; async function postJson(url, body) { const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); const text = await res.text(); let data = null; try { data = JSON.parse(text); } catch (e) { data = { raw: text.slice(0, 800) }; } if (!res.ok) throw new Error("HTTP " + res.status + " " + JSON.stringify(data).slice(0, 800)); return data; } async function main() { const args = parseArgs(process.argv.slice(2), DEFAULTS); const priorIndex = process.argv.indexOf("--prior"); if (priorIndex > 0) args.prior = String(process.argv[priorIndex + 1] ?? ""); const policyIndex = process.argv.indexOf("--tool-policy"); if (policyIndex > 0) args.toolPolicy = String(process.argv[policyIndex + 1] ?? "adapt"); if (!args.preset) throw new Error("缺少 --preset"); const presetName = path.basename(args.preset, ".json"); const raw = JSON.parse(readFileSync(args.preset, "utf8")); mkdirSync(args.dataRoot, { recursive: true }); mkdirSync(args.out, { recursive: true }); // 一条指向假上游的 API 配置。直接写盘,避免能力探测的额外请求混进抓包。 const now = new Date().toISOString(); const profiles = { version: 1, profiles: [ { id: "probe-mock", name: "mock", baseUrl: "http://127.0.0.1:" + args.capturePort + "/v1", apiKey: "sk-mock", model: "mock-model", createdAt: now, updatedAt: now, }, ], }; writeFileSync(path.join(args.dataRoot, "profiles.json"), JSON.stringify(profiles, null, 2), "utf8"); // 预设工具协议:preset = 按预设数据声明真 tools writeFileSync( path.join(args.dataRoot, "settings.json"), JSON.stringify( { version: 1, activeProfileId: null, activePresetId: null, messageSquash: "follow-preset", presetToolPolicy: args.toolPolicy, }, null, 2, ), "utf8", ); try { rmSync(path.join(args.dataRoot, "presets"), { recursive: true, force: true }); } catch (e) {} // 人设也用默认的(名字 User,跟酒馆一致),免得复用上一次跑留下的「玩家」 try { rmSync(path.join(args.dataRoot, "personas.json"), { force: true }); } catch (e) {} const known = new Set(listCaptures(args.out, "wa")); const env = { ...process.env, PORT: String(args.port), WRITING_AGENT_DATA_DIR: args.dataRoot }; const server = spawn(process.execPath, [path.join(REPO, "node_modules/tsx/dist/cli.mjs"), path.join(REPO, "src/server/web-server.ts")], { cwd: REPO, env, stdio: ["ignore", "pipe", "pipe"] }); const logs = []; server.stdout.on("data", (d) => logs.push(String(d))); server.stderr.on("data", (d) => logs.push(String(d))); const base = "http://127.0.0.1:" + args.port; try { await waitHttp(base + "/api/health", args.timeoutMs, "本仓库服务起来"); log("服务已启动: " + base); const imported = await postJson(base + "/api/presets/import", { raw, name: presetName, activate: true }); const presetId = imported.preset.id; log("预设已导入: " + presetName + " (id=" + presetId + ")"); if (imported.warnings && imported.warnings.length) log("导入提示: " + JSON.stringify(imported.warnings).slice(0, 600)); const body = { text: args.message, presetId, complete: true, turns: args.prior ? [ { id: "t1", user: { content: "上一轮用户", createdAt: now }, replies: [{ windowId: "w1", content: args.prior }], createdAt: now, }, ] : [], windows: [{ id: "w1", profileId: "probe-mock" }], systemPrompt: "", loreBefore: "", seedHistory: "", loreAfter: "", postTurn: "", // 跟酒馆那张探针卡对齐:角色名 + 卡里的示例对话原文 charName: args.charName, exampleDialogue: args.exampleDialogue, charDescription: args.charDescription, charPersonality: args.charPersonality, scenario: args.scenario, }; const result = await postJson(base + "/api/direct-chat/ephemeral/complete", body); const first = result.multi && result.multi.results && result.multi.results[0]; if (first && first.error) throw new Error("本轮请求失败: " + first.error); log("本轮完成, reply=" + JSON.stringify(String((first && first.reply) || "").slice(0, 40))); const captureFile = await waitForNewCapture(args.out, "wa", known, args.timeoutMs); log("抓到请求: " + captureFile); console.log(JSON.stringify({ side: "wa", preset: presetName, capture: path.join(args.out, captureFile) })); } catch (err) { console.error("[drive-wa] 失败: " + err.message); if (logs.length) console.error(logs.join("").slice(-4000)); process.exitCode = 1; } finally { try { server.kill(); } catch (e) {} } } await main();