进阶技巧#

请选择你感兴趣的部分进行阅读.

自定义项目配置#

  • .cursor/rules 中, 我预先为项目配置了一些编程助手编写规则 (相当于给编程助手添加了一个全局世界书). 你完全可以自行编写更多规则

  • .cursor/mcp.json 中, 我预先设置了 BrowserTools MCP 来让 AI 能够查看酒馆网页. 你完全可以为 AI 找更多好用的 MCP, 如效用 figma 设计好界面再用 figma MCP 让 AI 按设计结果编写

  • package.json 中, 我预先为项目代码添加了 jquery、zod 等方便的第三方库. 你可以让 AI 或自己用 pnpm add 第三方库 添加更多需要的第三方库, 它们一般添加上就能直接使用

常用资源网站#

免费字体#

你可以从 ZeoSeven Fonts 中找到很多免费字体.

Sarasa Gothic 更纱黑体 Mono 为例, 我们搜索到它, 进入它的页面, 点击右上角的嵌入到 Web 项目即会跳转到对应内容, 选择常规 CSS 然后点击复制即可.

../../../../_images/%E5%85%8D%E8%B4%B9%E5%AD%97%E4%BD%93.png

免费图标#

你可以从 FontAwesome 中找到很多免费图标. 它们在前端界面中直接可用.

免费 CDN#

jsDelivr 中有很多库或文件的 CDN. 你可以直接在脚本或界面中引用它们.

需要注意的是, 你应该用 https://testingcf.jsdelivr.net 这个国内也能访问的镜像, 而不是直接用 https://cdn.jsdelivr.net.

提示

如果是需要第三方库, 推荐直接通过 pnpm add 第三方库 来添加它们, 模板文件夹已经配置好会在打包时将第三方库转换为 jsDelivr 链接, 从而避免在多个脚本或界面中重复打包它们.

前端界面正则的推荐写法#

提高正则容错率#

你的前端界面可能对应于 AI 很复杂的输出格式, 但这些格式应该由前端界面的代码通过 getChatMessages 获取楼层消息内容后处理, 而不应该由酒馆正则处理.

前端界面正则唯一要做的是定位前端界面应该在的位置, 而不是处理输出数据.

例如, 如果你的输出格式是: (并不是说我推荐这种格式)

<status>
心情:→(平静通话)
衣着:居家长T恤
角色阶段:暗中观察
位置:自己房间
行动:结束与user通话
下一步行动:准备就寝
身体:及腰银发散落,深紫色的眼眸中带着一丝遗憾,白皙的肌肤在月光下泛着珍珠般的光泽,散发着淡淡的茶香
内心:心爱找他有什么事呢...不知道他会不会陪我过生日...
求签问卦:月下逢君意/花开并蒂时/缘定三生后
</status>

前端界面正则只需要定位这一段文本即可:

脚本名称: [界面]状态栏
查找正则表达式: <status>.*</status> # 里面具体是什么不由正则在意
替换为: 前端界面的 html 代码块
作用范围:
  - [x] 用户输入
  - [x] AI输出
短暂:
  - [x] 仅格式显示
  - [ ] 仅格式提示词

避免渲染卡顿#

酒馆在显示代码块时会使用 highlight.js 对它进行着色高亮处理, 但前端界面的 html 代码块多则几万字, 在被处理时会非常卡顿. 而且对前端界面的 html 代码块进行着色高亮处理本就没有意义: 它马上就会被酒馆助手渲染成前端界面.

为了避免前端界面正则替换时的卡顿, 我们应该使用 text 代码块来包裹前端界面:

```text
前端界面打包结果...
```

另一种避免渲染卡顿的方案是像下文 "发布会自动更新的前端界面或脚本" 那样, 将 html 转换为链接而正则代码块里只加载链接. 这样酒馆需要渲染的代码块内容没多少, 自然不会卡顿.

使用 vue 编写前端界面#

酒馆助手前端界面和脚本可以直接使用 vue, 它会让数据显示变得更为简单. src/界面示例 就是这么做的.

与外部应用程序通信#

