Files
writing-agent/tools/st-request-diff
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
..

真实请求对比(酒馆 vs 本仓库)

目的:让本机真酒馆和本仓库用同一个预设发一次真实请求,把两边真正发到上游的 JSON 落盘,再逐字段、逐条消息对齐,弄清"预设兼容"还差在哪。

不猜、不看内部对象,只看线上真实请求体。

前提

  • 酒馆源码在 D:/game/sillytavern,能 node server.js 起来(版本 1.18)。
  • Node 22+(用到内置 WebSocket)、本机有 Edge 或 Chrome(headless 也行)。
  • 不改你 D:/game/sillytavern/data 里的任何东西:两边都用独立数据目录 (酒馆 --dataRoot ./.st-data,本仓库 WRITING_AGENT_DATA_DIR=./.wa-data)。

链路(两边都走"纯导入 json")

  1. 假上游 capture-server.mjs:实现够用的 OpenAI 兼容端点,把收到的原始请求体原样写进 captures/,并回一句固定文本。
  2. 酒馆侧:起 ST → 起 headless 浏览器 → CDP 进页面 → 用页面上的「导入预设」文件输入把 json 丢进去(等于人手点导入)→ 选预设 → 连接指向假上游 → 选探针角色 ProbeCard → 清空历史 → ctx.generate("normal") 真发一轮。
  3. 本仓库侧:起 src/server/web-server.ts(独立数据目录)→ POST /api/presets/import → POST /api/direct-chat/ephemeral/complete 真发一轮。
  4. diff.mjs 对齐两份请求体,出 captures/report-<预设名>.md。

用法

全自动(两个预设、两边、报告):

node run-all.mjs
node run-all.mjs --only st --preset "C:/Users/zed/Downloads/linux_1.3.json"
node run-all.mjs --only wa|diff

手动挡(我起环境、你点发送、我只监控):

node manual-st.mjs --preset "C:/Users/zed/Downloads/NVWA-Galaxia-ST.json"
# 浏览器窗口打开后,你在里面写内容点发送即可;抓到会打出来

配套小工具(都对着已开着的页面,不动浏览器):

node attach-preset.mjs --preset <json>      # 往活页面里再导入一个预设并选中
node attach-clear.mjs  --preset <名字>      # 切预设 + 清空历史 + 打印当前生效参数
node attach-send.mjs   --preset <名字> --text 你好
node inspect-capture.mjs captures/st-001-xxx.json [--full 3]
node inspect-prompt-order.mjs <preset.json>  # 看 order/开关/marker/system_prompt/注入位置
node peek-preset-flags.mjs <preset.json>     # 看 function_calling / squash / extensions
node peek-gen-fields.mjs <preset.json>       # 看采样参数字段类型
node inspect-wa-assembly.mjs <preset.json>   # 用本仓库的装配代码看会拼出什么(不发请求)

已确认的机制差异(2026-10 实测)

预设 酒馆侧机制 证据
linux_1.3 预设里是纯文本的伪工具协议(6 条 【ToolCall引导】…),运行期被 tavern_helper 脚本抽 description/fields 动态注册成真 tool 4 条消息 + tools(1)=dispatch_stream_payload_xxx、tool_choice:auto
Reborn2.4 预设自带 function_calling:true、tool_call_recurse_limit:5、extensions.ToolBindings.game_content,提示词要求必须调用 game_content → 设计上就是真 function calling 两次抓包都是 33 条、无 tools → 绑定没注册上,退化成纯文本
NVWA-Galaxia-ST 无任何工具痕迹(无 tool 条目、无 function_calling) 14 条、无 tools,纯文本

