API 参考
沙盒暴露的所有内容的完整列表 —— 全局变量、组件、
useYumina()的每个字段和方法、类型定义,以及被屏蔽浏览器 API 的替代方案。
这是参考文档,不是教程。请先阅读自定义 UI 指南了解全貌;需要查找具体签名时再来这里。
本页面的所有内容均源自 packages/app/sandbox/ 中的实际实现,与编辑器附带的沙盒版本保持一致。
沙盒全局变量
以下名称在你的根组件树中随处可用,无需 import 语句:
| 名称 | 类型 | 说明 |
|---|---|---|
React | module | 完整的 React(useState、useEffect、useRef、useMemo、useCallback、useLayoutEffect、Fragment 等) |
useYumina | hook | 平台 SDK —— 参见 useYumina() SDK |
useAssetFont | hook | 从素材库加载自定义字体 —— 参见 useAssetFont() |
Icons | object | 1400+ Lucide 图标组件:<Icons.Heart />、<Icons.Sword />。完整目录:https://lucide.dev/icons |
Chat | component | 完整聊天构建块 —— 参见 <Chat> |
MessageList | component | 仅消息列表(不含输入框) —— 参见 <MessageList> |
MessageInput | component | 仅输入栏 —— 参见 <MessageInput> |
ChatCanvas | component | <Chat /> 的旧版别名 —— 参见 <ChatCanvas> |
exports、module | object | CJS 风格的导出回退;通常可以忽略 |
不要 import React 或上面列出的任何名称 —— 它们由沙盒注入。写 import React from "react" 会在编译时被静默剥离,但属于多余操作。
你自己的文件可以被 import —— 多文件根组件使用 ES 模块语法:import StatBar from "./stat-bar"。扩展名 .tsx、.ts、.jsx、.js 可以省略。
useYumina() SDK
在组件函数中调用它:
function MyWorld() {
const api = useYumina()
// api.variables, api.sendMessage(...), ...
}完整接口,按用途分组:
状态读取(同步)
读取最新的游戏状态。当其中任何值发生变化时,组件会重新渲染。
| 字段 | 类型 | 含义 |
|---|---|---|
variables | Record<string, unknown> | 会话级游戏变量。示例:{ health: 80, gold: 150 } |
globalVariables | Record<string, unknown> | 跨所有会话共享的全局变量 |
personalVariables | Record<string, unknown> | 跨会话的每玩家变量 |
roomPersonalVariables | Record<string, unknown> | 当前房间内的每玩家变量(多人游戏) |
worldName | string | 当前世界的名称 |
worldId | string | 当前世界的 UUID |
sessionId | string | 当前游戏会话的 UUID |
currentUser | { id, name?, image? } | null | 原始账号信息:id、显示名称、账号头像。未登录时为 null。用于账号级别的 UI,如「查看个人资料」。对于世界内的角色扮演渲染,请使用 user |
user | { name: string; avatar: string | null } | 角色扮演中的玩家 —— 与 宏的人设/账号分支逻辑相同。当玩家启用了人设时,user.name 是人设名称,user.avatar 是人设头像;否则回退到账号信息。这是你在世界内聊天气泡、角色卡片、个人面板中应该使用的 |
room | Record<string, unknown> | null | 当前多人游戏房间数据,单人模式下为 null |
mode | "session" | "guest-preview" | "session" 是真实游戏会话。"guest-preview" 是未登录的 Hub 预览 —— 修改状态的操作会变为空操作,并向父级弹出登录提示 |
capabilities | { canSendMessage, canPersistSession, canUseSessionApis, requiresAuth } | 当前 mode 允许的功能。读取这些值来禁用会变为空操作的按钮(例如访客预览中的发送按钮),或渲染内联的「登录以继续」提示 |
language | string | 宿主的当前 i18n 语言代码("en"、"zh" 等)。用于在卡片内选择翻译,无需依赖宿主的 i18next 实例 |
messages | Array<Record<string, unknown>> | 完整消息历史 —— 参见 SandboxMessage |
permissions | Record<string, unknown> | null | 当前玩家对此世界的权限(编辑、分享等) |
isStreaming | boolean | AI 正在生成回复时为 true |
streamingContent | string | AI 的实时流式文本(频繁更新) |
streamingReasoning | string | AI 的实时「思考」/推理文本(仅适用于推理模型) |
pendingChoices | string[] | 规则发出的选项按钮标签 |
error | string | null | 当前错误消息(API 故障、生成错误)或 null |
readOnly | boolean | 查看他人会话时为 true —— <Chat /> 会自动隐藏输入框 |
checkpoints | Array<Checkpoint> | 已保存的存档点 —— 参见 Checkpoint |
greetingContent | string | null | 从世界条目计算出的问候文本(<Chat /> 用作空状态内容) |
canvasMode | "chat" | "custom" | "fullscreen" | 当前画布模式 |
selectedModel | string | 当前选择的 AI 模型 ID |
userPlan | string | 用户的订阅计划("free"、"go"、"plus"、"pro"、"ultra"、"internal") |
preferredProvider | "official" | "private" | 官方 API 与用户自己的密钥 |
entries | ReadonlyArray<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? } —— 所有时长(fadeDuration、maxDuration)单位均为秒;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 提示时检查或手动挑选条目、构建游戏内日志查看器,或搭建调试面板。
| 字段 / 方法 | 功能说明 |
|---|---|
entries | ReadonlyArray<SandboxEntry> —— 每个已启用的条目,已按 position 排序。参见 SandboxEntry |
getEntry(name) | 按精确名称查找一个条目。返回 SandboxEntry | null。在 localhost 上,查找失败时会输出一次性警告,列出可用名称 —— 当你重命名条目后忘记更新卡片时很有用 |
大多数情况下你不需要直接使用这些:向 ai.complete() 传入 includeLorebook: "matched",服务器会自动为你组装知识(见下文)。当你需要精确控制时才使用 entries / getEntry —— 例如*「这个 NPC 只知道标记为 tavern 的条目」*。
原始 AI 补全
在主聊天管道之外调用 LLM。适用于「侧边栏中的 NPC 内心独白」、「AI 生成的物品描述」、「卡片内的手机聊天」等场景。不会写入消息历史,不会触发状态更新,不会消耗问候语。
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,如果 model 和 selectedModel 都未设置则回退到 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 的手机聊天能看到与主聊天相同的世界知识和角色描述。
// 保持设定一致的手机聊天
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:
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 应该知道但玩家不需要看到聊天气泡的信息。
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 }>,其中 models 为 Array<{ id, name, provider, contextLength }> |
pinModel(modelId) / unpinModel(modelId) | 固定 / 取消固定模型 |
素材
| 方法 | 功能说明 |
|---|---|
resolveAssetUrl(ref) | 将 @asset:xxx 引用转换为 CDN URL。纯字符串转换,无网络请求。HTTP/HTTPS URL 直接传递不变 |
Markdown
| 方法 | 功能说明 |
|---|---|
renderMarkdown(text) | 将 markdown 转换为安全的 HTML(HTML 实体已转义,危险标签已剥离,格式保留)。将结果传给自定义气泡中的 dangerouslySetInnerHTML 即可确保安全 —— 参见下方示例 |
<div dangerouslySetInnerHTML={{ __html: api.renderMarkdown(msg.rawContent) }} />组件
<Chat>
平台的完整聊天体验。这是日常构建块 —— 零 props 即可获得默认聊天。
包含:消息列表、自动滚动、流式光标、swipe 控件、消息操作(编辑/删除/重新生成)、输入栏、选项按钮、模型选择器、只读模式、问候语占位符。
<Chat renderBubble={(msg) => <MyBubble {...msg} />} />Props
| Prop | 类型 | 描述 |
|---|---|---|
renderBubble? | (props: BubbleProps) => ReactNode | 自定义每条消息气泡的外观。省略时回退到默认 markdown 渲染 |
className? | string | 外层容器的额外 CSS 类 |
children? | ReactNode | 渲染在消息列表上方的内容(例如固定的 HUD 头部) |
BubbleProps
你的 renderBubble 回调接收到的 msg 对象:
| 字段 | 类型 | 含义 |
|---|---|---|
contentHtml | string | 预渲染的安全 HTML(markdown 已转换)。通常通过 dangerouslySetInnerHTML 使用 |
rawContent | string | 渲染前的原始 markdown 文本(包含指令文本) |
role | "user" | "assistant" | "system" | 消息来源 |
messageIndex | number | 列表中的位置(0 = 第一条,通常是问候语) |
isStreaming | boolean | 当此消息正在流式传输时为 true |
stateSnapshot | Record<string, unknown> | null | 此消息生成时的游戏状态(适用于「当时的 HP/位置是多少」) |
variables | Record<string, unknown> | 当前(最新)游戏变量 |
renderMarkdown | (text) => string | 辅助函数:将任意 markdown 文本转换为安全 HTML |
<MessageList>
仅消息流(含滚动、流式光标、swipe 控件)。不包含输入栏。
<MessageList />不接受 renderBubble —— 要自定义气泡请使用 <Chat renderBubble={...} />,或完全跳过 <MessageList> 直接读取 api.messages(视觉小说模式)。
<MessageInput>
仅输入栏(含模型选择器、选项按钮、继续/重新开始菜单、流式状态)。
<MessageInput />当 api.readOnly 为 true 时自动隐藏。
<ChatCanvas>
旧版别名 —— 与 <Chat /> 完全相同。旧世界继续正常工作;新代码应优先使用 <Chat />。
useAssetFont()
加载已上传的字体素材为 @font-face,并返回一个可直接用于 CSS font-family 值的字符串。
const fontFamily = useAssetFont("@asset:my-font-id", {
family: "Cinzel",
fallback: "serif",
})
return <div style={{ fontFamily }}>Ancient runes</div>签名
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 | null | MIME 类型,用于推断格式 |
format? | "opentype" | "truetype" | "woff" | "woff2" | null | 显式格式覆盖 |
weight? | string | number | font-weight |
style? | string | font-style(例如 "italic") |
stretch? | string | font-stretch |
display? | FontDisplay | font-display(默认 "swap") |
类型
SandboxMessage
api.messages 中每个条目的结构:
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
interface Checkpoint {
id: string
name: string
messageCount: number
createdAt: string // ISO-8601
}SandboxEntry
通过 api.entries 和 api.getEntry() 暴露的单个只读知识库条目:
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
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 / revokeObjectURL | 为 Blob 生成临时内存 URL(例如保存前预览) |
<canvas> + getContext("2d") + toDataURL / toBlob | 在保存到变量之前调整大小、裁剪或合成图片 |
<img>、<audio>、<video> | 渲染本地源 URL、@asset:... 解析后的 URL、data:/blob: URL |
IntersectionObserver、ResizeObserver、matchMedia、requestAnimationFrame | 标准布局/动画原语 |
crypto.randomUUID、crypto.subtle | 客户端状态的哈希和 ID 生成 |
WebAudio(AudioContext) | 轻量音频合成或分析 |
Notification、navigator.vibrate、screen.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)