SDK 教程

@ai_wanjia/sdk
创作者教程

判断 HTML 消息和独立页面 + SDK 的使用边界,快速跑通第一个可交互界面,并理解 variable、storage、worldbook、chat 的职责。

@ai_wanjia/sdk 创作者教程

这份文档的目标很直接:

  1. 帮你判断该用哪种接入方式。
  2. 帮你最快跑通第一个可交互界面。
  3. 帮你分清 variablestorageworldbookchat 各自负责什么。

如果你只是想尽快起步,先看“先选方案”和“最快起步路径”这两节。


先选方案

平台里和 SDK 相关的自定义界面,实际上分成两条路:

方式 适合什么 是否需要 npm 推荐人群
HTML 消息 AI 回复里插入状态卡、按钮卡、小型面板 不需要 想快速做轻量效果的创作者
独立页面 + SDK 做完整游戏界面、背包页、地图页、React 页面 需要 想做独立页面或完整玩法的创作者

最简单的判断方法:


先理解边界

SDK 负责的是“界面层”和“交互层”,不是“模型层”。

SDK 能做的事情:

SDK 不做的事情:

这条边界非常重要。

独立页面是“平台运行时的前端外壳”,不是“自己单独实现一套聊天后端”。


最快起步路径

如果你要做独立页面,不要从空目录开始。直接从官方 starter 改。

模板 目录 适合什么
charx-h5-starter sdk/examples/charx-h5-starter 原生 html + css + js
charx-react-starter sdk/examples/charx-react-starter Vite + React + TypeScript

H5 模板本地启动

npx http-server ./sdk/examples/charx-h5-starter -p 5173 -c-1

React 模板本地启动

cd /Users/lee/GolandProjects/createChar/sdk/examples/charx-react-starter
npm install
npm run dev

这两套模板都已经内置:

推荐顺序:

  1. 先让模板本地跑起来。
  2. 先把首页文案和样式换成你的主题。
  3. 再把变量名、动作按钮、聊天逻辑改成你的玩法。
  4. 最后再上传到角色卡的独立界面资源包里联调。

方式一:HTML 消息

它适合什么

适合做这些东西:

不适合做这些东西:

它怎么接入

当 AI 回复里包含 HTML 代码块时,平台会把这段 HTML 放进沙盒 iframe,并自动注入 window.$charx

这意味着:

最小示例

```html
<div style="padding: 12px; border-radius: 12px; background: #111827; color: #fff;">
  <div style="font-size: 12px; opacity: 0.72;">当前 HP</div>
  <div id="hp-value" style="font-size: 24px; font-weight: 700;">--</div>
</div>
<script>
(async () => {
  const hp = await window.$charx.variable.get('hp')
  document.getElementById('hp-value').textContent = String(hp ?? 100)
})()
</script>

### `$charx` 当前推荐用法
#### `$charx.character`
HTML 消息里当前主要提供角色基础信息读取。

```javascript
const info = await window.$charx.character.getInfo()
console.log(info.name)

$charx.chat

window.$charx.chat.send('继续推进')

window.$charx.chat.setInput('先把这段话填到输入框')
const input = await window.$charx.chat.getInput()

await window.$charx.chat.setGreetingIndex(1)
const greetingIndex = await window.$charx.chat.getGreetingIndex()

const history = await window.$charx.chat.getHistory({ limit: 20, offset: 0 })

window.$charx.chat.onMessage((message) => {
  console.log('新消息:', message.content)
})

window.$charx.chat.onStreamChunk((payload) => {
  if (!payload.done) {
    console.log('流式片段:', payload.chunk)
  }
})

$charx.variable

const hp = await window.$charx.variable.get('hp')
const vars = await window.$charx.variable.getAll()

await window.$charx.variable.set('hp', 80)
await window.$charx.variable.setMany({ hp: 80, mp: 30 })

await window.$charx.variable.increment('affection', 5)
await window.$charx.variable.decrement('hp', 10)

await window.$charx.variable.delete('tempFlag')
await window.$charx.variable.clear()

$charx.storage

await window.$charx.storage.set('preferredTheme', 'deep-sea')

