9. 全局 JS 脚本
全局 JS 通过 window.CharxHooks 注册生命周期钩子,响应平台事件,实现动态交互效果。
可用钩子
window.CharxHooks = {
// 对话页加载完成、$charx 就绪后触发(仅触发一次)
onReady: (context) => {
console.log('就绪,角色:', context.character.name)
},
// 用户点击发送前触发,可修改或拦截消息
// 返回修改后的字符串:修改内容;返回 null / false:拦截发送(慎用)
onBeforeSend: (message, context) => {
return message // 直接返回原消息不修改
},
// 用户消息发出后(AI 开始生成前)触发
onAfterSend: (message, context) => {
console.log('消息已发出:', message)
},
// AI 流式输出每个 chunk 时触发
onStreamChunk: (chunk, context) => {
// chunk 是当前新增的文字片段
},
// ✨ AI 回复写入数据库前触发,可拦截并修改内容(支持 async)
// 返回新字符串 → 替换原内容(UI 展示 + 数据库持久化同时生效)
// 返回 null / undefined → 保持原样
onTransformMessage: async ({ content }) => {
// 示例:去除旁白括号
// return content.replace(/([^)]*)/g, '').trim()
return null // 不修改
},
// AI 消息完整接收后触发(onTransformMessage 之后,content 为最终写入 DB 的内容)
onMessageReceived: (message) => {
// message = { id: string, content: string }(已去除 charx_vars / charx_choices 标签)
console.log('收到消息:', message.content)
// 注意:此钩子的返回值会被忽略,不要尝试在此修改消息内容(请用 onTransformMessage)
},
// 对话页卸载时触发(用于清理定时器等)
onDestroy: () => {
// clearInterval(myTimer)
}
}
Context 对象结构
interface ChatContext {
character: {
id: number
name: string
avatar: string
}
conversation: {
id: string
messageCount: number
}
user: {
id: number
nickname: string
}
}
示例:根据关键词触发音效
window.CharxHooks = {
onMessageReceived: (message) => {
if (message.content.includes('战斗')) {
$charx.media.bgm.play('battle_bgm')
}
return message
}
}
安全限制
- 禁止使用
eval() - 禁止访问
localStorage/sessionStorage(使用sdk.storage替代) - 禁止访问
document.cookie globalJs保存时会进行关键字校验,不符合规则的脚本会被后端拒绝- 单个角色卡
globalJs上限为1000KB - 代码在受限运行环境中执行;如需持久化,请优先使用
$charx.storage
调试
编辑 JS 时,点击 运行测试 按钮可以在编辑器内执行代码,下方的 调试日志面板 会显示 console.log 输出和错误信息。
9.1 音乐播放器编辑器
编辑器的「音乐播放器」标签页允许为角色卡配置背景音乐系统,用户在聊天界面会看到一个可拖动的浮动播放器。
配置项
| 配置项 | 说明 |
|---|---|
| 启用音乐播放器 | 总开关,关闭后聊天界面不显示播放器 |
| 自动播放 | 进入对话时是否自动开始播放 |
| 默认音量 | 0–100,初始音量 |
| 播放器位置 | 浮动播放器在聊天界面的默认位置(右下角等) |
| 循环模式 | 单曲循环 / 列表循环 / 随机播放 |
轨道管理
在「轨道列表」区域可以添加多个 BGM 轨道:
| 字段 | 说明 |
|---|---|
| 轨道名称 | 显示在播放器上的曲目名 |
| 音频 URL | 音频文件地址(支持 MP3/OGG/WAV) |
| 封面图 | 显示在播放器上的专辑封面 |
| 默认播放 | 勾选后该轨道为对话开始时的默认曲目 |
自定义播放器外观
在「自定义 HTML/CSS/JS」区域,可以完全替换默认播放器 UI:
<!-- 自定义播放器外观示例 -->
<div id="my-player" style="...">
<button id="prev">◀</button>
<button id="play-pause">▶</button>
<button id="next">▶</button>
<span id="track-name">加载中...</span>
</div>
<script>
// 通过 $charx.media 控制播放
document.getElementById('play-pause').onclick = () => {
void $charx.media.bgm.play('https://cdn.example.com/audio/theme.mp3', {
loop: true,
volume: 0.75,
fadeIn: 600,
})
}
</script>
在 JS 脚本中控制播放器
// 在 CharxHooks 中根据剧情切换曲目
window.CharxHooks = {
onMessageReceived(message) {
if (message.content.includes('战斗开始')) {
void $charx.media.bgm.play('https://cdn.example.com/audio/battle-theme.mp3', {
loop: true,
volume: 0.8,
})
} else if (message.content.includes('战斗结束')) {
void $charx.media.bgm.play('https://cdn.example.com/audio/peaceful-theme.mp3', {
loop: true,
volume: 0.6,
fadeIn: 400,
})
}
}
}
// 完整的 media API
$charx.media.bgm.play('https://cdn.example.com/audio/theme.mp3', {
loop: true,
volume: 0.7,
fadeIn: 500,
})
$charx.media.bgm.pause() // 暂停
$charx.media.bgm.resume() // 继续播放
$charx.media.bgm.stop({ fadeOut: 400 }) // 停止,可选淡出
$charx.media.bgm.setVolume(0.8) // 设置音量(0.0 - 1.0)
$charx.media.sfx.play('https://cdn.example.com/audio/coin.mp3', { volume: 0.9 })
$charx.media.sfx.preload([
'https://cdn.example.com/audio/coin.mp3',
'https://cdn.example.com/audio/click.mp3',
])
$charx.media.speak('欢迎来到世界', { voice: 'character', speed: 1, pitch: 1 })
$charx.media.stopSpeak()
10. 平台 API 能力
平台在运行角色卡时暴露了一套完整的 JavaScript API,可以在全局 JS 脚本和消息内嵌 HTML 中使用。
10.1 window.$charx — 主动调用接口
$charx 是全局 JS 脚本能调用的核心对象,在 CharxHooks.onReady 触发前即可使用。
作用域说明
- 全局 JS(
window.CharxHooks/window.$charx):角色卡的 globalJs 字段注入到主页面,直接调用下列 API。- 消息内嵌 HTML(iframe):AI 回复中的 HTML 片段运行在沙箱 iframe 里,所有
$charx方法均返回 Promise,须await。另外还能使用$charx.worldbook.*和$charx.chat.onMessage/onStreamChunk(详见 10.4 节)。
$charx.character
读取当前角色卡与用户 persona 信息(全局 JS 中同步返回,iframe 中返回 Promise)。
const info = $charx.character.getInfo()
// 返回:
// {
// id: number,
// name: string,
// avatar: string, // 头像 URL
// creator: string, // 创作者用户名
// displayIntro: string,
// firstMes: string,
// alternateGreetings: string[],
// tags: string[]
// }
console.log('当前角色:', info.name)
// 获取用户当前 persona(若未设置则返回 null)
const persona = $charx.character.getPersona()
// 返回:{ callName: string, gender: string, description: string } | null
if (persona) {
console.log('用户称呼:', persona.callName)
}
// 获取当前对话的 AI 配置
const settings = $charx.character.getSettings()
// 返回:{ model: string | null }
console.log('当前模型:', settings.model)
安全边界:
getInfo()只返回公开资料,不返回description、personality、scenario、系统提示词、记忆书内容、正则规则等内部运行时设定。需要影响 AI 行为时,请把内容写入角色卡字段本身,由服务端注入 prompt;不要通过前端 JS 读取这些设定。
$charx.chat
控制消息发送、AI 生成流程、历史消息读取,以及后台静默生成等高级功能。
// 代替用户发送一条消息(会触发 AI 回复流程)
$charx.chat.send('继续故事')
// 中止当前 AI 流式输出(AI 正在生成时有效)
$charx.chat.stop()
// 重新生成最后一条 AI 消息(相当于点击"重新生成"按钮)
$charx.chat.regenerate()
// 续写:让 AI 从上文继续输出(适合 AI 被截断时使用)
$charx.chat.continue()
// 获取当前对话的历史消息(全局 JS 中同步返回,iframe 中返回 Promise)
// opts 可选:{ limit?: number, offset?: number }
// 返回:Array<{ id: number, role: 'user' | 'assistant', content: string, createdAt: string }>
const history = $charx.chat.getHistory() // 全部历史
const last10 = $charx.chat.getHistory({ limit: 10 }) // 最近(末尾)10 条
const page2 = $charx.chat.getHistory({ limit: 10, offset: 10 }) // 第 11–20 条
// 示例:根据变量自动触发剧情
window.CharxHooks = {
onMessageReceived: (message) => {
const hp = $charx.variable.get('hp')
if (Number(hp) <= 0) {
setTimeout(() => $charx.chat.send('[系统:角色已倒下,触发结局]'), 500)
}
}
}
后台静默生成 generateRaw
不写入聊天记录,直接调用 AI 模型,适合后台计算、裁判判定、不可见旁白等场景。等同于 SillyTavern 的 generateRaw()。
window.CharxHooks = {
async onMessageReceived(message) {
// 在 AI 回复后,后台让模型判断战斗结果,不产生额外对话
const verdict = await $charx.chat.generateRaw([
{ role: 'system', content: '你是战斗裁判,只返回胜/负两字。' },
{ role: 'user', content: `玩家攻击力=${$charx.variable.get('atk')},敌人防御=${$charx.variable.get('def')},结果?` },
])
if (verdict.includes('胜')) {
await $charx.variable.increment('gold', 50)
$charx.ui.toast({ text: '战斗胜利!获得 50 金币', type: 'success' })
}
}
}
选项说明:
generateRaw(messages, options)第二个参数可选:
includeCharacterContext: boolean(默认true)— 是否在请求中包含角色设定和世界书model: string— 指定模型,不填则使用当前对话配置的模型temperature: number— 温度(0–2)maxTokens: number— 最大 token 数
注入系统提示词 injectPrompt
向下一次正常聊天请求追加一条系统提示词,发送完自动清除(一次性)。等同于 SillyTavern 的 injectPrompts()。
window.CharxHooks = {
onBeforeSend(message) {
// 根据当前地点注入额外场景描述,只影响本次请求
const scene = $charx.variable.get('scene')
if (scene === '地下城') {
$charx.chat.injectPrompt('【当前场景补充】四周漆黑,玩家视线极差,声音有回声。')
}
return message
}
}
插入系统消息 insertSystemMessage
向聊天记录插入一条 system 角色的消息,不触发 AI 生成。适合章节分隔线、剧情提示等场景。
window.CharxHooks = {
async onReady() {
const chapter = $charx.variable.get('chapter')
if (chapter === 2) {
// 插入章节标题,静默,不触发 AI
await $charx.chat.insertSystemMessage('=== 第二章:遗忘之海 ===')
}
}
}
$charx.variable
读写对话变量。变量随对话持久化,适合存储游戏状态、角色属性等。
// 读取(全局 JS 中同步,立即返回值)
const hp = $charx.variable.get('hp') // 不存在则返回 undefined
const all = $charx.variable.getAll() // 返回所有变量的对象副本
// 写入(异步,自动持久化到后端)
await $charx.variable.set('hp', 100)
await $charx.variable.setMany({ hp: 100, mp: 50, gold: 0 })
// 数值原子增减(返回新值)
const newHp = await $charx.variable.increment('hp', 10) // hp += 10
const newMp = await $charx.variable.decrement('mp', 5) // mp -= 5
// 删除
await $charx.variable.delete('tempFlag')
await $charx.variable.clear() // 清空所有变量(慎用)
// 订阅单个变量变化(仅全局 JS 中有效,iframe 内无法使用)
// 返回取消订阅函数,在 onDestroy 中调用以避免内存泄漏
const unwatch = $charx.variable.watch('hp', (newValue, oldValue) => {
console.log(`hp 变化:${oldValue} → ${newValue}`)
document.documentElement.style.setProperty('--charx-hp', String(newValue))
})
// 取消订阅
unwatch()
// 订阅所有变量变化(仅全局 JS 中有效)
// 任意变量发生变化时触发,参数为完整变量快照
const unwatchAll = $charx.variable.watchAll((vars) => {
console.log('变量快照:', vars)
})
unwatchAll()
注意:
watch()/watchAll()仅在全局 JS(window.$charx)中有效。消息内嵌 HTML(iframe)因跨 iframe 无法传递回调函数,这两个方法不可用;如需在 iframe 中响应变量变化,请改用$charx.chat.onMessage()订阅消息,在回调中主动读取最新值。
$charx.storage
读写跨对话持久化存储。以 user + character 为作用域,切换对话或刷新后数据依然存在,适合存储成就、解锁状态、全局配置等。
与对话变量的区别见 10.5 节
// 写入(异步)
await $charx.storage.set('unlocked_ending_1', true)
await $charx.storage.set('player_config', { bgm: true, fontSize: 14 })
// 读取(异步,不存在时返回 null)
const unlocked = await $charx.storage.get('unlocked_ending_1')
const config = await $charx.storage.get('player_config')
// 删除
await $charx.storage.remove('unlocked_ending_1')
// 列出所有 key
const keys = await $charx.storage.keys() // ['player_config', ...]
// 清空(慎用,不可恢复)
await $charx.storage.clear()
典型用法:
window.CharxHooks = {
async onReady() {
const config = await $charx.storage.get('settings') ?? { volume: 80 }
applyConfig(config)
const hasSecret = await $charx.storage.get('secret_ending')
if (hasSecret) console.log('[存档] 隐藏结局已解锁')
},
onMessageReceived(message) {
if (message.content.includes('隐藏结局触发')) {
$charx.storage.set('secret_ending', true)
}
}
}
$charx.ui
控制聊天界面行为。
// 滚动到消息列表底部
$charx.ui.scrollToBottom()
// 显示 / 隐藏输入框(适用于演出模式)
$charx.ui.hideChatInput()
$charx.ui.showChatInput()
// Toast 通知
$charx.ui.toast({ text: '获得道具:星尘碎片', type: 'success' })
// type 可选:'success' | 'error' | 'warning' | 'info'
// duration(可选):显示时长,毫秒,默认 3000
// 切换聊天页面主题
$charx.ui.setTheme('dark') // 切换为深色主题
$charx.ui.setTheme('light') // 切换为浅色主题
$charx.ui.setTheme('system') // 跟随宿主系统主题
// 打开 / 关闭扩展面板(适合挂载独立说明页、背包页、任务面板等)
await $charx.ui.openPanel({
url: 'https://example.com/panel',
title: '任务面板',
mode: 'sidebar', // 可选:'fullscreen' | 'sidebar' | 'modal'
width: 420,
})
await $charx.ui.closePanel()
// 弹出提示框(自定义对话框,非浏览器原生,CSP 下可正常工作)
await $charx.ui.alert('任务完成!') // 返回 Promise<void>,用户点击确认后 resolve
// 弹出确认/取消对话框,返回 Promise<boolean>
const confirmed = await $charx.ui.confirm('确定要放弃任务吗?')
if (confirmed) {
await $charx.variable.set('quest_abandoned', true)
}
// 弹出输入对话框,返回 Promise<string | null>(用户取消则返回 null)
const name = await $charx.ui.prompt('请输入你的角色名:', '勇者')
if (name !== null) {
await $charx.variable.set('player_name', name)
}
注意:
alert()、confirm()、prompt()均为平台自定义对话框,不调用浏览器原生window.alert等,在 CSP 严格限制下也能正常工作。
openPanel()/closePanel()当前在全局 JS 宿主和独立页面 SDK 中可用;消息内 HTML 的轻量$charxshim 不暴露这组接口。
$charx.media / $charx.game
这两组能力现在已经在正式聊天页宿主、创作者预览宿主和独立页面 SDK 中打通。
$charx.media.*:TTS 语音、BGM 背景音乐、音效播放$charx.game.*:轻量存档、成就、排行榜
注意:这里说的是全局 JS 宿主和独立页面 SDK。消息内 HTML 的轻量
$charxshim 目前仍不暴露media.*/game.*。
背景音乐如需“默认自动播 + 歌单配置”,仍建议优先使用编辑器内置的「音乐播放器」标签页(见 9.1 节);$charx.media更适合在运行时按剧情控制播放。
10.2 window.CharxHooks — 生命周期钩子
在全局 JS 中通过给 window.CharxHooks 赋值来注册钩子。所有钩子均为可选,只注册你需要的。
window.CharxHooks = {
// 对话页加载完成、$charx 就绪后触发(仅触发一次)
onReady(context) {
// context = { character: { id, name, avatar }, conversation: { id, messageCount }, user: { id, nickname } }
if ($charx.variable.get('hp') === undefined) {
$charx.variable.setMany({ hp: 100, mp: 50, gold: 0 })
}
},
// 用户点击发送前触发,可修改或拦截消息
onBeforeSend(message, context) {
// 返回修改后的字符串:修改内容
// 返回 null / false:拦截发送(用户无响应,慎用)
return message
},
// 用户消息发出后(AI 开始生成前)触发
onAfterSend(message, context) {
console.log('消息已发出:', message)
},
// AI 流式输出每个 chunk 时触发(流式过程中多次触发)
onStreamChunk(chunk, context) { },
// ✨ AI 回复写入数据库前触发,可拦截修改(支持 async,修改后同步影响 UI 和 DB)
// 返回新字符串 → 用新内容替换;返回 null / undefined → 保持原样
async onTransformMessage({ content }) {
// 示例:去除括号内旁白后再存库
// return content.replace(/([^)]*)/g, '').trim()
return null
},
// AI 消息完整接收后触发(onTransformMessage 之后触发,content 为最终写入 DB 的内容)
// ⚠️ 此钩子返回值被忽略,不能在这里修改消息内容(请用 onTransformMessage)
onMessageReceived(message) {
// message = { id: string, content: string }
const hp = $charx.variable.get('hp')
if (Number(hp) <= 0) {
setTimeout(() => $charx.chat.send('[系统:HP 归零,触发结局]'), 600)
}
},
// 对话页卸载时触发(清理定时器、移除 DOM 等)
onDestroy() {
clearInterval(myTimer)
},
// ✨ 用户切换到其他对话时触发
// payload = { conversationId: number }
onChatChanged(payload) {
console.log('切换到对话:', payload.conversationId)
// 适合在此处重置 UI 状态、重新读取变量
},
// ✨ AI 消息写入完成后触发(流式结束并存入数据库后)
// message = { id: number, content: string, role: 'assistant', createdAt: string }
onMessageUpdated(message) {
// 与 onMessageReceived 的区别:此时内容已确定入库,id 有效
console.log('消息已入库:', message.id)
},
// ✨ 用户手动编辑某条消息后触发
// message = { id: number, content: string, role: string, createdAt: string }
onMessageEdited(message) {
console.log('消息已编辑:', message.id, message.content)
}
}
钩子执行时序
用户点击发送
↓
onBeforeSend(message) → 可修改内容或返回 false 拦截
↓
[发送到服务端,开始流式接收]
↓
onAfterSend(message) → 发出后立即触发
↓
onStreamChunk(chunk) → 每个流式片段触发一次
↓
[流式结束,提取 <charx_vars> / <charx_choices>]
↓
onTransformMessage({ content }) → 可修改最终内容(async)
↓
[修改后的内容写入 DB + 更新 UI]
↓
onMessageReceived({ id, content }) → 读取最新变量、更新状态面板
↓
onMessageUpdated({ id, content, ... }) → 消息已确定入库,可做最终状态计算
用户切换对话
↓
onChatChanged({ conversationId }) → 可在此重置 UI 状态
用户编辑消息
↓
onMessageEdited({ id, content, ... }) → 消息内容已更新入库
注意事项:
- 所有钩子内的错误会被平台静默捕获,不影响对话运行——调试时请善用
console.log onTransformMessage是唯一能影响最终存储内容的钩子,其他钩子的返回值均被忽略(除onBeforeSend)onMessageReceived与onMessageUpdated都在流式结束后触发,区别在于语义:前者强调"收到回复",后者强调"消息已入库";二者在同一次生成中都会触发
10.2.1 实用场景示例
以下示例综合使用 $charx 新 API,展示典型创作者用法。
示例 1:根据变量实时更新 CSS 变量(watch)
window.CharxHooks = {
async onReady() {
// 订阅 hp 变量,实时将值同步到 CSS 自定义属性
const unwatch = window.$charx.variable.watch('hp', (hp) => {
document.documentElement.style.setProperty('--charx-hp', String(hp))
})
// 保存取消函数,onDestroy 时清理
window.CharxHooks.onDestroy = unwatch
}
}
示例 2:添加"停止生成"按钮
window.CharxHooks = {
onReady() {
const btn = document.createElement('button')
btn.textContent = '停止生成'
btn.style.cssText = 'position:fixed;bottom:80px;right:20px;z-index:9999;padding:8px 16px'
btn.onclick = () => window.$charx.chat.stop()
document.body.appendChild(btn)
}
}
示例 3:发送前弹出确认框
window.CharxHooks = {
async onBeforeSend(message) {
if (message.includes('攻击')) {
const ok = await window.$charx.ui.confirm('确定要攻击吗?此操作无法撤销。')
return ok // 返回 false 则拦截发送
}
return true
}
}
示例 4:根据 persona 个性化开场
window.CharxHooks = {
onReady() {
const persona = window.$charx.character.getPersona()
if (persona) {
window.$charx.ui.toast({ text: `欢迎,${persona.callName}!`, type: 'success' })
}
}
}
示例 5:用输入框让用户设置角色名
window.CharxHooks = {
async onReady() {
const existing = window.$charx.variable.get('player_name')
if (!existing) {
const name = await window.$charx.ui.prompt('请输入你的角色名:', '勇者')
if (name) {
await window.$charx.variable.set('player_name', name)
window.$charx.ui.toast({ text: `角色名已设置为:${name}`, type: 'success' })
}
}
}
}
示例 6:用 generateRaw 做后台状态裁判
window.CharxHooks = {
async onMessageReceived(message) {
// AI 回复后,后台静默判断是否触发特殊事件
const hp = Number($charx.variable.get('hp') ?? 100)
if (hp <= 0) return // 已死亡,不再判断
const verdict = await $charx.chat.generateRaw([
{
role: 'system',
content: '你是事件裁判。只能回答"触发"或"不触发",不要解释。'
},
{
role: 'user',
content: `玩家 HP=${hp},最近一条 AI 回复内容:「${message.content.slice(0, 200)}」。是否触发濒死事件?`
}
], { includeCharacterContext: false })
if (verdict.includes('触发')) {
await $charx.chat.insertSystemMessage('【系统】你陷入了濒死状态……')
$charx.ui.toast({ text: '⚠️ 濒死!请立即恢复 HP', type: 'warning' })
}
}
}
示例 7:用 injectPrompt 按场景注入补充指令
window.CharxHooks = {
onBeforeSend(message) {
const scene = $charx.variable.get('scene')
const injections = {
'地下城': '【场景提示】环境黑暗,玩家视野受限,声音有回声,请在描写中体现这些细节。',
'皇宫': '【场景提示】环境庄严,NPC 措辞正式,请保持皇宫礼节氛围。',
}
const extra = injections[scene]
if (extra) {
$charx.chat.injectPrompt(extra) // 只注入本次,下次不影响
}
return message
}
}
示例 8:切换对话时重置 UI 状态
window.CharxHooks = {
onReady() {
// 初始化 UI
renderStatusPanel($charx.variable.getAll())
},
onChatChanged({ conversationId }) {
// 切换到其他对话时,重新读取变量并更新面板
const vars = $charx.variable.getAll()
renderStatusPanel(vars)
$charx.ui.toast({ text: `已切换到对话 #${conversationId}`, type: 'info' })
}
}
function renderStatusPanel(vars) {
document.getElementById('hp-display').textContent = vars.hp ?? 100
document.getElementById('gold-display').textContent = vars.gold ?? 0
}
10.3 AI 输出特殊标签
可以在系统提示词或后置指令中要求 AI 在回复中输出特殊标签,平台会自动解析并处理,不会显示给用户。
<charx_vars> — 自动更新变量
# 在 System Prompt 或 Post History Instructions 中写:
每次回复末尾必须附带以下 JSON 块,更新角色当前状态,不要省略:
<charx_vars>{"hp": 当前HP数值, "mp": 当前MP数值, "location": "当前地点"}</charx_vars>
AI 输出示例:
*Aria 皱眉,抵挡住了攻击,但仍受了轻伤。*
"这点小伤算什么。"
<charx_vars>{"hp": 75, "location": "星港走廊"}</charx_vars>
平台会自动:
- 解析 JSON 并调用
$charx.variable.setMany({...}) - 将
<charx_vars>...</charx_vars>块从显示内容中移除
<charx_choices> — 提供快捷选项
# 在 System Prompt 中写:
每次回复末尾必须附带选项供玩家选择:
<charx_choices>["选项一的文字", "选项二的文字", "选项三的文字"]</charx_choices>
AI 输出示例:
*她看向你,等待你的回答。*
<charx_choices>["我愿意帮你", "我需要报酬", "先告诉我更多细节"]</charx_choices>
平台会自动:
- 在输入框上方显示快捷选项按钮,用户点击即可发送
- 将
<charx_choices>...</charx_choices>从显示内容移除
10.4 消息内嵌 HTML 中的 $charx
如果 AI 回复包含 HTML 片段(被 ```html 代码块包裹,或直接包含 <div> 等块级标签),平台会在独立 iframe 中渲染该 HTML,并自动注入 window.$charx shim。
与全局 JS 的关键区别:
- iframe 内所有
$charx方法都按异步接口使用,调用时应默认await。 - iframe 内可直接使用
$charx.worldbook.*,并在同层卡模式下订阅$charx.chat.onMessage()/onStreamChunk()。 - 开场白切换接口只允许在当前对话还没有用户消息时调用。
角色信息
| 方法 | 说明 |
|---|---|
$charx.character.getInfo() |
获取角色公开信息(异步 Promise) |
$charx.character.getPersona() |
获取用户 persona(异步 Promise;返回 { callName, gender, description } 或 null) |
$charx.character.getSettings() |
获取当前对话 AI 配置(异步 Promise;返回 { model: string | null }) |
变量与存储
| 方法 | 说明 |
|---|---|
$charx.variable.get(key) |
读取单个变量(异步 Promise) |
$charx.variable.getAll() |
读取全部变量(异步 Promise) |
$charx.variable.set(key, val) |
写入单个变量(异步) |
$charx.variable.setMany(vars) |
批量写入变量(异步) |
$charx.variable.increment(key, delta) |
数值递增,返回新值(异步) |
$charx.variable.decrement(key, delta) |
数值递减,返回新值(异步) |
$charx.variable.delete(key) |
删除单个变量(异步) |
$charx.variable.clear() |
清空全部变量(异步,慎用) |
$charx.storage.get(key) |
读取持久化存储(异步) |
$charx.storage.set(key, val) |
写入持久化存储(异步) |
$charx.storage.remove(key) |
删除持久化存储中的 key(异步) |
$charx.storage.clear() |
清空持久化存储(异步,慎用) |
$charx.storage.keys() |
列出全部持久化存储 key(异步) |
聊天控制
| 方法 | 说明 |
|---|---|
$charx.chat.send(message) |
代发消息,触发 AI 回复 |
$charx.chat.setInput(text) |
仅填充输入框,不立即发送 |
$charx.chat.getInput() |
读取当前输入框内容(异步 Promise) |
$charx.chat.stop() |
中止当前 AI 流式输出 |
$charx.chat.regenerate() |
重新生成最后一条 AI 消息 |
$charx.chat.continue() |
续写当前回复 |
$charx.chat.getHistory(opts?) |
获取历史消息数组(异步 Promise;opts 为 { limit?, offset? }) |
$charx.chat.setGreetingIndex(idx) |
切换当前对话的开场白索引。仅允许在首条用户消息前调用 |
$charx.chat.getGreetingIndex() |
读取当前对话的开场白索引 |
$charx.chat.setFirstMesIndex(idx) |
旧名称兼容别名,行为等同于 setGreetingIndex(idx) |
$charx.chat.onMessage(callback) |
订阅新消息。仅同层卡模式可用,见第 12 章 |
$charx.chat.onStreamChunk(callback) |
订阅流式 chunk。仅同层卡模式可用,见第 12 章 |
UI 与世界书
| 方法 | 说明 |
|---|---|
$charx.ui.toast(options) |
显示 Toast 通知 |
$charx.ui.scrollToBottom() |
滚动到底部 |
$charx.ui.setTheme('dark' | 'light') |
切换主题 |
$charx.ui.alert(message) |
弹出提示框(异步,用户确认后 resolve) |
$charx.ui.confirm(message) |
弹出确认框(异步,返回 boolean) |
$charx.ui.prompt(message, defaultValue?) |
弹出输入框(异步,返回字符串或 null) |
$charx.worldbook.getAll() |
获取当前对话所有记忆书条目及状态(异步) |
$charx.worldbook.enable(entryKey) |
启用指定条目(异步) |
$charx.worldbook.disable(entryKey) |
禁用指定条目(异步) |
$charx.worldbook.batchToggle(entries) |
批量设置多个条目(异步) |
$charx.worldbook.setContent(entryKey, content) |
覆盖指定条目的内容(异步) |
$charx.worldbook.clearContent(entryKey) |
清除指定条目的内容覆盖(异步) |
$charx.worldbook.reset() |
重置全部覆盖(异步) |
注意:
$charx.variable.watch()/watchAll()在 iframe 内不可用(跨 iframe 无法传递回调),这两个方法仅限全局 JS 使用。
示例:在 HTML 卡片中显示角色状态
<!DOCTYPE html>
<html>
<body style="background:#1a1a2e;color:#e0f0ff;font-family:sans-serif;padding:12px;margin:0">
<div id="status"></div>
<script>
// iframe 内所有 $charx 方法均为 async,必须 await
(async () => {
const hp = (await $charx.variable.get('hp')) ?? 100
const mp = (await $charx.variable.get('mp')) ?? 50
document.getElementById('status').innerHTML = `
<div>❤️ HP: ${hp} / 100</div>
<div>💙 MP: ${mp} / 50</div>
`
})()
</script>
</body>
</html>
10.5 变量与存储的区别
平台提供两套数据持久化机制,用途不同:
对话变量($charx.variable) |
持久化存储($charx.storage) |
|
|---|---|---|
| 作用域 | 单个对话会话 | 跨对话、跨会话 |
| 典型用途 | HP/MP、当前剧情状态、临时标记 | 成就、全局解锁、跨剧情存档 |
| AI 可写入 | 可(通过 <charx_vars> 标签) |
不可(仅 JS 脚本可写) |
| 对话重置后 | 可配置保留或清空 | 始终保留 |
10.6 $charx.worldbook — 程序化控制记忆书
可在开场白 HTML(同层卡)或 AI 返回的消息 HTML 中使用,动态控制当前对话的记忆书状态。
所有操作仅对当前对话会话生效,不影响角色卡模板本身,也不影响其他用户的对话。
API 参考
// 获取所有条目及当前状态
const wb = await $charx.worldbook.getAll()
// wb.entries 是一个数组,每个条目格式:
// {
// entryKey: string, // 条目唯一 ID(字符串)
// comment: string, // 条目名称/注释
// enabled: boolean, // 当前是否启用(含覆盖后的状态)
// baseEnabled: boolean, // 角色卡模板默认状态
// isOverridden: boolean, // 是否被本对话覆盖过
// constant: boolean, // 是否为「常驻注入」类型
// keys: string[], // 触发关键词
// insertionOrder: number // 注入优先级
// }
// 启用 / 禁用单个条目
await $charx.worldbook.enable('1234567890')
await $charx.worldbook.disable('1234567890')
// 批量切换
await $charx.worldbook.batchToggle([
{ entryKey: '1234567890', enabled: true },
{ entryKey: '9876543210', enabled: false },
])
// 覆盖条目内容(在当前对话中使用自定义内容,不修改原始条目)
await $charx.worldbook.setContent('1234567890', '新的条目内容,可用于动态剧情')
// 清除内容覆盖,恢复该条目的原始内容
await $charx.worldbook.clearContent('1234567890')
// 重置所有覆盖(恢复所有条目到角色卡默认状态)
await $charx.worldbook.reset()
典型用途
1. 剧情分支解锁新世界观条目
在系统提示词或后置指令中要求 AI 输出分支标记,然后在全局 JS 中根据 AI 回复激活对应条目:
window.CharxHooks = {
async onMessageReceived(message) {
// AI 回复中包含特殊标记,激活对应世界观条目
if (message.content.includes('[UNLOCK:天剑宗秘史]')) {
// 先查找该条目的 entryKey
const wb = await $charx.worldbook.getAll()
const entry = wb.entries.find(e => e.comment === '天剑宗秘史')
if (entry) {
await $charx.worldbook.enable(entry.entryKey)
console.log('[世界书] 已解锁:天剑宗秘史')
}
}
}
}
2. 同层卡根据游戏状态动态更新 NPC 信息
// 在同层卡中,随着玩家选择,动态修改 NPC 的世界书条目内容
async function updateNpcStatus(npcEntryKey, newStatus) {
const npcContent = `【NPC当前状态】${newStatus}`
await $charx.worldbook.setContent(npcEntryKey, npcContent)
}
// 玩家选择"与 Aria 结盟"后
async function onAllianceChosen() {
const wb = await $charx.worldbook.getAll()
const ariaEntry = wb.entries.find(e => e.comment === 'Aria-关系状态')
if (ariaEntry) {
await $charx.worldbook.setContent(ariaEntry.entryKey,
'Aria 目前是玩家的盟友,对玩家态度友善,会主动提供帮助和情报。')
}
}
3. 开场白切换时根据场景激活不同条目
创作者可以设计多个开场白(备用开场白),每个对应不同的场景,在对话创建时通过 API 传入 greetingIndex 参数,并在开场白 HTML 中激活对应场景的世界书条目:
// 在开场白 HTML 中检测当前场景并激活对应条目
(async function() {
const wb = await $charx.worldbook.getAll()
// 批量设置:仅激活当前场景需要的条目
await $charx.worldbook.batchToggle(
wb.entries.map(entry => ({
entryKey: entry.entryKey,
enabled: entry.comment.startsWith('场景A_') // 激活所有"场景A"开头的条目
}))
)
})()
注意事项
entryKey是条目的数字 ID(字符串形式),通过getAll()获取。可在记忆书编辑器的条目详情中查看- 内容覆盖(
setContent)不会影响其他用户,刷新页面后覆盖数据依然存在(存储在服务端) - 调用
reset()会清除当前对话所有的启用/禁用覆盖和内容覆盖,谨慎使用
10.7 $charx.game — 游戏存档、成就与排行榜
适用于有进度保存需求的 RPG / 策略类角色卡。所有操作均以 user + character 为作用域,不同用户或不同角色卡之间数据完全隔离。
存档系统
// 保存存档。slot 为数字槽位,snapshot 由宿主自动从当前运行时状态抓取
await $charx.game.save({
slot: 1,
description: '第二章开始前',
})
// 读取存档槽位快照(不存在时返回空对象)
const saveData = await $charx.game.load({ slot: 1 })
console.log('读取存档:', saveData)
// 列出所有存档位
const saves = await $charx.game.listSaves()
// 返回:[{ slot: number, description?: string, savedAt: number, snapshot?: Record<string, unknown> }, ...]
// 删除存档
await $charx.game.deleteSave(1)
成就系统
// 解锁成就(achievementId 在角色卡后台配置)
await $charx.game.achievements.unlock('first_win')
// 列出已记录成就
const achievements = await $charx.game.achievements.list()
// 返回:[{ id: string, name: string, description: string, unlockedAt?: number }, ...]
排行榜系统
// 提交分数(category 可选,默认 'default')
await $charx.game.ranking.submit(9800, { category: 'main_board' })
// 获取排行榜前 N 名
const top = await $charx.game.ranking.getTop({ category: 'main_board', limit: 10 })
// 返回:[{ rank: number, userId: string, username: string, score: number }, ...]
// 查询当前用户在排行榜中的名次
const myRank = await $charx.game.ranking.getSelf({ category: 'main_board' })
// 返回:{ rank: number, userId: string, username: string, score: number }