酒馆助手脚本可以安装和使用 socket.io-client 乃至所有浏览器环境支持的第三方库, 从而和外部应用程序进行通信. 实时编写角色卡、世界书或预设 (StageDog/tavern_sync) 就是如此实现的.

如果你要传输数据, 请注意调整服务器端的 maxHttpBufferSize 参数, 它默认仅为 1mb.

在脚本中用 jquery 修改页面元素#

在脚本中, 你可以用 jquery 来修改页面元素. 酒馆助手内置库中的预设条目更多按钮预设防误触等脚本都是通过这种方式来实现的:

预设防误触#
function lock_inputs(enable: boolean) {
  // 用 jquery 访问酒馆页面元素, 让它们不能被修改
  $('#range_block_openai :input').prop('disabled', enable);
  $('#openai_settings > div:first-child :input').prop('disabled', enable);
  $('#stream_toggle').prop('disabled', false);
  $('#openai_show_thoughts').prop('disabled', false);
}

$(() => {
  // 启用脚本时锁定预设设置, 防止误触预设
  lock_inputs(true);
});

$(window).on('pagehide', () => {
  // 卸载脚本时解锁预设设置, 允许修改预设
  lock_inputs(false)
});

甚至, 你可以不使用酒馆助手的前端界面渲染方式, 而是自己用 jquery 将代码块替换为要渲染的界面. 文生图行动选择框等脚本就是如此:

替换第 0 楼中含 <galgame> 的代码块为 galgame 界面#
const $galgame = extract_galgame_element();

const $mes_text = retrieveDisplayedMessage(0);
$mes_text.find('pre:contains("<galgame>")').replaceWith($galgame);

在脚本中对发送出的提示词进行修改#

除了通过 jquery 修改页面元素, 脚本也可以监听酒馆的提示词发送事件, 进而自行修改提示词.

要做到这一点, 我们可以监听的事件有很多, 如 (按请求生成时事件发生先后顺序) tavern_events.GENERATE_AFTER_COMBINE_PROMPTStavern_events.GENERATE_AFTER_DATA (提示词模板、酒馆助手宏发生在这里)、tavern_events.CHAT_COMPLETION_PROMPT_READYtavern_events.CHAT_COMPLETION_SETTINGS_READY.

此处以 tavern_events.CHAT_COMPLETION_PROMPT_READY 为例:

eventOn(
  tavern_events.CHAT_COMPLETION_PROMPT_READY,
  async (event_data: Parameters<ListenerType['chat_completion_prompt_ready']>[0]) => {
    // 移除非 user 提示词
    assignInplace(
      event_data.messages,
      event_data.messages.filter(message => message.role === 'user'),
    );
  },
);

function assignInplace<T>(destination: T[], new_array: T[]): T[] {
  destination.length = 0;
  destination.push(...new_array);
  return destination;
}

注意修改方式

不要用 event_data.chat = event_data.chat.map(message => ...) 之类的方法, 这是让 event_data.messages 指向一个新数组, 而不是修改 event_data.messages 原本指向的数组.

你应该使用:

  • lodash 中的某些原地修改函数

  • 数组的 splicepush 等函数

  • 上面代码中给出的 assignInplace 函数

流式传输前端界面#

简单方案#

要简单做流式传输, 我们使用酒馆助手的前端界面, 在其中设计发送按钮来触发酒馆助手的generate函数.

也就是说:

  • 玩家只在一个前端界面内进行游玩

  • 对于玩家的输入, 我们通过前端助手的 generategenerateRaw 命令自行要求 ai 回复, 并监听 iframe_events.STREAM_TOKEN_RECEIVED_FULLYiframe_events.STREAM_TOKEN_RECEIVED_INCREMENTALLY 事件来获取流式传输文本

  • 为了记录剧情, 我们使用酒馆助手的世界书、消息楼层接口, 直接将剧情写入世界书条目或新建消息楼层.

更自由的办法#

