Skip to content

API 参考

沙盒暴露的所有内容的完整列表 —— 全局变量、组件、useYumina() 的每个字段和方法、类型定义,以及被屏蔽浏览器 API 的替代方案。

这是参考文档,不是教程。请先阅读自定义 UI 指南了解全貌;需要查找具体签名时再来这里。

本页面的所有内容均源自 packages/app/sandbox/ 中的实际实现,与编辑器附带的沙盒版本保持一致。


沙盒全局变量

以下名称在你的根组件树中随处可用,无需 import 语句

名称类型说明
Reactmodule完整的 React(useStateuseEffectuseRefuseMemouseCallbackuseLayoutEffectFragment 等)
useYuminahook平台 SDK —— 参见 useYumina() SDK
useAssetFonthook从素材库加载自定义字体 —— 参见 useAssetFont()
Iconsobject1400+ Lucide 图标组件:<Icons.Heart /><Icons.Sword />。完整目录:https://lucide.dev/icons
Chatcomponent完整聊天构建块 —— 参见 <Chat>
MessageListcomponent仅消息列表(不含输入框) —— 参见 <MessageList>
MessageInputcomponent仅输入栏 —— 参见 <MessageInput>
ChatCanvascomponent<Chat /> 的旧版别名 —— 参见 <ChatCanvas>
exportsmoduleobjectCJS 风格的导出回退;通常可以忽略

不要 import React 或上面列出的任何名称 —— 它们由沙盒注入。写 import React from "react" 会在编译时被静默剥离,但属于多余操作。

你自己的文件可以被 import —— 多文件根组件使用 ES 模块语法:import StatBar from "./stat-bar"。扩展名 .tsx.ts.jsx.js 可以省略。


useYumina() SDK

在组件函数中调用它:

tsx
function MyWorld() {
  const api = useYumina()
  // api.variables, api.sendMessage(...), ...
}

完整接口,按用途分组:

状态读取(同步)

读取最新的游戏状态。当其中任何值发生变化时,组件会重新渲染。

