Repository navigation
Releases: wacilimonster-source/pockettavern
Release list
掌上酒馆 v1.10.16
掌上酒馆 v1.10.16
这一版是第六轮全量审查的修复落地(由另一路 agent 复核 + 实现,我做交叉复核并补了三处),外加脚本宿主的一期地基(MVU 支持的第一块砖)。
1. 不再丢东西 / 不再必然失败
| 症状 | 根因 | 现在 |
|---|---|---|
| 「跟随网页端当前预设」从来没生效,界面显示"网页端未选择预设" | 预设名读的是 settings.json 根层,而上游把它放在 oai_settings 里(本机实测:根层无此键,oai_settings.preset_settings_openai = "夏瑾 天琴座 V2 Beta 1.0") |
改读 oai_settings(根层仍兜底)。行为会变:App 第一次真正接管网页端预设的参数/预算/顺序 —— 这是预期 |
| 点进角色打开的是最老那段对话,之后发的消息写进老对话 | 服务端对任何有内容的对话给的是字符串 last_mes(send_date 或 ISO),只有零字节文件才给数字 mtime;旧实现只认数字 → 本机 72 份候选全被丢弃 → 退化成"取字母序第一个" |
数字/ISO/humanDate 三种形态都解析(提成公用 parseStDate) |
| 新对话首轮生成中退出 → 这一轮一个字都不剩 | 退出保存时文件名还是空的,直接 return;草稿已在发送时清掉 |
退出时按同一命名规则补文件名再落盘(判据只看用户真的发过消息) |
| 回复刚出完就退出,可能没落盘 | 保存闭包排队后才检查"已销毁",闭包至少晚一个事件循环 → 被丢弃 | 排队之前取快照与文件名,闭包只守"写 state" |
| 弱网抖一下就把老对话当新的、另建一份文件(记忆留在旧文件) | 列表拉取异常被 catch (_) {} 静默吞掉 |
区分"服务端说没有"与"没问到":传输失败 → 锁输入 + 报错,下拉刷新/重进自愈 |
| 并发写冲突后永久停止落盘 | 重载把"历史已加载"闸门关掉,早退分支不回填,所有保存被拦 | 早退分支回填闸门 |
| Claude / Gemini 两个源必然「未收到回复(服务器未返回内容)」 | App 只认 OpenAI 帧(choices[0].delta.content),而服务端对这两个源是原样转发厂商帧 |
新增帧分派:OpenAI / Claude(delta.text/delta.thinking)/ Gemini(candidates[].content.parts[].text,thought:true 归思考) |
2. 发给模型的内容更对
- EJS 条件块真正生效(
<% if/else/else if/} %>):此前两个分支的正文全都发(模板卡的状态判断等于没写);现在按分支门控,未生效分支里的<% setvar %>也不执行。缺}时如实上报、不猜。 - 宏替换顺序对齐上游:
{{char}}/{{user}}移到字段宏之后替(上游script.js:2889注释:"必须最后替,才能替进 {{description}} 里面");尖括号<BOT>/<CHAR>/<USER>保持在字段宏之前(上游 preEnv)。此前卡描述/性格里写的{{user}}会以字面量发给模型。 - 以人名做关键词的世界书条目现在会命中:扫描文本按上游拼「名字: 正文」(开关
world_info_include_names,上游出厂 true;你服务器当前为 false,所以本机行为不变,只是开始跟着设置走)。 - 卡内嵌世界书字段补全:整包
extensions透传(此前丢 role/group/outlet/scanDepth/matchWholeWords,且position取错层 —— 本机 57 份卡内书 1,384 条里有 555 条深度注入条目被当成静态注入),useProbability缺省改为 true(上游口径,旧写法方向相反)。 - 作者禁用的深度注入条目不再照发(
enabled:false门控;实测「三人逆行v10.0」里有 2 条),同时保留"不在顺序表 → 放行"的既有宽容口径。 - 动一次顺序表不再把深度注入条目从服务端预设文件里删掉(写回时按原顺序合并回去)。
- 预设带的
reasoning_effort: 'auto'不再被强行钉成 medium(上游auto= 不发送;你的Default.json就是 auto,此前等于强开思考并固定中档)。 - 新建预设不再把上下文预算砍到 4095/300(那对键"存在即接管",而你的真实模型是 2000000/65535)。
- 切 API 源后立刻用新源的采样参数(补一次生效预设 provider 失效)。
3. 界面、渲染与工程
- 走 WebView 的消息不再挤成一坨:网页端 showdown 开了
simpleLineBreaks,App 的 markdown 渲染会把段落里的单换行折成空格 → 同一消息"流式时一行行、定稿挤一坨"。现在只在确定交给 WebView 的正文段重算硬换行(不动原生渲染器与面板围栏,避免回退 v1.10.13~15 那三次修复)。 - 围栏 info 串放宽:
```</maintext>这类合法开标记不再被当正文(此前气泡尾部多一个空黑框)。 - 面板高度非正值不再被当有效值(一帧把 3000px 的面板压成 48px,后面的内容再也够不着)。
- 头像名含
#/ 空格(如「Vivi 6.png」「出轨计划 0.85.png」)不再 404:逐路径段编码(网页端也是encodeURIComponent,服务端解码一次正好还原)。 - IPv6 / 带下划线的主机名不再被判"地址格式不正确"(
http://[::1]:8000此前恒拒,而回归测试还断言它受支持)。 - 世界书条目页一次 build 只排一次序(此前 1+N 次全量排序,大书滚动卡顿);预设**「出厂」徽标开始显示(判据与 UI 都在,只是构造时从不传);重新生成的失败回滚不再留下两份**同一条回复;停止串清理不再受"最多 4 条"上限截断。
4. 脚本宿主一期地基(MVU 支持第一块砖)
- 消息级变量进模型:按上游语义存成 per-swipe 数组(
variables[swipe_id]),jsonl 往返保真,旧式"纯对象"形态不改形状。 - ST 权威事件表(104 项,取值 snake_case)+ 生成工具:App 旧表把"键当值"用,框架按 ST 语义订阅时名字对不上(实测 MVU 引用的 6 个事件名里有 4 个在我们表里是
undefined)。 - 宿主契约 + 文档构建 + 宿主运行时(
assets/script_host/host.js):0 期 spike 实测出的七条硬要求(saveChat、semver 版本、同名脚本互斥门、ST 事件表、世界书旧名 API、message.variables[swipe]、TH 方法摊成裸全局)全部实现。 - 真浏览器验证:把 Dart 构建出的宿主文档丢进无头浏览器,道渊卡内的 MVU + ZOD 起来了(11 个 API 成员、6 个脚本按钮、0 报错)。
5. 我在复核中补的三处
- 退出时建文件的判据收紧:只认"用户真的发过消息"。另一路的实现把开场白也算作"有内容",于是打开一张卡看一眼就退出会为每张看过的卡在服务端建一份只含开场白的对话文件(首页「最近对话」会堆满空对话)。
- EJS 真值口径对齐 JS:字符串
'false'/'0'一律为真(Boolean('false') === true)。旧写法把'false'当假,与网页端相反 —— 卡里存字符串开关时会走错分支。 - EJS 控制行容忍尾随注释(
<% } else { // 说明 %>):此前条件为真时两分支同发、条件为假时 else 正文静默丢失(连上报都没有)。现按分支门控,认不出的控制行也如实上报。
(另把语料体检测试的超时从 30s 放宽到 5 分钟 —— 它要遍历 80+ 对话,机器一忙就假红。)
6. 如实说明(重要)
- 审查设计里有 10 项这次没做,别以为都修好了。其中一条是 P0:正文出现孤立的
<%时,这条消息后半段仍会被整段丢弃(代码里的"未闭合就截断"还在)。其余未做项:显示端/入库正则仍不带宏替换器(带宏的正则脚本失效)、placement=6(推理)脚本从不执行、世界书 sticky/cooldown 到期不清盘(删消息后会"复活")、递归步数仍多跑一轮、@@activate/@@dont_activate未实现、"导出全部预设"同名互相覆盖、首页空态/搜索无结果仍不能下拉刷新、服务器懒加载时"按描述搜索"仍静默无结果、预设/世界书编辑页的数值无上限与deepCopy性能、短请求超时与连接未 close、仓库根仍没有analysis_options.yaml(flutter_lints依然没生效)。 - 没有真机验证。判定依据:本机 SillyTavern 1.18.0 源码逐条对照 + 你的真实语料与 settings.json + 定向探针(EJS 边界、MVU 启动)+ 全量
flutter test676 条通过 +flutter analyze lib test0 告警。退出时序(上面第 1 节第 4 行那条)本机测不到,两路都只能静态判断。 - 脚本宿主只到"能起来":
stat_data的初始化落地(MVU 把数据写进"正在生成的那层")与接进聊天页(进出场、事件转发、写回落盘、面板刷新)是下一步。 - 预设立场的接管(第 1 节第 1 行)会让生成结果与上下文长度跟着网页端走 —— 你手机上"感觉变了"是预期,不是新 bug。
安装包:pocket_tavern_1.10.16.apk
掌上酒馆 v1.10.15
掌上酒馆 v1.10.15
这一版先是一次全量语料体检(87 份真实对话、772 条消息、逐张角色卡),拿 App 的显示管线把每条消息跑一遍看落哪条渲染路径;体检抓到两处「显示不正常」,两处都已修。
1. 修:面板判据比网页端严 —— ERA 系卡片的状态栏在 App 里是「一屏 HTML 源码」
现象与根因
App 判断「这段围栏是不是可交互面板」用的是自己的约定:内容要以 <!DOCTYPE html 或 <html 开头。网页端酒馆助手(JS-Slash-Runner)用的是另一套(src/util/is_frontend.ts):
['html>', '<head>', '<body'].some(tag => content.includes(tag)) // 内容里含这几个就够了比 App 宽松得多。于是以 <head> 开头的面板在网页端渲染成面板、在 App 里落进「普通代码块」分支:用户看到的是整屏 HTML/JS 源码。
体检实测命中的围栏(旧判据会漏、网页端会渲染):
| 卡片 | 围栏 | 大小 |
|---|---|---|
| 肉便器仙子 | ERA 状态栏(<head><meta …> 开头) |
31,114 字 |
| 肉便器仙子 | 行动选项(打包好的 Vue 前端) | 6,712 字 |
| 圣樱学院 / 性癖大世界 | 面板只是一段 loader:$('body').load('https://…') |
130–144 字 |
改法
isFrontendHtml() 逐字对齐上游判据(message_segments.dart),message_bubble 的面板分支改用它。体检数据:围栏段 272 条里按现有判据命中 147 条(旧判据 138),「面板降级成代码块」4 → 0;原先 116 条两边都不算面板的(例如 NTR 卡的 ASCII 状态栏)保持不变,仍是代码块。
test/message_segments_test.dart 新增 4 条判据锚点(<head> 开头 / Vue 打包前端 / 裸 </html> / 纯 <div> 片段不算)。
2. 修:面板缺网页端那套全局库 —— Vue 面板整块空白
现象与根因
网页端 JS-Slash-Runner 会给每个面板 iframe 预置一批全局库(third_party_message.html:tailwind、fontawesome、jQuery+UI+touch、Vue runtime、vue-router;predefine.js:从主页继承 lodash、toastr、YAML、showdown、EjsTemplate)。卡片面板普遍不加守卫直接用:
- 道渊 v5.3:
window.Vue69 处、window._19 处、toastr.xxx14 处 - 肉便器仙子:整块状态栏就是一个 Vue 应用(
const a=Vue+<div id="app">)
App 此前只注入 jQuery。真浏览器复现:Uncaught ReferenceError: Vue is not defined,#app 子节点 0 个、可见文本 0 字 —— 面板一片空白。
改法
assets/vendor/新增 vue 3.5.43(runtime global prod) 与 lodash 4.17.21(均 MIT,来源/版本/校验见该目录README.txt),随 APK 打包;- 面板文档按需注入(
core/chat/webview_vendor.dart):自带<script>的文档一律注入两个(含 loader 型面板 —— 远端界面要用什么全局静态看不出来,网页端也是无条件预置);纯 markdown 富文本段不注入; - toastr 不落盘:用一段 shim 把
toastr.success/info/warning/error接到宿主提示(E:通道 → 应用内 Snackbar),clear/remove留空实现免得卡片脚本断在缺方法上。
验证
真浏览器里按 App 的注入路径拼页面(本地 vendor 文件 + 真 shim + 真实面板),肉便器仙子的状态栏渲染出正文:
苏沧兰状态 苍岚剑府·灵丹坊 · 482年 启灵季 三月 26日 辰时 开 概览 神通 肉身 储物 天地 …
修真状态 修为境界 炼气期一层 修为进度 0/100 淫欲之心 0 气血 100 …
注入前同一页面是 ReferenceError + 空白。test/webview_vendor_test.dart 7 条锁判据与资产登记。
3. 体检里「没问题」的部分
- 772 条消息:HTML 面板段 156、富文本 WebView 段 60,其余走纯文本/原生渲染;没有出现「围栏没切开」「内容被吃空」「变量快照 JSON 当正文」这几类。
- 4 条「某条正则该生效没生效」逐条比对后确认网页端同样如此、不是 App 的锅:模型在自己的正文里先写了一遍
<StatusBar>字样(复述指令),而卡片那条正则没有g标志 —— ST 也是「只替换第一处」(regexFromString+String.replace),真状态栏照样裸露成 YAML。属模型输出/卡片侧。 - 4 个会话目录没有对应角色卡(历史遗留、名字是 URL 编码或乱码),按原文渲染,正常。
4. 仍然缺的(本轮没动,需要你定)
- Mvu(MVU 变量框架):道渊 v5.3 的面板里有 36 处
Mvu.getMvuData / replaceMvuData / events。装上 Vue 后它的 UI 组件能挂载了,但变量/事件层还是没有 —— 道渊的面板只能算「半通」。App 里同样没有 EjsTemplate / faker(docs/扩展兼容可行性研究.md早记为 gap)。要不要补 Mvu 是个独立且不小的工程,等你发话。 - 面板在流式生成期间仍走纯 Dart 渲染器(防平台视图闪烁),script-only 面板那几秒是空白或显示自身源码;生成结束挂 WebView 后正常。行为与上一版一致,未改。
如实说明:
- 没在真机上肉眼看过(同前几版)。判定依据是:全量语料跑 App 自己的显示管线(772 条)+ 真浏览器里用真实卡片面板和本地 vendor 文件复现/验证。
- 面板判据放宽后,App 会像网页端一样把「含
<head>/<body的围栏」都当面板渲染 —— 如果某张卡本意是展示 HTML 源码示例,两边都会渲染成面板,这与网页端一致。 - 注入的 vendor 库是新增的第三方代码(Vue/lodash,MIT),来源与校验值记在
assets/vendor/README.txt;升级这两者时请同步更新该文件。
安装包:pocket_tavern_1.10.15.apk
掌上酒馆 v1.10.14
掌上酒馆 v1.10.14
修「状态栏类卡片的行动选项点了没反应」:「淫符控仙录」这类模板的选项点击是直接去操作网页端那个聊天输入框元素(#send_textarea),App 的面板里没有这个元素,卡片查不到就静默 return —— 点一下什么都不会发生。顺带补上 /setinput 命令。
1. 现象与根因
「淫符控仙录」的状态栏面板(同一族模板还有肉便器仙子、三人逆行、双人成行、夏瑾天琴座)点行动选项时执行的是:
const $textarea = $(parent.document).find('#send_textarea'); // 淫符控仙录
const inp = window.parent.document.querySelector('#send_textarea'); // 三人逆行 / 双人成行
if ($textarea.length === 0) { console.log('未找到聊天输入框'); return; } // ← 静默返回网页端这个元素就是酒馆自己的输入框,卡片读它、往后追加、再触发 input —— 于是选项文本就落进输入框。App 里卡片面板本身是顶层文档(parent === window),页面里根本没有 #send_textarea,所以分支直接返回:点击有视觉反馈(卡片自己会高亮一下),但什么都不会发生。
这与上一版(v1.10.13)修的那类不一样:齐鲁/道渊那批走的是 triggerSlash('/send …|/trigger'),这批走的是直接操作 DOM。
2. 改法:给面板补一个 #send_textarea 桥
面板文档里注入一个同名隐藏输入框(position:fixed 移出视口,不进文档流、不参与高度上报)。卡片照旧读写,写入由宿主转成输入框操作:
- 前缀扩展(卡片在原内容后面追加,模板的普遍写法)→ 追加进 App 输入框(
_appendInput); - 整段改写 → 替换输入框内容(
_fillInput); - 写成空串 → 不动(不替用户清掉已经打的字;网页端那种"清空"动作在 App 里是静默无操作)。
检测方式两条都挂上:卡片改写后大多会派发 input(jQuery 写法还会 trigger('input'))→ 收到事件立即上报;只改值不派发事件的,250ms 轮询兜底,值没变不重复上报。空输入框的初始值就是"用户没打过字",卡片读到的 currentContent 为空时行为与网页端一致(不额外加换行)。
顺带补一条同族通道:/setinput 文本 现在会把文本整段设进输入框(道渊 v5.3 的「非直发」模式用的是它,此前点了同样没反应)。
#send_textarea 只在不存在的文档里注入;卡片自己渲染了同名元素时不干预。
3. 测试
- 全量
flutter test:632 条全部通过;dart analyze lib test:0 告警。 tools/verify_trigger_slash.cjs扩到 16 条断言:管道那 10 条 + 桥的 6 条(首点追加、连点 read-modify-write、value +=不派发事件走轮询、整段改写走替换、写成空串不动、重复轮询幂等)。桥的用例跑的是真实 shim 源码,用最小 DOM 桩驱动。- 新增
tools/panel_input_bridge_probe.cjs:生成真浏览器探针页 —— 把卡片那条状态栏正则产出的面板 HTML、App 自带 jQuery、真 shim 拼成文档,自动点第一个行动选项,把宿主收到的消息写进 DOM。用无头 Edge 跑真实聊天记录(淫符控仙录 2026-09-17 那条 A 类回复):- 面板渲染出 4 个选项;
- 程序化点击第 1 个选项 → 宿主收到
A:[最佳] 将中指整根拔出再重新捅入,加快抽送速度,同时拇指按压阴蒂(序号1.是卡片自己去掉的,与网页端一致); #send_textarea存在、无 JS 报错。- 修复前同一份探针页的结果是
posted: [](点击什么都没发生),即本报告的现象。
- 该探针页写出的 DOM 结果里还带 diag(shim 函数是否挂上、yj/yaml 长度、卡片选项列表 HTML),方便以后出问题时一眼看出断在哪一环。
4. 如实说明
- 没在真机上点过。判定依据是真实 Chromium(无头 Edge)+ 真实卡片面板 + 真实聊天记录,点击走的是卡片的 jQuery 代码路径;App 的 WebView 同为 Chromium 内核,但不是同一台机器,输入框落字后的界面表现(比如光标位置)没在手机上看过。
- 这次修的是默认模式(卡片设置里的「点击复制到聊天框」)。卡片设置里的另一种模式「点击直接执行」走
triggerSlash('/send …|/trigger'),那是 v1.10.13 修好的那条通道。 - 注入的输入框是隐藏的:卡片如果指望它可见(个别模板会把它当可见控件用),在 App 里看不到 —— 目前没遇到这种卡。
- 轮询最长 250ms 才落进输入框:点了之后文字可能"隔一拍"出现,不是没生效。
- 上一版(v1.10.13)说明里提到的那份本地探针
test/chinv_pipeline_probe_test.dart仍未入库。
安装包:pocket_tavern_1.10.14.apk
掌上酒馆 v1.10.13
掌上酒馆 v1.10.13
修一个卡片面板的回传缺陷:状态栏卡片的「行动选项」点一下,输入框里会多出一截 /trigger,按发送后这截指令还会跟着写进聊天记录。测试卡:「【ntl】我在齐鲁当黑人交换生」。
1. 现象
点行动选项,输入框(原样)填进来的是:
[隐晦挑逗]黄毛不急着进入,只用龟头在她唇上碾磨,命令她自己主动含进去,看她羞耻又渴望的样子。|/trigger
末尾那截 |/trigger 是卡片的内部指令,本不该出现在文本里。如果直接发送,它会随正文一起入库 —— 用户那份聊天备份里就能看到这么一条(build/chat-backup-20260923/【ntl】我在齐鲁当黑人交换生/…jsonl 第 8 条),之后它还会作为历史进提示词。
2. 根因:App 侧的 shim 不认识 STscript 的管道
这类状态栏卡片(模板「萧谴写卡助手 v3.5Beta 状态栏美化」,齐鲁 / 淫符控仙录 / 道渊 v5.3 等都在用)的选项点击执行的是:
triggerSlash(`/send ${选项文本}|/trigger`);网页端 SillyTavern 的 STscript 把 | 当管道分隔符,整串是两条命令:先 /send 选项文本(写入这条用户消息),再 /trigger(触发生成)。所以网页端聊天里只有干净的选项文本。
App 的 WebView 宿主 shim(lib/presentation/widgets/message_web_view.dart)此前用一句 match(/^\/(\S+)\s*([\s\S]*)$/) 解析命令,/send 之后整段都当正文 —— |/trigger 就这么进了输入框。它同时意味着:任何走这条模板的卡片、任何写成管道形式的选项点击,都会带上这截尾巴。
3. 改法
shim 里按 STscript 语义先切管道、再逐段执行。切分规则刻意收窄,避免误伤文本本身:
- 只有竖线后面跟着一条命令(
/开头,或闭包{:)时才切; ||(连写管道)与管道周围的空格一并吞掉,段与段之间按顺序执行;'/random a|b|c'这种参数自带竖线的命令保持整段(App 支持的/random用|分隔候选项);- 参数里的宏带竖线(
{{getvar::x|y}})不受影响 —— 它后面跟的不是命令; - 竖线后面不是命令时原样保留:宁可多留一点文本,也不吞用户内容。
/trigger 这类「生成」命令被吃掉、不产生动作。这是 App 的既有契约:面板点击只把文本送进输入框,生成由用户按发送触发(不开放「卡片脚本代用户发送」的通道,否则一个卡片脚本在页面载入时就能自动发消息、耗额度)。点选项后再按发送,结果与网页端一致:干净的选项文本进聊天 + 开始生成。
无管道的命令(/send 文本、/echo、/setvar…)行为与上一版逐字节相同。
4. 测试
- 全量
flutter test:632 条全部通过;dart analyze lib test:0 告警。 - 新增
tools/verify_trigger_slash.cjs:从message_web_view.dart里抠出真实注入的 shim 源码,在 node vm 里真跑一遍,9 条断言 —— 含本卡的真实选项串、道渊的'/send x || /trigger'写法、/random a|b|c不被切碎、宏内竖线保真、多段顺序执行、竖线后非命令原样保留。 - 新增
test/webview_trigger_slash_test.dart:把上面这份校验钉进 flutter 套件(本机没装 node 时自动跳过),防回退。
5. 如实说明
- 没在真机上点过。判定依据是把注入 WebView 的那份 shim 源码抽出来在 node 里执行真实卡片指令串(修复前的版本跑同一组用例会输出带
|/trigger的结果,修复后不再有),不是真机截图。 - 点一下仍然是「填进输入框、你再按发送」,不是网页端那种「点一下就直接发送并生成」。如果希望选项一点就推进剧情,这是一处产品决策(要开卡片代发的口子),本轮没动,可以单独提。
- 相邻缺口没动:道渊 v5.3 的「非直发」模式用的是
/setinput 文本,这个命令 shim 目前不认识,在 App 里点了没反应。 - 本机测试套件里那份
test/chinv_pipeline_probe_test.dart(未入库的本地探针)原先写死了一份 2026-09-30 的聊天文件路径,本机已不存在导致套件报红;顺手让它自动取最新语料、语料缺失时跳过。它不参与发版内容。
安装包:pocket_tavern_1.10.13.apk
掌上酒馆 v1.10.12
掌上酒馆 v1.10.12
两件事:提示词编排与网页端对齐(预设读错桶、条目角色丢失、触发条件语义错、预设自带正则不生效),以及气泡最后一行被横切掉半截的渲染修复。
1. 预设:四处与网页端不一致的地方改齐
1.1 提示词顺序表读错了桶(影响最大)
网页端的聊天补全把「提示词顺序表」统一存在 100001 这一个全局桶里(上游配置 promptOrder: { strategy: 'global', dummyId: 100001 },不分单人/群聊)。App 此前按「单人读 100000、群聊读 100001」分桶,于是一直读的是 100000。
后果:社区预设(例如夏瑾天琴座)真正在网页端 Prompt Manager 里排好的顺序全写在 100001,App 读不到,只能发出 100000 里那几条骨架 —— 用户看到的现象就是「电脑上调好的提示词顺序,手机上发出去不一样」。
现在只认 100001。老预设只有 100000 一个桶时回退读它:网页端这种情况下会按出厂骨架现建一份空表、老编排全部失效,App 选择保留作者意图,这是一处有意的宽容偏差。
1.2 预设条目的 role 不再一律当 system
预设里每个条目都可以自带 role(system / user / assistant),社区预设常把角色描述、世界书前缀标成 user。App 此前只认 system,等于把作者排的对话结构压平了。
现在按上游 prompt.role = collectionPrompt.role ?? prompt.role 透传,占位条目(charDescription、worldInfoBefore 这类)也认;只放行三个合法值,预设里写的 model 之类非常规值回退 system(上游会原样透传,App 收口以免被 OpenAI 系端点直接拒掉)。作者注的 note_role 同样透传。
1.3 injection_trigger 语义纠正
上游这个字段匹配的是本轮生成类型(normal / continue / impersonate),App 此前实现成「拿最近聊天文本去扫关键词」。所以「设了触发条件却从不生效 / 不该出现时却出现」都是这一处。
现在按生成类型门控,生成类型随请求一起走管线。附带对齐一处:被禁用的 main 条目仍保留锚点位置(前情摘要、作者注是按 main 定位的,上游 getPromptCollection 就是这个语义)。
1.4 预设自带的正则脚本现在会生效(并且照抄网页端的授权门)
网页端正则引擎是三层:全局 → 预设 → 角色卡,App 此前只有全局和角色卡两层,预设层整个缺失。
补上预设层的同时,把网页端那两道门一起搬过来:① Regex 扩展总开关关掉时三层都不跑;② 预设必须在服务端 preset_allowed_regex 授权名单里(按「当前 API + 当前预设名」)。没授权时不静默失效 —— 聊天菜单里会多出一条「允许当前预设的正则脚本」,点一次确认后写进服务器上的同一份名单,回到电脑网页端也能看到这条授权记录。
2. 修复:气泡最后一行被横切掉半截
2.1 现象
富文本气泡(带折叠块/状态栏/表格那类)底部那一行只剩上半截,字没丢但看不全。实测角色卡「好兄弟的痴女女友」第 2 条回复:正文下面那行「💌 林若曦的悄悄话」标题 23px 高,只画出来 7px。
2.2 原因
这类气泡由内嵌网页视图渲染,窗口高度由页面自己上报。上报口径量的是「内容自身的跨度」,量不到内容上方那段真实存在的留白:正文第一段自带 16px 上边距,会把整块内容往下推 16px,同时这 16px 也算在页面高度里。于是上报 770、真实内容底在 786,窗口被设成 770,底部 16px 直接裁掉。
2.3 影响面
拿服务器上真实聊天记录量了一遍:75 个对话文件、660 条 AI 回复里会走这条渲染路径的正文段抽 40 段,20 段被裁,裁 10–32 像素,涉及 Vivi 6、九九、出轨APP、好兄弟的痴女女友、桃桃、桃色公寓、鬼父 七张卡。
2.4 改法(以及为什么不用最省事的写法)
最省事的写法是直接采信页面自身高度,但那正是以前踩过的坑:状态栏卡片会把页面高度绑到视口(min-height:100vh),「上报值 ↔ 窗口高度」互相喂,一路放大收不回来(道渊 v5.3 整块内容不显示就是这个)。
改成:只在页面自身高度比内容跨度高出「普通留白级」(≤64px)时才采信它,差额一大就退回原口径。误判的最坏结果是多出 64px 空白,不会失控。
3. 测试
- 全量
flutter test:631 条全部通过;dart analyze lib test:0 告警。 test/webview_height_test.dart新增 2 条契约,锁住「留白级差额内采信页面高度」以及「采信必须排在视口守卫之后、且带差额上限」。做过变异自证:把这次新加的分支删掉重跑,这两条确实报错,还原后复绿。- 新增
tools/height_metric_probe.cjs:直接从产品源码里抠出那段测量 JS,在真浏览器里跑 7 个用例(5 条视口自指护栏 + 2 条本次修复)+ 可选真实语料。本轮结果:护栏 5 条全部不随视口漂移,markdown 正文 62→94(等于真实内容高)、正文+折叠块 62→78(等于真实高),40 段真实语料裁切数 20→0、最大裁切 32px→0、无一段报高。 - 预设那四处改动的行为由
test/preset_test.dart、test/prompt_order_test.dart覆盖(含分桶回退、role 透传、生成类型门控、预设正则两层授权门)。
如实说明:
- 本次两处改动都没在真机上肉眼看过。渲染高度只能在装了包的机器上验证,浏览器量测与真机视口相当但不是同一台机器;预设正则授权那条菜单项的排版同样未过真机。
- 上一个版本 v1.10.11 从未发布:那次提交(思考强度自定义设置)既没推到代码仓、也没建 Release,只留了一个本地安装包。本包把它一并带上,所以从 v1.10.10 升级的用户会同时拿到思考强度设置 + 本版这两组改动。
- 1.1 的分桶纠正会改变实际发给模型的提示词内容与顺序。如果你的卡在网页端本来就没编排 100001 桶,行为不变;编排过的会看到回复风格变化 —— 那才是作者本来的设定。
- 1.4 的授权名单与网页端共用同一份服务端数据,在手机上点「允许」等于在网页端的 Regex 扩展面板里勾了同一个预设,反向亦然。
安装包:pocket_tavern_1.10.12.apk
v1.10.10
掌上酒馆 v1.10.10
修一个上一版生成统计的显示毛病,并把这个功能的外观用自动化测试锁住。
1. 修:慢回复显示成「0 tok/s」
现象:一条很短但生成很久的回复(比如 11 个 token 跑了 95 秒),气泡上会显示 0 tok/s。
问题在于这个 0 是四舍五入出来的:真实值是 0.1。而 0 tok/s 读起来像"模型卡住了 / 渠道坏了",会让人误判去换渠道。
修完:速度低于 10 tok/s 时保留一位小数,显示 0.1 tok/s、3.7 tok/s;不低于 10 的仍取整数(77 tok/s),避免出现 55.0 这种噪音。
2. 补:把这一行的外观钉进测试
新增 6 条渲染测试,把气泡真正渲染出来、直接断言那串文字。以下格式以后谁改坏了都会红:
| 场景 | 显示 |
|---|---|
| 新生成(正文 + 思维链) | 20.0s · 首token 1.2s · 77 tok/s |
| 老对话(v1.10.8 之前生成的,没记过首 token) | 20.0s · 55 tok/s |
| 慢渠道 / 短回复 | 30.0s · 3.7 tok/s |
| 超长回复 | 1m35s · 首token 1m01s · 0.1 tok/s |
| 完全没有耗时数据的消息 | 整段不显示,只剩时间戳 |
分隔符、单位、小数位、"缺数据就不显示(不补 0)"这几条规则一并锁住。
关于统计口径(沿用 v1.10.8 的说明):「首 token」只数正文第一个字,思维链先到不计时;「tok/s」是正文 + 思维链的合计平均速度;token 数是估算(与上下文预算同一口径),横向比较可信,拿去和供应商账单对会对不上。
如实说明:这一行文字的内容已由测试逐字锁定,但尚未在手机屏幕上肉眼确认过排版(会不会把变体箭头挤窄)。
安装包:pocket_tavern_1.10.10.apk
v1.10.9
掌上酒馆 v1.10.9
这一版对使用者没有任何功能或界面上的变化 —— 它是一次工程维护版本,改动全在仓库与开发工具链上。如果你不是自己编译/维护这个仓库,可以不用升级。
做了什么
- 仓库瘦身:15 个历史安装包(1.5.0 ~ 1.9.5)此前一直被版本库跟踪,现已移出跟踪。发版产物统一走 Release 下载,不再进版本库。
- 说明:这一步不会让已有仓库的历史体积变小(那需要重写历史 + 强制推送,代价是协作者要重新 clone,本版没做)。它的实际效果是"以后不会再往里堆",以及新 clone 可以浅克隆规避。
- 清掉 6 个"伪 submodule":
SillyTavern、几个参考实现目录、以及自带的 Flutter SDK 目录,当初是以 gitlink 方式入库但没有配套的.gitmodules—— 别人 clone 下来全是空目录,而且 SDK 那个会让git status永远挂着一个"已修改"标记。现在移出并加入忽略,状态栏脏标记实测消失。- 本地文件一律保留,只动索引。
- 静态检查规则升级:
flutter_lints3.x → 5.x。升级后全量分析零新增告警,等于免费拿到更严的新规则,以后写进来的代码会被管得更紧。
没做什么(以及为什么)
优化建议文档里另外几条性能改动,逐条核对后这一版没采纳,原因写在交付报告里,摘要:
- 限制历史消息的 WebView 数量:会把老卡片的样式和"点击翻看"交互一起弄丢(原生降级渲染器不解析
<style>与 CSS class,<details>折叠会全展开)。而且"每条富消息都常驻一个 WebView"这个前提不成立 —— 消息列表是懒构建的,全仓库没有任何一处请求保活,同时存活的 WebView 本来就只有视口附近的几个。 - 流式渲染增量缓存 / 大 JSON 放后台解析:都属于"没有实测数据支撑的优化"。前者现有代码已有 80ms 节流 + 签名缓存 + 超长降级三重兜底;后者要改网络层响应处理方式,风险大于收益。
- 保存请求合并:这条确实有价值(连续操作会重复上传整份聊天文件),但文档给的写法在这份代码里会造成"退出页面时那一次保存被合并掉",属于丢数据级别的坑。需要先设计一个能自动测的写法,放下一版。
安装包:pocket_tavern_1.10.9.apk
v1.10.8
掌上酒馆 v1.10.8
这一版加了一个生成统计,另外把上一轮整理对话代码时改出的三个真实问题一起带上了。
1. 新增:对话气泡显示生成统计
每条助手回复的时间那一行,现在除了总耗时,还会显示两个数:
15:11 56.5s · 首token 1.2s · 42 tok/s ‹ 1/7 ›
- 首 token:从发出请求到正文第一个字出现,你等了多久。思维链先到不计时——推理模型经常先吐几十秒思考,把那一刻算进来会比你实际的等待时间长得多,看着像"卡住了"。
- tok/s:这条的平均生成速度,正文和思维链都算。
三点要说清楚:
- token 数是估算值,用的就是 App 算上下文预算那套口径(中文约 1 字 = 1.1 token,其他约 3.5 个字符 = 1 token),不是模型自己的分词器。所以横向比较(这个渠道 vs 那个渠道、这条 vs 那条)可信,拿去和供应商账单对会对不上。
- 它是整条的平均速度,不是瞬时速度。被"续写"过的消息耗时是累加的,这个数会被摊平;续写也不会覆盖原来的首 token(保留你最初等了多久那个数)。
- 老对话会立刻多出 tok/s(总耗时本来就存着),但不会有"首 token"——这个数以前没记录,只有从这一版起新生成的回复才有。
2. 修:从角色页点进去,打开的是最老的一段对话
现象:一个角色有好几段对话时,从「角色」页点进去,落到的往往是最早那一段,而不是你最近在聊的。
原因:服务端返回对话列表时不排序(就是文件系统的读取顺序),而 App 直接取了第一条。
修完:改成按"最后一条消息的时间"取最近的那段;万一时间字段缺失,才退回服务端给的顺序。
3. 修:编辑消息 / 重命名时输入框可能提前失效
现象:弹出输入框、输入一半,界面会自己关掉或者内容没了;反复开关几次还可能泄漏。
原因:输入框的控制器在弹窗退场动画还没结束时就被销毁(另一条路径上则压根没销毁)。
修完:把弹窗改成自己管理生命周期的写法,动画期间不再提前销毁,关掉就一定释放。
4. 修:删掉某些对话后,草稿永远留着
现象:删除一段对话,它在「角色」页留下的未发送草稿删不掉,一直占着。
原因:置顶用的键和草稿用的键拼法不一样(对"角色头像为空"这种情况的处理不同),删除时按置顶那套拼法去清草稿,对没有头像的角色就清不掉。
修完:两处统一成一个键生成函数,删除时两个一起清掉。
另外这一版还包含对「对话」功能的一次较大代码整理(重复逻辑收敛、类型收紧、把一个 2200 行的核心文件按职责拆成 8 个)。对用户没有可感知的变化。
验证状态如实说明:上面第 2~4 项修复都在真机上验过;第 1 项(生成统计)过了静态检查、595 条自动化测试(含 4 处"故意改坏必被抓"的变异校验)和 release 构建,但没有在真机上跑过一次真实生成,所以那两个数长什么样、排版会不会太挤,还没亲眼确认。
安装包:pocket_tavern_1.10.8.apk
掌上酒馆 v1.10.7
掌上酒馆 v1.10.7
这一版修两个"和网页端不一致"的显示问题,都用真实角色卡复现、并逐项量过数。
1. 状态栏卡片字数多的时候显示不全
现象:像「女拳师的男闺蜜」这类卡片,状态栏里字一多,底部内容就被切掉一截,往上滑也滑不到底。
原因:这类卡片的宽度是写死的(380px),手机屏幕窄,App 会把它压到 300px。卡片为了保持竖版比例(3:4),宽度一窄、高度就跟着等比缩水——380×507 变成 300×400。可卡片内部那层内容是绝对定位、自己滚动的,外层高度缩了它并不跟着变矮,多出来的部分就直接被裁掉了。
修完:App 现在先量出卡片内容真正需要多高,再把卡片撑到那个高度,不再强行维持比例。同一张卡在手机上从「300×400、裁掉 95px」变成「300×495、裁掉 0px」。代价是卡片不再严格保持正反面同比例(统一到较高的那一面),换内容完整显示。
2. 正文里多出一个 </正文>
现象:消息里显示出一行 </正文>,网页端同样的消息却没有。
原因:这个标签名是中文。浏览器解析时 <正文> 和 </正文> 的待遇不一样——开标签会当成普通文字显示出来(所以网页端其实也看得见开头的 <正文>),闭标签则整段被当成注释丢掉。App 走"纯文本"快捷渲染时直接显示原文,没做这个区分,闭标签就露出来了。
修完:闭标签也丢掉、开标签照旧保留,与网页端逐字一致。
安装包:pocket_tavern_1.10.7.apk
掌上酒馆 v1.10.6
这一版修两个"和网页端不一致"的显示问题。
1. 对话列表里的时间差了 8 小时
现象:对话里最后一条明明是 03:47,列表上却写「昨天 19:47」。
原因:列表拿服务器返回的不带时区提示的时间直接当本地时间显示,也没换算就来判断"今天/昨天"。所以钟点差 8 小时,凌晨的对话还会被标成昨天。
修完:列表时间和对话里的时间一致,日期归属也按你手机所在时区算。
2. 状态栏的"禁止查看区"不再摊开
现象:黒人転校生这类卡片的状态栏里,"禁止查看"那一块在网页端是默认收起、点一下展开的,但在手机上整块摊开,也没有展开/收起的按钮。
原因:卡片用的是网页标准的折叠标签(<details>)。这个标签 App 认得(所以没把它当垃圾清掉),但"认得"和"能画出来"是两回事——折叠需要点击交互,而 App 的轻量渲染器不能交互,于是就平铺给你了。现在这类带交互的块会交给完整网页引擎渲染,收起和点开都由浏览器处理,和网页端一样。
一个小现象:AI 正在生成这条消息的过程中,那块还是展开的;生成一结束就变成收起的折叠块。这是为了避免流式输出时反复重建网页视图导致闪烁,属于有意的取舍。
说明
- 上面两条都只影响显示,不改聊天文件内容,也不影响发给模型的内容。
- 之前那轮"对话时间戳打错时刻"的四处问题(助手回复的时间打在发出请求那一刻、切变体不更新、续写不更新、新对话开场白每次重打)这一版没有改,它们是几十秒到分钟级的偏差,不是这次的 8 小时。要修的话说一声。
校验:静态分析无问题,自动化测试 522 项全通过(新增 3 项回归锁,其中折叠那条是用真实卡片结构端到端跑的)。