前面提到我们可以在酒馆助手脚本中用 jquery 修改页面元素, 那么我们完全可以把酒馆的楼层显示隐藏起来, 自己在它原本的位置制作一个楼层显示:

  • 监听 tavern_events.MESSAGE_SEND -> message_id, 我们可以知道玩家要求酒馆调用 ai 生成;

  • 监听 tavern_events.STREAM_TOKEN_RECEIVED -> text, 我们可以获取流式传输文本;

  • 监听 tavern_events.MESSAGE_RECEIVED -> message_id, 我们可以知道酒馆结束了回复.

这样一来, 我们是自己获取流式传输文本, 自己控制该怎么显示界面.

例如, 假设我们让 AI 以 YAML 的形式发送对话, 流式过程中可能得到:

- 角色: 络络
  对话: 杂鱼喵
- 角色: 青空莉
  对话: 杂鱼哦
- 角色: 络络 
  

我们可以在还没收到消息时就显示 Galgame 对话界面, 而只将已经完全发送的消息添加到界面中, 允许玩家翻页阅读.

@hakoyukaya 的流式 Galgame 界面

发布会自动更新的前端界面或脚本#

还记得我们是如何实现实时修改前端界面或脚本的吗? 我们利用 Go Live 将本地文件夹转换为了网络链接, 而正则或脚本通过网络链接来加载最新打包内容.

既然这样, 我们完全可以发布一个网络链接给玩家, 从而让玩家永远加载到最新的前端界面或脚本!

最简单的方式是利用 jsDelivr 为 github 文件提供的 CDN 功能. 对于上传在 github.com/组织名/仓库名路径 下的文件, 你可以直接用 https://testingcf.jsdelivr.net/gh/组织名/仓库名/路径 来访问它们.

酒馆助手内置库即采用了这种方法. 例如, 如果你从酒馆助手 ‣ 脚本库 ‣ 内置库中导入标签化, 编辑它就会发现它的代码里仅有一行:

import 'https://testingcf.jsdelivr.net/gh/StageDog/tavern_resource/dist/酒馆助手/标签化/index.js'

然而, jsDelivr 的服务器和玩家的浏览器都会缓存文件, 因此并不是 github 上文件更新后, 这个链接就会立即得到最新的打包结果.

你可以通过以下方式来刷新缓存:

  • StageDog/tavern_helper_template Use this template 来创建新仓库而不是仅在本地使用模板文件夹, 这个仓库配置了自动工作流, 会自动打包结果并在每次更新时都修改版本号,可以做到 12h 刷新服务器缓存

  • 更新版本号后,在 https://www.testingcf.com/tools/purge 中输入链接, 将 testingcf.jsdelivr 改成 cdn.jsdelivr, 然后点击确认, 能立即刷新服务器缓存

  • 玩家主动清除浏览器缓存

或者, 你可以在使用网络链接时尾附 ?time=时间戳, 不同的时间戳将被视为访问不同的链接, 因此会独立缓存:

永远最新 (但永远缓存不上需要从网上获取)#
import `https://testingcf.jsdelivr.net/gh/StageDog/tavern_resource/dist/酒馆助手/标签化/index.js?time=${Date.now()}`
每日刷新#
import `https://testingcf.jsdelivr.net/gh/StageDog/tavern_resource/dist/酒馆助手/标签化/index.js?time=${new Date().setHours(0, 0, 0, 0)}`

仅当代码不报错时才能成功打包#

无论是前端界面还是脚本都需要我们编写 typescript 代码. 当代码有语法错误时, 我们会在 Cursor 中看到报错:

../../../../_images/%E4%BB%A3%E7%A0%81%E5%87%BA%E9%94%99.png

显然, 我们应该先解决代码的语法错误, 再进行打包. 但当 AI 用 pnpm buildpnpm watch 验证打包是否成功时, 你会惊讶地发现即便代码里有报错依然能成功打包. 这是因为在打包时检查语法错误消耗的内存和时间都太多了 (我的仓库 StageDog/tavern_resource 需要耗费不开启时 10 倍的时间). 出于性能考虑, 模板文件夹默认关闭了这一功能.

要开启这个功能, 你需要打开 webpack.config.ts 文件, 将其中的两处 transpileOnly: true 改为 transpileOnly: false.