Files
writing-agent/tools/st-request-diff/drive-wa.mjs
chenmoranzhi 9dc927282f 预设兼容:按真实抓包对齐装配与包装层,补上声明式工具与预设正则。
- 装配只认 prompt_order.enabled;squash_system_messages 只合并相邻 system(新增 system 档,连接符按酒馆用单换行)
- 包装层:new_chat_prompt、示例对话(new_example_chat_prompt + <START> 切段 + name)、角色卡三块与 personality_format/scenario_format、{{user}}/{{char}} 取值、任务契约并进预设条目
- 工具协议:按 ToolBindings / 伪工具协议数据声明真 tools(presetToolPolicy=preset),工具描述用到的条目不再重复作为消息发出
- 正则:promptOnly 作用于对话历史(含 depth 过滤)、写入时脚本改写回复;{{//}} 注释按配平扫描
- tools/st-request-diff:真假上游抓包对比工具、三份预设的机制结论与实测报告
2026-10-23 00:13:08 +08:00

151 lines
6.3 KiB
JavaScript

// 让本仓库发同一次真实请求,落到 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:
"<START>\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();