详细手册

Ai玩家详细教程:JS 与平台 API

从完整创作者说明中拆出的主题页。需要其它主题可返回详细目录。

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
  }
}

安全限制

调试

编辑 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 触发前即可使用。

作用域说明

  • 全局 JSwindow.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() 只返回公开资料,不返回 descriptionpersonalityscenario、系统提示词、记忆书内容、正则规则等内部运行时设定。需要影响 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() 仅在全局 JSwindow.$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 的轻量 $charx shim 不暴露这组接口。


$charx.media / $charx.game

这两组能力现在已经在正式聊天页宿主、创作者预览宿主和独立页面 SDK 中打通。

注意:这里说的是全局 JS 宿主和独立页面 SDK。消息内 HTML 的轻量 $charx shim 目前仍不暴露 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, ... })  → 消息内容已更新入库

注意事项:


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>

平台会自动:

  1. 解析 JSON 并调用 $charx.variable.setMany({...})
  2. <charx_vars>...</charx_vars> 块从显示内容中移除

<charx_choices> — 提供快捷选项

# 在 System Prompt 中写:

每次回复末尾必须附带选项供玩家选择:
<charx_choices>["选项一的文字", "选项二的文字", "选项三的文字"]</charx_choices>

AI 输出示例:

*她看向你,等待你的回答。*

<charx_choices>["我愿意帮你", "我需要报酬", "先告诉我更多细节"]</charx_choices>

平台会自动:

  1. 在输入框上方显示快捷选项按钮,用户点击即可发送
  2. <charx_choices>...</charx_choices> 从显示内容移除

10.4 消息内嵌 HTML 中的 $charx

如果 AI 回复包含 HTML 片段(被 ```html 代码块包裹,或直接包含 <div> 等块级标签),平台会在独立 iframe 中渲染该 HTML,并自动注入 window.$charx shim。

与全局 JS 的关键区别:

  1. iframe 内所有 $charx 方法都按异步接口使用,调用时应默认 await
  2. iframe 内可直接使用 $charx.worldbook.*,并在同层卡模式下订阅 $charx.chat.onMessage() / onStreamChunk()
  3. 开场白切换接口只允许在当前对话还没有用户消息时调用。

角色信息

方法 说明
$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"开头的条目
    }))
  )
})()

注意事项


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 }