const theme = await window.$charx.storage.get('preferredTheme')
const keys = await window.$charx.storage.keys()

await window.$charx.storage.remove('preferredTheme')

$charx.ui

HTML 消息场景里,当前最常用的是 toastscrollToBottom

window.$charx.ui.toast({ text: '获得道具', type: 'success' })
window.$charx.ui.scrollToBottom()

$charx.worldbook

这套能力只影响当前对话,不会改角色卡模板。

const wb = await window.$charx.worldbook.getAll()

await window.$charx.worldbook.enable('combat-mode')
await window.$charx.worldbook.disable('peace-mode')

await window.$charx.worldbook.batchToggle([
  { entryKey: 'combat-mode', enabled: true },
  { entryKey: 'peace-mode', enabled: false },
])

await window.$charx.worldbook.setContent('combat-mode', '新的世界书内容')
await window.$charx.worldbook.clearContent('combat-mode')
await window.$charx.worldbook.reset()

HTML 消息的推荐做法


方式二:独立页面 + npm SDK

它适合什么

适合做这些东西:

最小原生示例

import { CharxSdk } from '@ai_wanjia/sdk'

const sdk = CharxSdk.getInstance()
await sdk.init()

const info = await sdk.character.getInfo()
console.log('角色:', info.name)

const hp = await sdk.variable.get<number>('hp')
console.log('HP:', hp)

sdk.chat.send('总结我当前的状态', {
  onData: (chunk) => console.log(chunk),
  onComplete: (message) => console.log(message.content),
  onError: (error) => console.error(error),
})

最小 React 示例

import { CharxProvider, useChat, useVariable, useCharacter } from '@ai_wanjia/sdk/react'

export default function App() {
  return (
    <CharxProvider fallback={<div>连接平台中...</div>}>
      <GamePage />
    </CharxProvider>
  )
}

function GamePage() {
  const character = useCharacter()
  const { messages, send, status, error, stop } = useChat()
  const [hp, setHp] = useVariable<number>('hp', 100)

  return (
    <div>
      <h1>{character?.name}</h1>
      <p>HP: {hp}</p>
      <button onClick={() => setHp(hp + 5)}>恢复 5 点 HP</button>
      {status === 'streaming' ? (
        <button onClick={stop}>停止</button>
      ) : (
        <button onClick={() => send('继续推进剧情')}>继续</button>
      )}

      {error ? <p>{error.message}</p> : null}

      {messages.map((message) => (
        <div key={message.id}>{message.content}</div>
      ))}
    </div>
  )
}

你真正要关注的模块

sdk.character

用来拿角色基础信息、人设、当前对话配置。

const info = await sdk.character.getInfo()
const persona = await sdk.character.getPersona()
const settings = await sdk.character.getSettings()

sdk.chat

用来发送消息、接收流式回复、停止生成、切开场白、读取历史消息,以及进行后台静默生成。

sdk.chat.send('继续', {
  onData: (chunk) => console.log(chunk),
  onComplete: (message) => console.log(message.content),
})

const task = sdk.chat.sendStream('继续推进剧情', {
  timeoutMs: 60_000,
  onStart: ({ requestId, assistantMessageId }) => {
    console.log('开始生成:', requestId, assistantMessageId)
  },
  onData: (chunk) => console.log(chunk),
  onComplete: (message) => console.log(message.content),
  onError: (error) => console.error(error),
})

await task.done
// task.stop() 可以停止这一次生成

await sdk.chat.stop()
await sdk.chat.regenerate()
await sdk.chat.continue()

sdk.chat.setInput('填入输入框')
const input = await sdk.chat.getInput()

await sdk.chat.setGreetingIndex(1)
const greetingIndex = await sdk.chat.getGreetingIndex()

const history = await sdk.chat.messages.getAll()

// 获取当前对话 ID
const conversationId = await sdk.chat.getConversationId()

chat.send() 是兼容旧用法的便捷方法。需要拿到本次请求 ID、等待完成结果或只停止这一条生成时,优先用 chat.sendStream()

timeoutMs 表示流式事件空闲超时。默认是 60 秒。只要平台持续返回流式片段,即使整条回复需要 2 分钟,也不会因为超过 60 秒被 SDK 中断。