已确认的差距(按优先级)

  1. 脚本/正则层(最贵):本仓库导入时明确不执行 ST 正则与前端脚本;linux_1.3 的 tools 升格、Reborn2.4 的 game_content 都在这层。
  2. prompts[].enabled 双闸(已修:装配只认 prompt_order.enabled):酒馆只用 prompt_order.enabled(openai.js 从不读 prompts[].enabled),本仓库要求两者都开 → linux_1.3 少发 <世界设定与背景>、作者简介 古典武侠、结尾 共约 1 万字。
  3. squash_system_messages 语义(已修:新增 system 档,只合并相邻 system):酒馆只合并相邻 system(跳过带 name 的、跳过 newMainChat/newChat/groupNudge),本仓库按"同角色合并"把 结尾(框架闭合)、/历史记录、你好 三条 user 合成一条。
  4. 角色卡插槽 + 格式模板:charDescription/charPersonality/scenario 及 personality_format/scenario_format 包装(如 <character_personality>…</character_personality>)。
  5. 示例对话:new_example_chat_prompt 打头,mes_example 按 <START> 切段成多条 system,并带 name=example_user/example_assistant。
  6. new_chat_prompt:空聊天时插一条 [Start a new Chat] / [NVWA chat history begins below.]。
  7. 本仓库多发的:姓名:玩家(persona 无描述时的兜底)、### 任务契约 …、stream_options:{include_usage:true}。

工具协议:声明式映射(不跑脚本)

预设里的「伪工具协议」有两种数据形态,src/preset/tool-declaration.ts 都直接读数据翻译成真 tools:

  1. extensions.SPreset.ToolBindings[].form → name / description / parameters(Reborn2.4 的 game_content);
  2. 只有协议条目、没有 ToolBindings 时 → 从条目正文取工具名(dispatch_stream_payload_…)、 「工具说明」条目当 description、「字段说明」条目当参数的 description(linux_1.3 的 display_stream)。

开关:设置页「预设工具协议」= preset(只发预设要求的工具)。默认仍是 adapt,不动现有行为。 命中后 complete / completeStream 会改用带 tools 的请求,并把工具调用里的正文按声明字段折回正文。

实测(linux_1.3):酒馆与我们的 tool_choice 都是 auto、都只有 1 个工具、参数都是 display_stream; 差别只剩工具名后缀(酒馆每次导入随机生成 hash)与 description 覆盖范围(酒馆把「系统声明」也并进去了)。

正则脚本:三类,只做数据驱动的三类

酒馆 regex_scripts 的语义(extensions/regex/engine.js + getRegexedString 调用点):

类别 条件 作用对象 我们做了吗
① 发送前 promptOnly === true 对话历史(user=placement 1、assistant=2、思维链=6),带 minDepth/maxDepth ✅ src/preset/regex-scripts.ts + play-frame 的 chatHistory 装配
② 展示 markdownOnly === true 只影响界面渲染 ❌ 暂不做(纯 UI)
③ 写入时 两个 flag 都不是 true 消息进 chat 那一刻改写文本 ✅ 回复落盘前(direct-chat/turn.ts)

支持的脚本字段:findRegex、replaceString(含 {{match}}、$1、$<name>)、trimStrings、 placement、promptOnly/markdownOnly、minDepth/maxDepth、disabled、substituteRegex(0/1; 2 按 RAW 处理并标记)。

实测(linux_1.3,两轮对话):上一轮助手回复 回答正文<choices>A、B、C</choices> → 发出的请求里变成 回答正文(预设的「去」promptOnly 正则生效); 上一轮用户消息被预设的「userinput」正则改写成 【HISTORICAL_USER_INPUT】【LAST_USER_INPUT】…。

决策记录

  • 采样参数跟默认走:预设缺 temperature/top_p/max_tokens 时,本仓库回落到 API 配置默认即可(酒馆是"缺就不改,沿用上次的值",不做对齐)。所以 NVWA-Galaxia-ST 报告里那行参数差异算已知取舍。
  • names_behavior(NONE:-1/DEFAULT:0/COMPLETION:1/CONTENT:2)只影响聊天历史是否加 名字: 前缀,三个预设都是 0,单角色下无效果 → 暂不实现。
  • system_prompt:true 标的是 ST 的内置槽位(不可删/不在用户列表/不参与深度注入,落位由 openai.js 里 systemPrompts=['nsfw','jailbreak'] 等硬编码决定),不是位置规则;当前三份抓包里两边位置一致 → 不是当前核心差距。