字段类型含义
variablesRecord<string, unknown>会话级游戏变量。示例:{ health: 80, gold: 150 }
globalVariablesRecord<string, unknown>跨所有会话共享的全局变量
personalVariablesRecord<string, unknown>跨会话的每玩家变量
roomPersonalVariablesRecord<string, unknown>当前房间内的每玩家变量(多人游戏)
worldNamestring当前世界的名称
worldIdstring当前世界的 UUID
sessionIdstring当前游戏会话的 UUID
currentUser{ id, name?, image? } | null原始账号信息:id、显示名称、账号头像。未登录时为 null。用于账号级别的 UI,如「查看个人资料」。对于世界内的角色扮演渲染,请使用 user
user{ name: string; avatar: string | null }角色扮演中的玩家 —— 与 宏的人设/账号分支逻辑相同。当玩家启用了人设时,user.name 是人设名称,user.avatar 是人设头像;否则回退到账号信息。这是你在世界内聊天气泡、角色卡片、个人面板中应该使用的
roomRecord<string, unknown> | null当前多人游戏房间数据,单人模式下为 null
mode"session" | "guest-preview""session" 是真实游戏会话。"guest-preview" 是未登录的 Hub 预览 —— 修改状态的操作会变为空操作,并向父级弹出登录提示
capabilities{ canSendMessage, canPersistSession, canUseSessionApis, requiresAuth }当前 mode 允许的功能。读取这些值来禁用会变为空操作的按钮(例如访客预览中的发送按钮),或渲染内联的「登录以继续」提示
languagestring宿主的当前 i18n 语言代码("en""zh" 等)。用于在卡片内选择翻译,无需依赖宿主的 i18next 实例
messagesArray<Record<string, unknown>>完整消息历史 —— 参见 SandboxMessage
permissionsRecord<string, unknown> | null当前玩家对此世界的权限(编辑、分享等)
isStreamingbooleanAI 正在生成回复时为 true
streamingContentstringAI 的实时流式文本(频繁更新)
streamingReasoningstringAI 的实时「思考」/推理文本(仅适用于推理模型)
pendingChoicesstring[]规则发出的选项按钮标签
errorstring | null当前错误消息(API 故障、生成错误)或 null
readOnlyboolean查看他人会话时为 true —— <Chat /> 会自动隐藏输入框
checkpointsArray<Checkpoint>已保存的存档点 —— 参见 Checkpoint
greetingContentstring | null从世界条目计算出的问候文本(<Chat /> 用作空状态内容)
canvasMode"chat" | "custom" | "fullscreen"当前画布模式
selectedModelstring当前选择的 AI 模型 ID
userPlanstring用户的订阅计划("free""go""plus""pro""ultra""internal"
preferredProvider"official" | "private"官方 API 与用户自己的密钥
entriesReadonlyArray<SandboxEntry>世界知识库条目 —— 仅已启用的,按 position 排序。参见知识库查询SandboxEntry

游戏动作(即发即忘)

这些方法不返回任何值;它们只是将意图发送给父应用。

方法功能说明
sendMessage(text)以玩家身份发送消息,触发 AI 回复
setVariable(id, value, options?)设置变量。options{ scope?: string; targetUserId?: string }scope 选择变量作用域(用于全局/个人),targetUserId 允许你在多人游戏中为特定玩家写入变量
executeAction(actionId)触发规则引擎定义的命名动作(例如 "attackBoss"
switchGreeting(index)按索引切换到不同的问候语变体
clearPendingChoices()不选择任何选项,直接关闭待处理的选项按钮
setComposerDraft(text)text 放入聊天输入框并聚焦。不会发送。 当你希望玩家在点击发送前先审阅或编辑消息时使用(例如一个 NPC 交互按钮预填对话开场白)。仅在沙盒本地运行 —— 无需父级往返 —— 因此只能与内置的 <MessageInput> / <Chat> 组件配合使用

聊天控制

默认聊天栏能做的所有操作,均已暴露,让你的自定义 UI 也能使用。

方法功能说明
editMessage(messageId, content)编辑已有消息。返回 Promise<boolean>;成功时为 true
deleteMessage(messageId)删除一条消息。返回 Promise<boolean>
regenerateMessage(messageId)请求 AI 重新生成指定回复(即发即忘)
continueLastMessage()从最后一条 AI 消息继续生成(即发即忘)
stopGeneration()中断当前流式生成(即发即忘)
restartChat()清除所有消息,重置状态,重新开始
swipeMessage(messageId, "left" | "right")在一条消息的 AI 备选回复(swipes)之间切换。返回 Promise<Record<string, unknown>>

会话与分支

方法功能说明
revertToMessage(messageId)将对话回退到 messageId 之前。返回 Promise<void>
branchFromMessage(messageId)在指定消息处分叉新会话(克隆该消息及之前的所有消息和状态快照)。返回 Promise<string | null> —— 新会话 ID,失败时为 null(流式生成中、多人房间、缺少消息等情况都会失败)
getBranchContext()获取当前分支切片(自身、父级、兄弟、子级)。返回 Promise<BranchContext>。每次调用都会重新获取;无客户端缓存。参见 BranchContext
createSession(worldId)为指定世界创建新会话。返回 Promise<string>,包含新会话 ID
deleteSession(sessionId)删除一个会话。返回 Promise<void>
listSessions(worldId)列出指定世界的所有会话。返回 Promise<Array<Record<string, unknown>>>

存档点

存档点是当前会话内的一个命名快照,你可以回退到该快照。

方法功能说明
saveCheckpoint()将当前会话状态保存为新存档点。返回 Promise<void>(之后 checkpoints 字段会被刷新)
loadCheckpoints()请求父级刷新 checkpoints 数组。返回 Promise<void>
restoreCheckpoint(checkpointId)将会话恢复到已保存的存档点。返回 Promise<void>
deleteCheckpoint(checkpointId)删除一个存档点。返回 Promise<void>

音频

方法功能说明
playAudio(trackId, opts?)播放条目中定义的音频轨道。opts{ volume?, fadeDuration?, chainTo?, maxDuration?, duckBgm?, loop? } —— 所有时长(fadeDurationmaxDuration)单位均为chainTo 指定下一个要播放的 trackId;duckBgm 在播放期间降低 BGM 音量;loop 覆盖该音轨的循环设置,仅对本次播放生效
stopAudio(trackId?, fadeDuration?)停止一个轨道(省略 trackId 停止所有音频)。fadeDuration 单位为秒。会销毁该音频元素 —— 想从原位置继续请用 pauseAudio
pauseAudio(trackId)原地暂停一个轨道,保留其播放位置
resumeAudio(trackId)继续一个被 pauseAudio 暂停的轨道
onAudioEnded(cb)订阅「非循环音轨播放结束」事件 —— cb(trackId)。返回一个取消订阅的函数。用于让手写播放列表自动播放下一首
setAudioVolume(type, volume)type"bgm""sfx"volume 范围 0–1
getAudioVolume(type)同步返回当前音量(0–1)

UI / 导航

方法功能说明
toggleImmersive()切换沉浸式(全屏)模式
openPersonaManager()打开玩家的人设管理器——不离开世界即可切换 / 新建 / 编辑人设(与 composer「+」菜单打开的是同一面板)。当前人设为账号级全局,切换会在下一条消息生效
copyToClipboard(text)复制到剪贴板(替代 navigator.clipboard.writeText
navigate(path)请求父级导航到路径,如 "/app/hub"(替代 window.location = ...
showToast(message, type?)在父级 UI 中显示 toast 通知。type"success""error""info"(默认)

持久化存储(按世界隔离)

localStorage 的替代方案。按 worldId 隔离;世界之间不能读取彼此的键。

方法功能说明
storage.get(key)读取。返回 Promise<string | null>
storage.set(key, value)写入(仅限字符串)。返回 Promise<void>
storage.remove(key)删除。返回 Promise<void>

需要存储复杂数据?在存取时使用 JSON.stringify / JSON.parse

知识库查询

从卡片内只读访问世界的知识库。适用于在组装辅助 LLM 提示时检查或手动挑选条目、构建游戏内日志查看器,或搭建调试面板。

字段 / 方法功能说明
entriesReadonlyArray<SandboxEntry> —— 每个已启用的条目,已按 position 排序。参见 SandboxEntry
getEntry(name)精确名称查找一个条目。返回 SandboxEntry | null。在 localhost 上,查找失败时会输出一次性警告,列出可用名称 —— 当你重命名条目后忘记更新卡片时很有用

大多数情况下你不需要直接使用这些:向 ai.complete() 传入 includeLorebook: "matched",服务器会自动为你组装知识(见下文)。当你需要精确控制时才使用 entries / getEntry —— 例如*「这个 NPC 只知道标记为 tavern 的条目」*。

原始 AI 补全

在主聊天管道之外调用 LLM。适用于「侧边栏中的 NPC 内心独白」、「AI 生成的物品描述」、「卡片内的手机聊天」等场景。不会写入消息历史,不会触发状态更新,不会消耗问候语。

tsx
const api = useYumina()
const text = await api.ai.complete({
  messages: [
    { role: "system", content: "You are a surly merchant." },
    { role: "user", content: "Price me an iron sword." },
  ],
  onDelta: (chunk) => setStreaming((s) => s + chunk),  // 可选,逐 token
  model: "claude-sonnet-4-6",                           // 可选,默认为 selectedModel
  maxTokens: 500,                                       // 可选,默认 2048,最大 8192
  temperature: 0.7,                                     // 可选
  includeLorebook: "matched",                           // 可选 —— 见下文
})

返回 Promise<string>,包含完整响应。客户端超时时间为 120 秒。

限制与费用

限制来源
每次调用最大消息数50服务器以 HTTP 400 拒绝
最大总内容量所有消息合计 50,000 字符服务器以 HTTP 400 拒绝
maxTokens 默认值2048省略时的默认值
maxTokens 上限8192超出值会被静默截断
temperature 范围0–2,默认 1.0超出范围的值会被截断
默认模型玩家的 selectedModel,如果 modelselectedModel 都未设置则回退到 anthropic/claude-sonnet-4.6
速率限制与主聊天共享 —— 辅助调用和主聊天回合使用相同的每分钟额度超出时返回 HTTP 429 + INSUFFICIENT_CREDITS 风格的代码
额度与主聊天相同的按 token 计费。BYOK 用户跳过服务器额度扣减,但仍需向自己的供应商付费以 endpoint "side-completion" 记录
认证会话必须属于当前玩家;否则调用以 HTTP 404 失败

includeLorebook —— 自动注入世界知识

辅助调用绕过主聊天的提示词组装,因此除非你提供知识库,否则模型对你的角色一无所知。传入 includeLorebook,服务器会在前面插入一条由世界条目构建的系统消息:

行为
省略 / false不注入(默认)。用于翻译、摘要、分类 —— 任何不需要世界上下文的场景
true / "all"注入所有已启用的非问候语条目,按 position 排序。可预测,token 成本较高
"matched"messages 中的最后一条用户消息运行与主聊天相同的关键词匹配器。alwaysSend 条目始终包含;关键词触发的条目仅在相关时添加。推荐用于角色内辅助调用

没有这个选项,一个「角色内」的辅助调用会让模型仅凭名字编造角色性格 —— 卡片内的人设会偏离主聊天。使用 "matched" 后,与 NPC 的手机聊天能看到与主聊天相同的世界知识和角色描述。

tsx
// 保持设定一致的手机聊天
api.ai.complete({
  messages: [
    { role: "system", content: "Stay strictly in character as Balder. Reply in one or two short lines." },
    ...history,
    { role: "user", content: userText },
  ],
  includeLorebook: "matched",  // 服务器拉取 Balder 的角色描述 + 相关世界知识
})

如果你需要更精细的控制 —— 按名称注入特定条目,或只注入带有特定标签的条目 —— 遍历 api.entries 自行组装系统消息,而不是使用 includeLorebook

tsx
const tavernLore = api.entries
  .filter((e) => e.tags?.includes("tavern"))
  .map((e) => `【${e.name}】\n${e.content}`)
  .join("\n\n")

api.ai.complete({
  messages: [
    { role: "system", content: `You are the tavern keeper.\n\n${tavernLore}` },
    { role: "user", content: userText },
  ],
})

"matched" 模式注意事项:它只扫描最后一条用户消息中的关键词(不是完整历史),而且依赖游戏变量的条件门控条目在辅助调用中不会触发(匹配器看到的是空状态存根)。如果精确度比 token 更重要,请使用 true 强制包含所有内容。

上下文注入

下一次主聊天 AI 回合注入一条一次性上下文消息。使用一次后即被消耗;不会创建可见的聊天消息。适用于「手机消息」、「NPC 幕后对话」、「环境变化」—— 主 AI 应该知道但玩家不需要看到聊天气泡的信息。

tsx
api.injectContext("You just received a cryptic text: 'Tonight, 9pm, usual place.'", { role: "system" })
// 玩家下一次发送消息时,主 AI 会看到这条系统消息。

options{ role?: "system" \| "user" }(默认为 "system")。

模型选择器

字段 / 方法功能说明
selectedModel当前模型 ID
userPlan用户的计划层级
preferredProvider"official""private"
setModel(modelId)切换模型(即发即忘)
getModels()返回 Promise<{ models, pinnedModels, recentlyUsed }>,其中 modelsArray<{ id, name, provider, contextLength }>
pinModel(modelId) / unpinModel(modelId)固定 / 取消固定模型

素材

方法功能说明
resolveAssetUrl(ref)@asset:xxx 引用转换为 CDN URL。纯字符串转换,无网络请求。HTTP/HTTPS URL 直接传递不变

Markdown

方法功能说明
renderMarkdown(text)将 markdown 转换为安全的 HTML(HTML 实体已转义,危险标签已剥离,格式保留)。将结果传给自定义气泡中的 dangerouslySetInnerHTML 即可确保安全 —— 参见下方示例
tsx
<div dangerouslySetInnerHTML={{ __html: api.renderMarkdown(msg.rawContent) }} />

组件

<Chat>

平台的完整聊天体验。这是日常构建块 —— 零 props 即可获得默认聊天。

包含:消息列表、自动滚动、流式光标、swipe 控件、消息操作(编辑/删除/重新生成)、输入栏、选项按钮、模型选择器、只读模式、问候语占位符。

tsx
<Chat renderBubble={(msg) => <MyBubble {...msg} />} />

Props

Prop类型描述
renderBubble?(props: BubbleProps) => ReactNode自定义每条消息气泡的外观。省略时回退到默认 markdown 渲染
className?string外层容器的额外 CSS 类
children?ReactNode渲染在消息列表上方的内容(例如固定的 HUD 头部)

BubbleProps

你的 renderBubble 回调接收到的 msg 对象:

字段类型含义
contentHtmlstring预渲染的安全 HTML(markdown 已转换)。通常通过 dangerouslySetInnerHTML 使用
rawContentstring渲染前的原始 markdown 文本(包含指令文本)
role"user" | "assistant" | "system"消息来源
messageIndexnumber列表中的位置(0 = 第一条,通常是问候语)
isStreamingboolean当此消息正在流式传输时为 true
stateSnapshotRecord<string, unknown> | null此消息生成时的游戏状态(适用于「当时的 HP/位置是多少」)
variablesRecord<string, unknown>当前(最新)游戏变量
renderMarkdown(text) => string辅助函数:将任意 markdown 文本转换为安全 HTML

<MessageList>

仅消息流(含滚动、流式光标、swipe 控件)。不包含输入栏。

tsx
<MessageList />

不接受 renderBubble —— 要自定义气泡请使用 <Chat renderBubble={...} />,或完全跳过 <MessageList> 直接读取 api.messages(视觉小说模式)。

<MessageInput>

仅输入栏(含模型选择器、选项按钮、继续/重新开始菜单、流式状态)。

tsx
<MessageInput />

api.readOnlytrue 时自动隐藏。

<ChatCanvas>

旧版别名 —— 与 <Chat /> 完全相同。旧世界继续正常工作;新代码应优先使用 <Chat />


useAssetFont()

加载已上传的字体素材为 @font-face,并返回一个可直接用于 CSS font-family 值的字符串。

tsx
const fontFamily = useAssetFont("@asset:my-font-id", {
  family: "Cinzel",
  fallback: "serif",
})
return <div style={{ fontFamily }}>Ancient runes</div>

签名

ts
useAssetFont(
  assetRef: string | null | undefined,
  options?: AssetFontOptions
): string

字体异步加载。加载期间,hook 返回 options.fallback(默认为 "serif");加载完成后,触发重新渲染并返回完整的 family 字符串(带后缀以避免名称冲突)。

AssetFontOptions

字段类型描述
family?string字体族名称。省略时从文件名或 assetRef 推断
fallback?string加载期间显示的后备字体。默认 "serif"
filename?string | null原始文件名,用于推断格式
mimeType?string | nullMIME 类型,用于推断格式
format?"opentype" | "truetype" | "woff" | "woff2" | null显式格式覆盖
weight?string | numberfont-weight
style?stringfont-style(例如 "italic"
stretch?stringfont-stretch
display?FontDisplayfont-display(默认 "swap"

类型

SandboxMessage

api.messages 中每个条目的结构:

ts
interface SandboxMessage {
  id: string
  sessionId: string
  role: "user" | "assistant" | "system"
  content: string
  status?: "complete" | "streaming" | "failed"
  errorMessage?: string | null
  authorUserId?: string | null          // 发送者(多人游戏)
  authorNameSnapshot?: string | null    // 发送时的显示名称
  stateChanges?: Record<string, unknown> | null   // 此消息的变量更新差异
  stateSnapshot?: Record<string, unknown> | null  // 消息生成时的完整状态
  swipes?: Array<{ content, stateSnapshot }>      // AI 备选回复
  activeSwipeIndex?: number
  model?: string | null
  tokenCount?: number | null
  generationTimeMs?: number | null
  compacted?: boolean                   // 隐藏在「较早消息」部分中
  attachments?: Array<{ type, mimeType, name, url }> | null
  createdAt: string                     // ISO-8601
}

Checkpoint

ts
interface Checkpoint {
  id: string
  name: string
  messageCount: number
  createdAt: string   // ISO-8601
}

SandboxEntry

通过 api.entriesapi.getEntry() 暴露的单个只读知识库条目:

ts
interface SandboxEntry {
  id: string
  name: string
  content: string
  keywords: string[]
  position: number
  section: "system-presets" | "examples" | "chat-history" | "post-history"
  enabled: boolean
  role: string                            // "system" | "character" | "lore" | etc.
  tags?: string[]
}

这是引擎内部 WorldEntry 的精简视图 —— 仅包含卡片进行提示词组装所需的字段。运行时会预先过滤禁用的条目并按 position 预排序,因此卡片永远不需要自行处理这些。

BranchContext

ts
interface BranchNode {
  id: string
  name: string | null
  parentSessionId: string | null
  branchedFromMessageId: string | null
  messageCount: number
  updatedAt: string   // ISO-8601
  createdAt: string   // ISO-8601
}

interface BranchContext {
  current: BranchNode          // 你当前所在的会话
  parent: BranchNode | null    // 你分叉自的分支,根节点时为 null
  siblings: BranchNode[]       // 从同一父级分叉的其他分支,按时间从旧到新
  children: BranchNode[]       // 从 `current` 分叉的分支,按时间从旧到新
}

被屏蔽的浏览器 API

你的代码运行在跨域的 sandbox="allow-scripts" iframe 中,没有 allow-same-origin。这意味着:

  • 无法访问父应用的 cookies / localStorage
  • 无法发起带凭证的网络请求
  • 无法直接操作 window.parent

以下 API 要么完全被屏蔽,要么透明地重定向到 SDK 桥接。

重定向(旧代码继续正常工作)

你写的代码实际发生的事
fetch('/api/...')通过父级的已认证 fetch 代理
fetch('/cdn/...')允许(CSP 许可)
fetch('any other URL')被拒绝(抛出异常)
localStorage.getItem/setItem/removeItem/clear通过 api.storage 路由,按世界隔离
sessionStorage.*同上
navigator.clipboard.writeText()等同于 api.copyToClipboard()
navigator.clipboard.readText() / read() / write()被拒绝(抛出异常)
window.location.pathname / href / assign / replace合成对象;pathname 始终为 /app/chat/{sessionId};赋值或调用 assign / replace 会触发导航
window.location.reload()桥接到重新加载会话
window.__yuminaToggleImmersive()等同于 api.toggleImmersive()

推荐用法

编写新代码时,直接使用 SDK —— 重定向是为旧世界存在的,但 SDK 更简洁稳定:

不要写应该写
fetch('/api/sessions', { method: 'POST' })api.createSession(worldId)
fetch('/api/sessions/' + sid, { method: 'DELETE' })api.deleteSession(sid)
localStorage.getItem("k")await api.storage.get("k")
window.location = "/app/hub"api.navigate("/app/hub")
navigator.clipboard.writeText(t)api.copyToClipboard(t)

可用的浏览器 API

沙盒对不涉及网络或共享源的操作非常宽容。以下 API 与普通浏览器中的行为相同,无需 SDK 包装:

API卡片中的典型用途
<input type="file"> + FileReader.readAsDataURL / readAsText让玩家选择图片/音频/文本文件 → 作为 data URL 或字符串存储在变量中。参见 Recipe: Player-Uploaded Images
URL.createObjectURL / revokeObjectURLBlob 生成临时内存 URL(例如保存前预览)
<canvas> + getContext("2d") + toDataURL / toBlob在保存到变量之前调整大小、裁剪或合成图片
<img><audio><video>渲染本地源 URL、@asset:... 解析后的 URL、data:/blob: URL
IntersectionObserverResizeObservermatchMediarequestAnimationFrame标准布局/动画原语
crypto.randomUUIDcrypto.subtle客户端状态的哈希和 ID 生成
WebAudioAudioContext轻量音频合成或分析
Notificationnavigator.vibratescreen.orientation受浏览器级别权限限制,而非沙盒限制

一览:完整 API

一张表,扫一遍即可。

useYumina()
├── 状态读取
│   ├── variables, globalVariables, personalVariables, roomPersonalVariables
│   ├── worldName, worldId, sessionId
│   ├── currentUser (账号), user (人设感知)
│   ├── room, mode, capabilities, language
│   ├── messages, permissions, entries
│   ├── isStreaming, streamingContent, streamingReasoning
│   ├── pendingChoices, error, readOnly, greetingContent, canvasMode
│   ├── checkpoints
│   └── selectedModel, userPlan, preferredProvider
├── 游戏动作
│   ├── sendMessage(text)
│   ├── setVariable(id, value, options?)
│   ├── executeAction(actionId)
│   ├── switchGreeting(index)
│   ├── clearPendingChoices()
│   └── setComposerDraft(text)              // 预填,不发送
├── 聊天控制
│   ├── editMessage(id, content) → Promise<boolean>
│   ├── deleteMessage(id) → Promise<boolean>
│   ├── regenerateMessage(id)
│   ├── continueLastMessage()
│   ├── stopGeneration()
│   ├── restartChat()
│   └── swipeMessage(id, direction) → Promise
├── 会话 / 分支
│   ├── revertToMessage(id) → Promise<void>
│   ├── branchFromMessage(id) → Promise<string | null>
│   ├── getBranchContext() → Promise<BranchContext>
│   ├── createSession(worldId) → Promise<string>
│   ├── deleteSession(id) → Promise<void>
│   └── listSessions(worldId) → Promise<Array>
├── 存档点
│   ├── saveCheckpoint() → Promise<void>
│   ├── loadCheckpoints() → Promise<void>
│   ├── restoreCheckpoint(id) → Promise<void>
│   └── deleteCheckpoint(id) → Promise<void>
├── 音频
│   ├── playAudio(trackId, opts?)
│   ├── stopAudio(trackId?, fadeDuration?)
│   ├── pauseAudio(trackId)
│   ├── resumeAudio(trackId)
│   ├── onAudioEnded(cb) → unsubscribe
│   ├── setAudioVolume(type, volume)
│   └── getAudioVolume(type) → number
├── UI / 导航
│   ├── toggleImmersive()
│   ├── openPersonaManager()
│   ├── copyToClipboard(text)
│   ├── navigate(path)
│   └── showToast(message, type?)
├── 存储
│   ├── storage.get(key) → Promise<string | null>
│   ├── storage.set(key, value) → Promise<void>
│   └── storage.remove(key) → Promise<void>
├── 知识库
│   ├── entries (ReadonlyArray<SandboxEntry>)  // 按 position 排序,仅已启用
│   └── getEntry(name) → SandboxEntry | null
├── AI
│   └── ai.complete({ messages, onDelta?, model?, maxTokens?, temperature?, includeLorebook? }) → Promise<string>
│        // includeLorebook: true | "all" | "matched" —— 自动注入世界知识
├── 上下文注入
│   └── injectContext(message, { role? })
├── 模型选择器
│   ├── setModel(modelId)
│   ├── getModels() → Promise<{ models, pinnedModels, recentlyUsed }>
│   ├── pinModel(id), unpinModel(id)
├── 素材
│   └── resolveAssetUrl(ref) → string
└── Markdown
    └── renderMarkdown(text) → string   // 安全 HTML

沙盒全局变量(无需 import)
├── React
├── useYumina, useAssetFont
├── Icons  (1400+ Lucide 图标)
├── Chat, MessageList, MessageInput, ChatCanvas (旧版别名)
└── Tailwind 实用类(CSS 级别)

被屏蔽 / 重定向
├── fetch('/api/...') → 代理
├── localStorage / sessionStorage → api.storage
├── window.location → 合成 + navigate
└── navigator.clipboard → copyToClipboard

可直接使用的浏览器 API
├── <input type="file"> + FileReader      // 玩家文件上传 → data URL
├── <canvas>, URL.createObjectURL          // 图片处理
├── IntersectionObserver, ResizeObserver, matchMedia, rAF
├── crypto.randomUUID, crypto.subtle
└── WebAudio (AudioContext)

下一步:返回自定义 UI 指南查看实际示例,或浏览 Recipes 查找最接近你目标的模板。