默认不要传 model。不传时,平台会使用当前会话配置的模型。只有平台明确提供可选模型并且你的功能确实需要覆盖时,才传 model

后台静默生成 generateRaw

等同于 SillyTavern 的 generateRaw()。不写入聊天记录,通过平台 AI 链路返回结果。适合后台计算、状态判断、不可见旁白等场景。

// 最简用法:自行组装消息列表
const result = await sdk.chat.generateRaw([
  { role: 'system', content: '你是一个战斗裁判,只需返回胜负结果,不要废话。' },
  { role: 'user',   content: '玩家攻击力 80,敌人防御力 60,是否命中?' },
])
console.log(result) // e.g. "命中,造成 20 点伤害。"

// 携带角色上下文(默认 true,包含角色设定和世界书)
const withContext = await sdk.chat.generateRaw(
  [{ role: 'user', content: '总结当前剧情状态,不超过 50 字。' }],
  { includeCharacterContext: true }
)

// 调整生成参数。通常不要指定 model,让平台使用当前会话模型。
const custom = await sdk.chat.generateRaw(
  [{ role: 'user', content: '生成一段不超过 80 字的线索描述。' }],
  { temperature: 0.9, maxTokens: 200, timeoutMs: 60_000 }
)

注意generateRaw 不写入聊天记录,也不触发任何 CharxHooks。适合“背后计算”,不适合替代正常聊天流程。创作者一般不需要指定 model,否则可能因为平台模型名变化导致功能失效。

注入系统提示词 injectPrompt

等同于 SillyTavern 的 injectPrompts()。向下一次正常聊天请求注入额外系统提示,注入后自动清除(一次性)。

// 在下一条 AI 回复前注入额外指令
sdk.chat.injectPrompt('【本轮特殊规则】玩家处于隐身状态,NPC 不应察觉玩家的存在。')

// 之后正常发送消息,注入内容会自动附加到本次请求的系统消息中
sdk.chat.send('我悄悄绕到守卫背后')
// 发送完成后,注入内容自动清除,不影响后续对话
插入系统消息 insertSystemMessage

向对话历史插入一条 role: system 的消息,不触发 AI 生成。适合在对话中间埋入旁白、场景提示或分隔线。

const msg = await sdk.chat.insertSystemMessage('=== 第二章:遗忘之海 ===')
console.log(msg.id, msg.content)

// 结合剧情进度
const progress = await sdk.variable.get<number>('chapter')
if (progress === 2) {
  await sdk.chat.insertSystemMessage('【系统提示】你已进入第二章,之前的选择将影响后续剧情。')
}
事件订阅
// 订阅对话切换(用户切换到其他对话时触发)
const unsub1 = sdk.chat.onChatChanged(({ conversationId }) => {
  console.log('切换到对话:', conversationId)
  // 可在此处重新初始化 UI 状态
})

// 订阅 AI 消息写入完成(流式结束并存入数据库后触发)
const unsub2 = sdk.chat.onMessageUpdated((message) => {
  console.log('新 AI 消息:', message.id, message.content)
  // 可在此处触发成就判断、状态面板刷新等
})

// 订阅消息被编辑(用户手动编辑某条消息后触发)
const unsub3 = sdk.chat.onMessageEdited((message) => {
  console.log('消息已编辑:', message.id)
})

// 取消订阅(页面卸载时调用)
unsub1()
unsub2()
unsub3()

sdk.variable

用来管理当前对话状态。绝大多数游戏状态都应该先考虑放这里。

const hp = await sdk.variable.get<number>('hp')
const vars = await sdk.variable.getAll()

await sdk.variable.set('hp', 88)
await sdk.variable.setMany({ hp: 88, scene: '档案室' })

await sdk.variable.increment('affection', 2)

const unwatch = sdk.variable.watch<number>('hp', (value) => {
  console.log('新的 HP:', value)
})

sdk.storage

用来保存长期数据。它不进入 AI 上下文,适合用户偏好和长期进度。
正式聊天宿主会写入平台后端;创作者编辑器和测试页只提供本地预览态。

