写卡攻略本AI玩家 · 创作者
技术手册

技术手册

JavaScript Hook 与交互

用安全 Hook 响应聊天事件,并在角色退出时完整清理副作用。

AI 助手可以代写

「收到消息时弹个提示」这类小机关,AI 创作助手能写(见第 10 关)。这一页讲 Hook 怎么注册、怎么清理。

JavaScript 用来响应发送、消息变化和页面生命周期。只改颜色、间距或动画时,优先使用 CSS。

先运行一个安全 Hook ​

把下面代码加入角色的“全局 JS”:

js
{
  const stopReady = $charx.hooks.on('onReady', function () {
    document.querySelector('.charx-chat-bg')?.setAttribute('data-ready', 'true')
  })

  $charx.hooks.once('onDestroy', function cleanup() {
    stopReady()
    document.querySelector('.charx-chat-bg')?.removeAttribute('data-ready')
  })
}

这段代码做两件事:

  1. 聊天运行环境就绪时,为聊天根节点增加 data-ready="true"。
  2. 页面销毁时取消监听,并移除这个属性。

外层 { ... } 用来隔离临时变量。多个 JS 片段合并执行时,不会因为都声明 stopReady 而冲突。

什么时候需要 JavaScript ​

需求是否需要 JS
改背景、字体、气泡和动画不需要,使用 CSS
把固定标签变成 HTML不需要,使用正则
原生折叠面板通常不需要,使用 <details>
收到特定消息后切换场景需要 Hook
发送前检查或修改文字需要 Hook
创建计时器、DOM 监听或额外节点需要 Hook,并且必须清理

完整的事件驱动示例见场景氛围动效。

注册方式 ​

  • $charx.hooks.on(name, handler) 注册持续监听,返回取消注册函数。
  • $charx.hooks.once(name, handler) 注册一次监听,也返回取消注册函数。
  • window.registerCharxHook(name, handler) 是 hooks.on 的宿主别名。

同一事件可以注册多个监听器,并按注册顺序运行。新脚本优先使用 $charx.hooks.on,不需要手工串接旧回调。

window.CharxHooks 是旧脚本兼容槽位。不要整体替换这个对象,否则可能覆盖其他脚本。onInit 是 onReady 的旧别名,onMessageSent 是 onAfterSend 的旧别名,新脚本不要重复注册新旧两个名称。

常用 Hook ​

Hook适合做什么
onReady(context)运行环境就绪后初始化界面
onBeforeSend(text, context)发送前检查文字;返回字符串可替换,返回 false 可取消
onAfterSend(text, context)用户消息发送后的通知
onStreamChunk(chunk, context)接收流式增量通知
onTransformMessage(message, context)返回非空字符串,转换最终消息内容
onMessageReceived(message, context)收到角色消息后更新界面状态
onMessageUpdated(message, context)消息最终更新后同步状态
onMessageEdited(message, context)消息被编辑后同步自定义 UI
onMessageDeleted({ id }, context)消息删除后清理关联状态
onChatChanged({ conversationId }, context)切换会话后重新读取状态
onDestroy(context)离开当前运行实例前清理资源

context 会提供当前宿主、角色和会话信息。previewSessionId、messageId、requestId、fullContent 只在对应宿主和事件提供时存在。

message 至少会提供 content,其他字段依事件和宿主而定。不要假定每个事件都有完整服务端消息结构。

正确清理副作用 ​

注册 Hook 后保存取消函数。定时器、DOM 事件监听器和脚本创建的节点也要在 onDestroy 中清理。

js
{
  const handleClick = function () {
    document.querySelector('.charx-chat-bg')?.classList.toggle('is-focused')
  }

  const stopReady = $charx.hooks.on('onReady', function () {
    document.addEventListener('click', handleClick)
  })

  $charx.hooks.once('onDestroy', function cleanup() {
    stopReady()
    document.removeEventListener('click', handleClick)
  })
}

Hook 回调返回一个函数,不会自动把它登记成清理函数。必须显式调用取消函数。

超时和错误 ​

共享运行时默认时间预算为 30 秒:

  • onBeforeSend 和 onTransformMessage 的整条监听链共享这个预算。
  • 其他通知事件按每个回调计时。
  • 发送前 Hook 返回 false 会取消发送。
  • 发送前 Hook 抛错或超时不能当作允许发送。
  • 转换 Hook 返回空白字符串会报错。
  • 通知类 Hook 抛错后会报告错误,并继续后续监听器。

超时不能停止同步死循环

超时只限制异步等待,不能抢占同步死循环,也不会自动撤销已经开始的副作用。回调必须短小、可结束,并在销毁时清理。

各端差异 ​

环境当前转换与运行边界
Creator 编辑预览host: creator、conversationId: 0,转换只更新预览消息,不证明正式消息已保存
Web 正式聊天消息保存完成后执行转换,再使用原内容和更新时间尝试并发写回
App 正式聊天原生层经 WebView 调用 Hook,再带并发校验条件保存

Hook 名称共享,不代表三个宿主的所有操作都会触发同样事件。发送、编辑、删除、切换会话和退出聊天都要在目标环境实际检查。

脚本限制 ​

合并后的角色全局 JS 上限为 100 × 1024 字节(100 KiB)。

禁止使用:

  • eval 或 Function 构造器
  • 直接访问 localStorage、sessionStorage 或 Cookie
  • fetch、XMLHttpRequest、WebSocket、EventSource、sendBeacon 等主动联网能力
  • 动态 import()
  • Worker、SharedWorker、BroadcastChannel

需要平台存储时使用 $charx.storage,并确认当前宿主的持久化范围。不要加载外部库绕过限制。

保存前检查 ​

  • 没有 JavaScript 也能完成的效果已改用 CSS 或原生 HTML。
  • 所有 Hook 使用注册接口,没有覆盖整个 window.CharxHooks。
  • 取消函数、计时器、DOM 监听器和新增节点都有清理逻辑。
  • 回调没有无限循环或长时间同步计算。
  • Creator 预览中没有控制台错误。
  • Web 和 App 的实际事件路径已经分别检查。
  • 合并脚本未超过 100 KiB,也没有使用禁止能力。