@ai_wanjia/sdk 创作者教程
这份文档的目标很直接:
- 帮你判断该用哪种接入方式。
- 帮你最快跑通第一个可交互界面。
- 帮你分清
variable、storage、worldbook、chat各自负责什么。
如果你只是想尽快起步,先看“先选方案”和“最快起步路径”这两节。
先选方案
平台里和 SDK 相关的自定义界面,实际上分成两条路:
| 方式 | 适合什么 | 是否需要 npm | 推荐人群 |
|---|---|---|---|
| HTML 消息 | AI 回复里插入状态卡、按钮卡、小型面板 | 不需要 | 想快速做轻量效果的创作者 |
| 独立页面 + SDK | 做完整游戏界面、背包页、地图页、React 页面 | 需要 | 想做独立页面或完整玩法的创作者 |
最简单的判断方法:
- 你只想在消息里插一个小卡片,用 HTML 消息。
- 你想让角色卡接管主界面,用 独立页面 + SDK。
先理解边界
SDK 负责的是“界面层”和“交互层”,不是“模型层”。
SDK 能做的事情:
- 发送消息到平台聊天链路
- 读取角色信息
- 读写变量
- 保存长期数据
- 控制独立页面 UI
- 在当前对话里动态调整世界书条目
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
这两套模板都已经内置:
- 本地 mock 运行时
- 平台 iframe 真实 SDK 运行时
- 角色信息示例
- 变量读写示例
storage示例- 聊天发送和流式渲染示例
推荐顺序:
- 先让模板本地跑起来。
- 先把首页文案和样式换成你的主题。
- 再把变量名、动作按钮、聊天逻辑改成你的玩法。
- 最后再上传到角色卡的独立界面资源包里联调。
方式一:HTML 消息
它适合什么
适合做这些东西:
- 血条卡片
- 状态面板
- 任务提示块
- 小型交互按钮
- 对话尾部附带的 UI 补充层
不适合做这些东西:
- 多页面独立应用
- 复杂路由
- 完整地图探索界面
- 背包页、商城页、存档页这类主界面系统
它怎么接入
当 AI 回复里包含 HTML 代码块时,平台会把这段 HTML 放进沙盒 iframe,并自动注入 window.$charx。
这意味着:
- 你不需要安装 npm 包
- 你不需要自己写握手代码
- 你可以直接在 HTML 代码块里调用
$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 消息场景里,当前最常用的是 toast 和 scrollToBottom。
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 消息的推荐做法
- 把它当“轻量卡片层”,不要把它硬做成大应用。
- 变量名尽量统一,不要今天叫
hp,明天叫current_hp。 - 多用
variable.get()读状态,少在 HTML 里自己维护一套复杂状态机。 - 如果界面开始出现多个面板、多个页面、复杂流程,及时切换到独立页面方案。
方式二:独立页面 + npm SDK
它适合什么
适合做这些东西:
- 角色卡的完整独立页面
- 地图、背包、任务、商店、存档页
- React 游戏界面
- 视觉小说 UI
- 多个交互区同时联动的大页面
最小原生示例
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
这是“当前对话状态”。
适合放:
- HP
- MP
- 好感度
- 当前地点
- 当前阶段
- 是否进入战斗
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')
推荐的实际开发顺序
很多创作者一上来就想把完整玩法全部做完,结果调试成本非常高。更稳的顺序是:
- 先跑官方 starter。
- 先做一个首页。
- 先接通
character、variable、chat。 - 再补
storage。 - 最后再做
worldbook、音频、成就、排行榜这类附加能力。
如果你是做独立页面玩法,推荐先只做这四个能力:
character.getInfo()variable.getAll() / set()chat.send()storage.getOrDefault() / set()
这四个先通了,后面再扩就容易很多。
当前能力对照表
这张表用来回答一个实际问题:同样是创作者写界面,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
后果:
- AI 看不到这些数据
- 你的剧情状态和界面状态会脱节
如果数据需要影响当前对话流程,优先放 variable。
误区二:把 HTML 消息硬做成大应用
后果:
- 页面结构越来越复杂
- 状态同步越来越难
- 调试成本越来越高
如果你已经开始做多面板、多区域、多流程,应该切到独立页面。
误区三:页面自己维护一套“假聊天”
正确做法:
- 玩家动作通过 SDK 发回平台
- AI 回复由平台返回
- 你的页面只负责把这些结果渲染出来
误区四:没有统一变量命名
建议从一开始就定好命名:
| 类型 | 推荐风格 | 示例 |
|---|---|---|
| 数值变量 | 简短小写 | 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?
先接这四个:
character.getInfo()variable.getAll()chat.send()storage.getOrDefault()
创作者需要知道平台正在用哪个 AI 模型吗?
不需要。
正常使用 chat.send()、chat.sendStream() 和 generateRaw() 时,都不要硬编码模型名。平台会使用当前会话配置的模型。
只有平台明确提供可选模型,并且你的玩法确实需要临时覆盖模型时,才考虑传 model。否则模型名变化会让你的角色卡独立页面变得不好维护。
AI 回复需要 2 分钟才流式输出完,会被 SDK 超时吗?
正常不会。
SDK 的默认 60 秒超时是“流式事件空闲超时”,不是整条回复的总时长限制。只要平台持续返回片段,2 分钟的长回复也可以继续接收。
如果连续 60 秒没有任何流式事件,SDK 才会认为这次生成可能已经断开。
相关入口
- SDK 包说明:
sdk/README.md - 原生 H5 模板:
sdk/examples/charx-h5-starter/README.md - React 模板:
sdk/examples/charx-react-starter/README.md
一句话结论
如果你只是想给消息补一个 UI,直接用 HTML 消息和 $charx。
如果你要做真正的独立页面,就从官方 starter 开始,用 @ai_wanjia/sdk 接平台运行时,不要自己从零搭通信层。