await sdk.storage.set('preferredTheme', 'mist')

const theme = await sdk.storage.get<string>('preferredTheme')
const bgm = await sdk.storage.getOrDefault('bgmEnabled', true)

sdk.worldbook

用来临时切换当前对话里生效的世界书条目,也可以读取条目内容。

await sdk.worldbook.enable('chapter-3-secret')
await sdk.worldbook.disable('chapter-1-normal')

// 读取所有世界书条目(含内容和触发词)
const entries = await sdk.worldbook.getAll()
// 每条结构:
// {
//   entryKey: string      // 条目唯一标识
//   comment: string       // 备注名
//   content: string       // 实际注入给 AI 的文本内容
//   keys: string[]        // 触发关键词列表
//   enabled: boolean      // 当前对话是否启用(可被程序覆盖)
//   baseEnabled: boolean  // 角色卡模板默认是否启用
//   isOverridden: boolean // 是否被程序覆盖过
//   constant: boolean     // 是否为常驻条目
//   insertionOrder: number
// }

// 读取某条目的内容用于展示
const lore = entries.find(e => e.entryKey === 'chapter-3-secret')
if (lore) {
  console.log('条目内容:', lore.content)
  console.log('触发关键词:', lore.keys)
}

sdk.ui

用来弹 Toast、滚动到底部、弹对话框、切主题。

sdk.ui.toast({ text: '获得线索', type: 'success' })
sdk.ui.scrollToBottom()

await sdk.ui.alert('任务失败')
const confirmed = await sdk.ui.confirm('是否继续?')
const name = await sdk.ui.prompt('请输入角色名', '默认值')

最容易混淆的三个概念

variable

这是“当前对话状态”。

适合放:

storage

这是“长期数据”。

适合放:

worldbook

这是“当前对话可动态切换的知识注入层”。

适合做:

最简单的记法:

能力 你可以把它理解成
variable 当前局内状态
storage 长期存档
worldbook 当前局里给 AI 的知识开关

React 项目怎么写更顺

如果你用 React,推荐这样分层:

建议职责
CharxProvider 负责 SDK 初始化
页面组件 组织布局和交互流程
useVariable() / useVariables() 读取和订阅对话状态
useStorage() 读写长期设置
useChat() 处理消息发送、流式任务、错误状态和回复展示

常见组合示例

状态 HUD

const [hp] = useVariable<number>('hp', 100)
const [mp] = useVariable<number>('mp', 50)
const [affection] = useVariable<number>('affection', 0)

对话回放区

const { messages, status } = useChat()

需要等待本次 AI 完成

const { sendStream } = useChat()

async function continueStory() {
  const task = sendStream('继续推进剧情')
  const message = await task.done
  console.log('AI 完整回复:', message.content)
}

长期设置区

const [theme, setTheme] = useStorage('preferredTheme', 'deep-sea')

推荐的实际开发顺序

很多创作者一上来就想把完整玩法全部做完,结果调试成本非常高。更稳的顺序是:

  1. 先跑官方 starter。
  2. 先做一个首页。
  3. 先接通 charactervariablechat
  4. 再补 storage
  5. 最后再做 worldbook、音频、成就、排行榜这类附加能力。

如果你是做独立页面玩法,推荐先只做这四个能力:

这四个先通了,后面再扩就容易很多。


当前能力对照表

这张表用来回答一个实际问题:同样是创作者写界面,HTML 消息和独立页面到底差在哪。

