技术手册
JavaScript Hook 与交互
用安全 Hook 响应聊天事件,并在角色退出时完整清理副作用。
「收到消息时弹个提示」这类小机关,AI 创作助手能写(见第 10 关)。这一页讲 Hook 怎么注册、怎么清理。
JavaScript 用来响应发送、消息变化和页面生命周期。只改颜色、间距或动画时,优先使用 CSS。
先运行一个安全 Hook
把下面代码加入角色的“全局 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')
})
}这段代码做两件事:
- 聊天运行环境就绪时,为聊天根节点增加
data-ready="true"。 - 页面销毁时取消监听,并移除这个属性。
外层 { ... } 用来隔离临时变量。多个 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 中清理。
{
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,也没有使用禁止能力。