能力 HTML 消息 $charx 独立页面 npm SDK 说明
character.getInfo() 基础角色信息读取
character.getPersona()
character.getSettings()
chat.send()
chat.sendStream() npm SDK 用于拿到本次请求 ID、完成 Promise 和单次停止能力
chat.stop()
chat.regenerate()
chat.continue()
chat.setInput()/getInput()
chat.setGreetingIndex()/getGreetingIndex()
chat.getHistory() HTML 用 getHistory(),npm 用 messages.list/getAll()
流式回调 HTML 用 onStreamChunk(),npm 用 send()/sendStream() 回调或 React Hooks
chat.generateRaw() 后台静默生成,等同于 SillyTavern generateRaw()
chat.injectPrompt() 向下次请求注入系统提示词(一次性)
chat.insertSystemMessage() 插入系统消息,不触发 AI 生成
chat.getConversationId()
chat.onChatChanged() 订阅对话切换事件
chat.onMessageUpdated() 订阅 AI 消息写入完成事件
chat.onMessageEdited() 订阅消息被编辑事件
chat.messages.edit()
chat.messages.delete() web 正式聊天页与 creator 预览宿主已补齐
chat.messages.swipe() 调用链路已补齐;当前宿主在无多候选消息时会显式报错
variable.get/set/getAll/setMany
variable.watch/watchAll $charx 通过轮询实现,npm SDK 通过实时推送实现
storage.*
storage.getOrDefault()
worldbook.*
ui.toast()
ui.scrollToBottom()
ui.hideChatInput()/showChatInput() HTML shim 与 npm SDK 已统一支持
ui.alert/confirm/prompt
ui.setTheme()
ui.openPanel/closePanel 当前只在独立页面 npm SDK 和全局 JS 宿主开放,HTML 消息 shim 不暴露
media.* 当前只在独立页面 npm SDK 和全局 JS 宿主开放,HTML 消息 shim 不暴露
game.* 正式聊天宿主走平台后端;创作者编辑器和测试页使用本地预览态,HTML 消息 shim 不暴露
React Hooks / Provider 只存在于 npm SDK

这里最关键的一句结论是:

HTML 消息适合补充 UI,独立页面适合承载主 UI。


常见误区

误区一:把 storage 当成 variable

后果:

如果数据需要影响当前对话流程,优先放 variable

误区二:把 HTML 消息硬做成大应用

后果:

如果你已经开始做多面板、多区域、多流程,应该切到独立页面。

误区三:页面自己维护一套“假聊天”

正确做法:

误区四:没有统一变量命名

建议从一开始就定好命名:

类型 推荐风格 示例
数值变量 简短小写 hp mp affection
状态变量 语义化英文 scene phase battleState
集合数据 明确含义 inventory quests

常见问题

window.$charx@ai_wanjia/sdk 有什么区别?

window.$charx 是平台自动注入给 HTML 消息 iframe 的轻量接口。

@ai_wanjia/sdk 是完整 npm 包,适合独立页面、React 页面、工程化开发。

为什么我本地直接打开页面,sdk.init() 会超时?

因为 SDK 需要在平台 iframe 内和父窗口握手。直接双击打开 HTML 文件,不存在父窗口运行时,自然无法连上平台。

本地调试要么用官方 starter 的 mock 运行时,要么通过平台预览页联调。

我什么时候该用 worldbook

当你希望“同一个角色卡,在不同阶段给 AI 注入不同知识块”时,用 worldbook

典型场景:

我什么时候该用 storage

当数据需要跨对话保留时,用 storage

比如:

React 是不是必须的?

不是。

原生 HTML、原生 JS 一样可以做独立页面。React 只是更适合复杂界面。

做独立页面时,最推荐先接哪几个 API?

先接这四个:

  1. character.getInfo()
  2. variable.getAll()
  3. chat.send()
  4. storage.getOrDefault()

创作者需要知道平台正在用哪个 AI 模型吗?

不需要。

正常使用 chat.send()chat.sendStream()generateRaw() 时,都不要硬编码模型名。平台会使用当前会话配置的模型。

只有平台明确提供可选模型,并且你的玩法确实需要临时覆盖模型时,才考虑传 model。否则模型名变化会让你的角色卡独立页面变得不好维护。

AI 回复需要 2 分钟才流式输出完,会被 SDK 超时吗?

正常不会。

SDK 的默认 60 秒超时是“流式事件空闲超时”,不是整条回复的总时长限制。只要平台持续返回片段,2 分钟的长回复也可以继续接收。

如果连续 60 秒没有任何流式事件,SDK 才会认为这次生成可能已经断开。


相关入口


一句话结论

如果你只是想给消息补一个 UI,直接用 HTML 消息和 $charx

如果你要做真正的独立页面,就从官方 starter 开始,用 @ai_wanjia/sdk 接平台运行时,不要自己从零搭通信层。