APIリファレンス
カードの画面コードで使えるものの一覧です。サンドボックスのグローバル、コンポーネント、
useYumina()の全フィールドとメソッド、型定義、ブロックされたブラウザAPIの代わりになるもの。
画面コードは パネル → フロントエンドコード にあります。入門としてはまず カスタムUIガイド を読んでください。このページはシグネチャを調べるためのものです。
サンドボックスのグローバル
次の名前は、画面コードのどこからでも import なしで使えます。
| 名前 | 種類 | 内容 |
|---|---|---|
React | module | React 一式(useState、useEffect、useRef、useMemo、useCallback、useLayoutEffect、Fragment、…) |
useYumina | hook | プラットフォームSDK。useYumina() SDK を参照 |
useAssetFont | hook | アセットライブラリからカスタムフォントを読み込む。useAssetFont() を参照 |
Icons | object | Lucide のすべてのアイコンをコンポーネントとして使える:<Icons.Heart />、<Icons.Sword />。一覧:https://lucide.dev/icons |
Chat | component | チャット一式の部品。<Chat> を参照 |
MessageList | component | 入力欄なしのメッセージ一覧。<MessageList> を参照 |
MessageInput | component | 入力バーだけ。<MessageInput> を参照 |
ChatCanvas | component | <Chat /> の古い別名。<ChatCanvas> を参照 |
ModelTrigger、ModelPickerModal | component | チャットのモデルボタンとモデル選択。モデルピッカーのコンポーネント を参照 |
SessionMemoryModal | component | チャットのセッションメモリーのダイアログ。モデルピッカーのコンポーネント を参照 |
LoreSlot、LoreButton、LorePanel、LoreGroup、LoreSwitch | component | オンのあいだだけロアブックのエントリを AI に送るコントロール。ロアのコントロール を参照 |
exports、module | object | CJS 形式のエクスポートの予備。ふつうは気にしなくてかまいません |
React や上の名前は import しないでください。 サンドボックスが注入します。import 文はコンパイル時に取り除かれるので、import { useState } from "react" と書くと useState は未定義のままになります。React.useState を使ってください。
自分のファイルは 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> | セッション単位のゲーム変数。キーは変数の ID です。表示名 でも読めます(ID が UUID でも、api.variables.hunger で hunger という名前の変数が見つかります)。例:{ health: 80, gold: 150 }。ID と表示名 を参照 |
globalVariables | Record<string, unknown> | プレイ中は variables と同じ値が入っています。ID でも名前でも読めるのも同じです |
worldName | string | 今のカードの名前 |
worldCover | string | null | カードのカバー画像の絶対URL。未設定なら null。カバーを変えると自動で追従します。キャラクターのアバター、ヘッダー、スプラッシュに使うときは、カバーをアセットとしてアップロードし直す必要はありません |
background | { id, url, blur, dim, opacity, position } | null | ホストがデフォルトのチャットの背後に描く画像。解決済みの値です(url は最終的なURL、blur は px、dim と opacity は 0〜1、position は "center" | "top" | "bottom")。カードに背景がなければ null。全画面の画面コードは、同じ背景を描いても無視してもかまいません |
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 がペルソナのアバターになり、そうでなければアカウントの情報になります。ワールド内のチャットの吹き出し、キャラクターカード、プロフィールパネルにはこちらを使います |
mode | "session" | "guest-preview" | "session" は実際のプレイセッション。"guest-preview" はログアウト状態でのハブのプレビューで、状態を変える操作は何もせず、親にログインを促す表示を出します |
capabilities | { canSendMessage, canPersistSession, canUseSessionApis, requiresAuth } | 今の mode で何ができるか。押しても何も起きないボタン(ゲストプレビューの送信ボタンなど)を無効にしたり、「ログインして続ける」の案内をその場に出したりするときに読みます |
language | string | ホストで使われている言語コード("en"、"zh"、…)。ホストの i18next に頼らずに、カードの中で訳を選ぶときに使います |
messages | Array<Record<string, unknown>> | メッセージ履歴(読み込み済みの範囲。hasEarlierMessages を参照)。SandboxMessage を参照 |
hasEarlierMessages | boolean | サーバーにまだ読み込んでいない古いメッセージがある。loadEarlierMessages() で取得します |
isLoadingEarlier | boolean | 古いページを取得しているあいだ true |
isStreaming | boolean | AI が返信を生成しているあいだ true |
streamingContent | string | AI からストリーミング中のテキスト(頻繁に更新されます) |
streamingReasoning | string | AI からストリーミング中の「思考」/推論テキスト(推論モデルのみ) |
pendingChoices | string[] | ルールが出した選択肢ボタンのラベル |
error | string | null | 今のエラーメッセージ(API の失敗、生成エラー)、なければ null |
errorCode | string | null | error の機械向けコード(例:"CONTENT_FILTER")。不明なら null |
sendFailureNonce | number | 送信が最終的に失敗するたびに 1 増えます。トーストだけ出して error を設定しない失敗も含みます。これを見張っておけば、プレイヤーの文章を自作の入力欄に戻せます |
composerSendKey | "enter" | "mod-enter" | プレイヤーの「Enter キーで送信」の設定。"enter":Enter で送信、Shift+Enter で改行。"mod-enter":Ctrl/⌘+Enter で送信、Enter で改行。自作の入力欄もこれに合わせてください |
readOnly | boolean | 他人のセッションを見ているときは true。<Chat /> は入力欄を自動で隠します。ホストが最初の状態を送ってくるまでは true です |
checkpoints | Array<Checkpoint> | 保存済みのチェックポイント。Checkpoint を参照 |
greetingContent | string | null | カードのエントリから計算されたオープニングの文章(<Chat /> が空のときの表示に使います) |
canvasMode | "chat" | "custom" | "fullscreen" | 今のキャンバスモード |
speakerBubbles | boolean | undefined | カードの 返答の処理 で 話者ごとに吹き出しを分ける がオンになっている |
selectedModel | string | 選択中の AI モデルの ID |
userPlan | string | ユーザーのサブスクリプションプラン("free"、"go"、"plus"、"pro"、"ultra"、"internal") |
preferredProvider | "official" | "private" | 公式 API か、ユーザー自身のキーか |
balance | number | null | プレイヤーのマッシー残高。不明なら null |
entries | ReadonlyArray<SandboxEntry> | カードのロアブックのエントリ。有効なものだけで、position 順に並んでいます。ロアブックの検索 と SandboxEntry を参照 |
worldbooks | ReadonlyArray<Worldbook> | カードのシチュエーション(id、name、description?、enabled?、activation、…)。SandboxEntry.worldbookId はこの ID を指します |
loreUiBindings | ReadonlyArray<LoreUiBinding> | 各 <LoreSlot> の id がどのエントリを送るか。ロアのコントロール を参照 |
ID と表示名
変数には 2 つの呼び名があります。
- ID:ゲーム状態が実際に保存されているキー。新しい変数には、変えるまで
a8c5a685-1317-40b2-8f11-baccd39f9042のようなランダムな UUID が付きます。 カードにある実際の ID を使ってください。インポートしたカードや古いカードでは、大文字と小文字が混ざった ID が残っていることがあります。 - 表示名:あなたが付けた名前(
hunger、Love meter、zombieKills)。
api.variables のキーは ID ですが、表示名でも引けます。
api.variables.hunger // works whether "hunger" is the ID or the display name
api.variables["Love meter"] // display names with spaces work too
api.setVariable("hunger", 50) // writes to that variable's ID両方に当てはまるときは ID が優先です。ある変数の ID が hp で、別の変数の表示名も hp なら、api.variables.hp は ID が hp の方を読みます。 同じ表示名の変数が複数あるときは、エンジンの名前検索と同じく、最後に定義されたものが使われます。 紛らわしくならないよう、名前は重ならないようにしましょう。
Object.keys(api.variables) が返すのは保存されているキーだけで、表示名の別名は含まれません。 中身を列挙しても、別名の分だけ行が重複することはありません。
ゲームアクション
| メソッド | 何をするか |
|---|---|
sendMessage(text, attachments?) | プレイヤーとしてメッセージを送り、AI の返信を起こします。attachments は pickChatImage() で得た画像の配列(任意)。送りっぱなしです |
pickChatImage() | チャットに添付する画像を、ホストの画像選択で選ばせます。{ type: "image", mimeType, name, data }(data は base64)を返し、キャンセルされたときや使えないときは null。結果は sendMessage(text, [image]) に渡します |
setVariable(id, value, options?) | 変数を設定します。id は変数の ID か表示名。送りっぱなしで、値はすぐ api.variables に反映され、保存は裏で行われます。options({ scope?, targetUserId? })は古いカードのために受け付けますが、ホストは無視します |
patchVariables(values) | 複数の変数を 1 回で保存します。values は { variableId: value, ... } で、キーは ID(ここでは表示名は引きません)。Promise<void> を返し、サーバーが書き込みを確認したら解決、失敗したら(読み取り専用の表示、セッションが変わった、サーバーエラー)reject します。次の処理が「値が保存されていること」を前提にするときに使います |
executeAction(actionId) | 発動条件が プレイヤーがボタンを押した で、その ボタン 名が一致するビヘイビアを動かします(例:"attack")。送りっぱなしで、値は渡せません。値を渡すなら executeActionAndWait を使います |
await executeActionAndWait(actionId, params?) | 同じことをして、結果を待ちます。値も渡せます。params は { price: 5, item: "Mushroom ale" } のような普通のオブジェクトで、ビヘイビアの側では条件の値、効果の値、お知らせの中で {param.price} として読めます(ビヘイビア · ボタンから値を渡す)。値はテキスト、数値、true/false のどれかで、30 個まで、名前は 60 文字まで、テキストは 500 文字まで。ホストの状態、実行時の記録、通知、オーディオの効果がすべて反映されたあとに { applied: true, variables, firedIds } で解決します。失敗、タイムアウト、使えない表示(ゲストプレビュー、読み取り専用、リプレイ)、空や文字列でない ID、古いセッションでは reject します。variables はサーバーが確認したスナップショットで、それより新しい先行書き込みは api.variables に残ります |
callAi(ai, input?) | カードの AI を名前か ID で動かします。ふつうは 回答形式 を持つ UI 型 の AI です。input には渡したいものを何でも(文字列か小さなオブジェクト)。{ text, fields, fallback } で解決します。text はその AI のせりふ、fields は埋めた値、fallback は失敗して予備のせりふが使われたとき true。動かせなかったとき(ここではプレイ中でない、クールダウン中、処理中、この表示からは AI を呼べない)は null で解決します。せりふと値の行き先は 回答形式 で決まり、ストーリーへ にしていればそのせりふはチャットにも追加されます |
onAiOutput(channel?, cb) | 画面のチャンネル に送られた言葉を受け取ります。送り主は、せりふの行き先 が 画面のチャンネル の AI か、画面のチャンネル に送る設定の 返答の処理 のルールです。cb({ channel, id, name, text, fields })。チャンネル名を渡すとそのチャンネルだけ、cb だけを渡すとすべてのチャンネルを受け取ります。返答の処理のルールからのときは、id が "reply"、name がルール名、fields が {} になります。購読解除の関数を返します |
setScene(scene, options?) | 画面に今何が映っているかを AI に伝えます。scene は短いテキストか小さなオブジェクト({ room: "kitchen", onCamera: false })で、null で消えます。これ以降のすべての AI 呼び出し(返信、再生成、裏で動く AI)に、専用のブロックとして渡されます。保存はされません。options.events は [{ name, when? }] で、AI が [event: name] と書いて起こしてよいイベントです |
onStoryEvent(cb) | ストーリーイベントを受け取ります。AI が [event: name] と書いたときと、ストーリーイベント に送る設定の 返答の処理 のルールからです。cb({ id, name })。実際のセッションでだけ発火します。購読解除の関数を返します |
switchGreeting(index) | 最初のメッセージを index 番目のオープニングに切り替えます(0 = 最初のオープニング)。切り替えが反映されたら解決する Promise<void> を返します。切り替えると変数がそのオープニングの開始値に戻るので、自分で変数を設定するのは await した あと にしてください |
clearPendingChoices() | 選択肢ボタンを、どれも選ばずに消します |
setComposerDraft(text) | text をチャットの入力欄に入れてフォーカスします。送信はしません。 送る前にプレイヤーに確認・編集してほしいときに使います(会話の切り出しを用意する NPC 対話ボタンなど)。サンドボックス内だけで完結し、親とのやりとりはないので、同梱の <MessageInput> / <Chat> と組み合わせたときだけ動きます |
// A shop button that runs the "buy" behavior with this item's price
async function buy() {
try {
await api.executeActionAndWait("buy", { item: "Mushroom ale", price: 5 })
} catch (e) {
api.showToast("Couldn't buy that right now", "error")
}
}
// A fortune-teller button that calls a UI-based AI
async function askFortune() {
const answer = await api.callAi("Fortune Mushroom", { question: "Will I find the key?" })
if (answer) setFortune(answer.fields.Fortune)
}チャット制御
デフォルトのチャットバーでできることを、カスタムUIからもできるように公開したものです。
| メソッド | 何をするか |
|---|---|
editMessage(messageId, content, options?) | 既存のメッセージを編集します。Promise<boolean> を返し、成功すれば true。options.swipeIndex には編集を開いたときのスワイプを指定します。保存の時点で別のスワイプが表示されていたら、保存は拒否されます(false) |
deleteMessage(messageId) | メッセージを削除します。Promise<boolean> を返します |
regenerateMessage(messageId) | 指定した返信を AI に作り直させます(送りっぱなし) |
continueLastMessage() | 最後の AI メッセージの続きを生成します(送りっぱなし) |
stopGeneration() | 今のストリームを止めます(送りっぱなし) |
restartChat() | すべてのメッセージを消して状態をリセットし、最初からやり直します |
swipeMessage(messageId, "left" | "right") | メッセージの AI の別案(スワイプ)を切り替えます。Promise<Record<string, unknown>> を返します |
loadEarlierMessages() | 履歴の前のページを取得し、messages の先頭に追加します。Promise<boolean> を返します |
セッションとブランチ
| メソッド | 何をするか |
|---|---|
revertToMessage(messageId) | 会話を messageId の直前まで巻き戻します。Promise<void> を返します |
branchFromMessage(messageId) | 指定したメッセージで新しいセッションに分岐します(そのメッセージまでのメッセージと状態のスナップショットを複製)。Promise<string | null> を返し、新しいセッション ID か、失敗したときは null(ストリーミング中、マルチプレイのルーム、メッセージが見つからないときは失敗します) |
getBranchContext() | 今のブランチの周辺(自分、親、兄弟、子)を取得します。Promise<BranchContext> を返します。毎回取り直し、クライアント側のキャッシュはありません。BranchContext を参照 |
createSession(worldId) | ワールドの新しいセッションを始めます。新しいセッション ID の Promise<string> を返します |
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> を返します |
オーディオ
trackId はトラックの 音声トラック ID です。パネル → オーディオ でトラックをクリックし、ID をコピー を使います。トラックの名前を変えても ID は変わりません。
| メソッド | 何をするか |
|---|---|
playAudio(trackId, opts?) | カードのオーディオトラックを再生します。opts:{ volume?, fadeDuration?, chainTo?, maxDuration?, duckBgm?, loop? }。時間は 秒 単位です。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)をその場で返します |
音声読み上げ(TTS)
プラットフォームの音声パイプライン(OpenRouter 経由の Fish Audio)で文章を読み上げます。料金は文字数に応じてプレイヤーにかかり、同じ文章と同じ声の組み合わせを再生し直すときはキャッシュが使われて無料です。同時に流れる声はひとつだけです。文章の中に [whisper] / [excited] のようなタグを入れると、読み方を指定できます。
読み上げは オプトイン です。プレイヤーが 設定 › 表示 でオンにします。オンにするまでは ttsState.enabled が false で、tts.speak / tts.preview は合成も課金もせずに { ok: false, reason: "disabled" } で解決し、tts.setPrefs は何も変えません。enabled が false のあいだは、音声まわりの UI を隠してください。
| メソッド / フィールド | 何をするか |
|---|---|
tts.speak(opts) | opts:{ messageId?, text?, key?, voice?, waitForEnd? }。チャットのメッセージを読むなら messageId を渡します(プレイヤーの読み上げモードの設定に従います)。カードの任意のせりふなら text を渡します(key で再生に一定の識別子を付けられます)。voice を指定すると、プレイヤーの声の代わりに fish.audio マーケットプレイスの id(32 桁の16進数)を使えるので、カードが自分のキャラクターに声を当てられます。waitForEnd: true のときは、すべての音声クリップが実際に終わったら成功で、キャンセル、再生のブロック、メディアエラー、タイムアウトは ok: false を返します。指定しないときは、再生がキューに入った時点で成功です。Promise<{ok, reason?}> を返します。reason は "disabled"(プレイヤーが読み上げをオンにしていない)、"unavailable"(プレビュー/リプレイ)、"insufficient"(マッシー不足)、"rate-limited"、"empty"、"timeout"、またはエラー |
tts.stop() | 今の読み上げを止めます(待機中の合成と、自動読み上げのキューも取り消します) |
tts.onPlaybackFrame(cb) | せりふの再生中、実際の出力のフレーム(毎秒約 15 回)を購読します。フレームの key を、自分が再生したせりふと照らし合わせてください。購読解除の関数を返すので、アンマウント時に解除します |
tts.setPrefs(prefs) | プレイヤーの読み上げの設定を更新します:{ enabled?, autoPlay?, mode?, voice?, voicePool?, volume? }(mode は "full" か "dialogue"、volume は 0〜100、voicePool は AI の配役がキャラクターに割り当ててよい声の id の一覧で [] ならすべて、voice は 1 つだけのプールを設定し "" ならすべて)。アカウントに保存され、新しい値は ttsState に戻ってきます。プレイヤーがオプトインしたあとでだけ効きます。enabled: true は無視され(読み上げをオンにできるのは設定画面だけです)、enabled: false はオフにします |
tts.preview(voice) | 声の短いサンプルを再生します("" = 自動の声)。初めて聞くときは通常の合成と同じく課金され、その後は全員にキャッシュされます。Promise<{ok, reason?}> を返します |
ttsState | 読み取り専用:{ available, enabled, voice, voicePool, mode, autoPlay, volume, playback }。available は実際のセッションの外(ゲストプレビュー、リプレイ)では false で、そのときは音声の UI をすべて隠してください。enabled はプレイヤーのオプトイン(設定で読み上げをオンにするまで false)。playback は { key, status: "loading" | "playing" | "blocked", progress?, level? } か null です(progress は 0〜1 で約 2Hz)。playing は実際の再生に合わせて変わります。waitForEnd の会話では、実際の出力の level(0〜1、約 15Hz)が入ることがあり、対応していない環境では入りません。カスタムUIでせりふごとの話者の状態や進捗リングを描けます |
音声入力(長押しで話す)
プレイヤーが文字を打つ代わりに話せるようにします。録音はプラットフォームが行い(カードがマイクに触れることはありません)、言葉にして返します。プレイヤーには無料です。デフォルトのチャットの入力欄にはもうマイクがあるので、これらは人狼ゲームの発言台のような、自分用の話すボタンを作るときに使います。
| メソッド / フィールド | 何をするか |
|---|---|
voice.prepare(opts?) | ブラウザのマイクの許可を確認し、録音も文字起こしもせずにマイクを解放します。自動の無音検出を使うなら { requireLevels: true } を指定します。音量の解析が動かせないときは { ok: false, reason: "levels-unavailable" } を返します。お金のかかる音声処理の前に呼んでおけば、マイクがブロックされていても会話が止まったままになりません。voice.cancel() で取り消せます。 |
voice.record(opts?) | 録音を始めます。話すボタンが押されたときに呼びます。自動の無音検出を使うなら opts.requireLevels: true も指定します。普通の長押しで話す使い方なら省略できます。opts.onLevel(level) には入力の音量(0〜1、毎秒約 15 回)がリアルタイムで渡されるので、波形の表示に使えます。voice.stop() のあとに { ok: true, text } か { ok: false, reason } で解決します。reason は "cancelled"、"too-short"(長押しでなくタップだった)、"denied"(マイクがブロックされている)、"empty"、"rate-limited"、"unavailable"(プレビュー/リプレイ)、"timeout"、"error" のどれかです。AI の声が録音されないよう、読み上げは一時停止されます |
voice.stop() | 録音を終えて文字起こしします(話すボタンを離したとき) |
voice.cancel() | 録音を捨てます |
voice.setPrefs(prefs) | プレイヤーの音声入力の設定を更新します:{ enabled?, mode?, key? }。mode は ""(カードに従う)、"confirm"、"auto" のどれか。key は押し続けるキーの KeyboardEvent.code です |
voiceInputState | 読み取り専用:{ available, enabled, mode, cardMode, playerMode, key }。mode はプレイヤーが離したときにどうしたいかで、"auto"(話したとおりに送る)か "confirm"(確認してから)。プレイヤーが上書きしていなければ、カードの settings.voiceInputMode です。key は話すときに押し続ける KeyboardEvent.code です |
カードの初期設定は カードの設定 → 音声 → プレイヤーの音声入力 で決めます。確認してから が "confirm"、すぐ送信 が "auto" です。ワールドの JSON では settings.voiceInputMode: "auto" | "confirm" です。
連続ライブ音声(OpenAI Realtime)
api.realtimeVoice は、編集可能な保存済みセッションの中で、マイクを使った途切れない会話を提供します。実際に使えるか、誰が費用を出すかは getConfig() で確かめてください。オブジェクトがあるだけでは使える保証になりません。ホストは録音を始める前に、マイクの使用について明示的な同意を求めます。費用は、保存した OpenAI キー、有効にしたテスト用スポンサー枠、または明示的に有効にした Yumina の残高でまかなえます。残高を使うときは、同意の画面にサーバーの一時的な確保額と、実際の使用量に応じた請求が表示されます。マイクの音声とワールドのコンテキストは OpenAI に送られ、文字起こしは物語に保存されることがあります。カメラは要求しません。永続的な受領記録と再読み込み後の復旧はネイティブ残高音声の仕様(英語)を参照してください。
| メソッド | 取り決め |
|---|---|
realtimeVoice.getConfig() | 引数なし。費用も録音も発生しません。残高通話では transport: "server-ws-v1"、funding: "balance"、サーバーの reservationCredits、finishAcknowledged: true を返し、旧方式では false を返します。無効な表示では利用不可を返します。 |
realtimeVoice.finish() | 引数なし。残高通話ではまず手元で音を止め、この試行の永続的な最終記録を取りきってから finished を返します。pending、incomplete、unavailable、アトミックな not-started はそれぞれ別の結果です。pending には最終カーソルがありません。旧方式は unsupported を返し、stop の動作は変わりません。 |
realtimeVoice.prepare({ avatar? }?) | プレイヤーのクリックの中で、時間のかかる処理(コンテキストの組み立て、保存)より前に呼びます。{ intent } で解決するので、その intent を start に渡します。この表示で音声が使えないときは reject します |
realtimeVoice.start({ instructions, intent?, context?, voice?, avatar?, tools?, interruptionMode? }) | { status: "connected" } で解決します。instructions は 1〜12,000 文字。context は updateContext と同じ形を受け付けます。voice は "marin"(デフォルト)か "cedar"。gpt-realtime-2.1、中程度のセマンティック VAD、ホスト管理の応答を使います。interruptionMode は "automatic"(デフォルト)か "manual" で、不正なモードは録音前に reject します。tools は [](確認ツールなし)か ["request_inspection_focus"] で、省略するとデフォルトのままです。avatar: true を指定すると、ホストに設定されたテスト用の LiveAvatar サービスが必要になり、そのメディア接続を待ちます。使えないときは目に見える形で reject します。reject されたらプレイヤーに知らせてください。 |
realtimeVoice.interrupt() | 残高通話では今の出力をすぐに捨て、実際に再生した位置を送ります。録音はそのまま続きます。旧方式は入力のクリアと話す番の取り方を従来どおり行います。通話は切れません。 |
realtimeVoice.stop() | ホスト側の録音と再生をすぐに閉じ、費用の出る通話ではサーバーに切断を要求します。手元で音が止まることと、終了が確認されることは別です。 |
realtimeVoice.setMuted({ input, output }) | 録音と再生を別々にミュートします。アシスタントが話していても、マイクはミュートされません。 |
realtimeVoice.updateContext(context) | 見聞きしている場面のコンテキストを更新します:文字列か { state: Record<string, string>, events: Array<{ id: string, text: string }> }。平たくしたテキストが 4,000 文字に収まる必要があります。 |
realtimeVoice.updateInstructions(text) | 旧方式では、静かなターンの区切りで最初の指示を置き換えます(1〜12,000 文字)。残高通話の指示は固定で、呼ぶと致命的でない「未対応」の通知が出るだけです。残高通話の updateContext は見えた更新をまとめ、リビジョン付きの pending/configuring/applied の確認を返します。 |
realtimeVoice.reactToScene(notice) | すでに確定した公開の場面イベントを、観察として送ります。そのイベントのアクションは実行しません。下の決められた通知を参照してください。非公開のノートやページの文章は絶対に入れないでください。 |
realtimeVoice.cancelSceneReaction(id?) | 待機中または再生中の通知を ID で取り除きます。ID を省くと未処理のフォローアップだけを取り消し、物理的なイベントのアナウンスは残します。通話は止めず、マイクの入力も消しません。 |
realtimeVoice.resolveTool(callId, { accepted, focus?, reason? }) | 受け付ける前に、ワールドが範囲の決まった確認リクエストを検証します。プロバイダーがコードを実行したり、状態の効果を適用したりすることはありません。 |
realtimeVoice.setSpatial({ x, z, yaw, sourceX, sourceZ }) | 空間再生の位置と向きを更新します。 |
realtimeVoice.onEvent(callback) | 購読します。購読解除の関数を返します。 |
アバターモードでは、AI の音声がサーバーの選んだ LiveAvatar を通り、同期して返ってきた音声だけが再生されます。Web カメラは使わず、マイクの音声を LiveAvatar に直接送ることもありません。ホストは video-status イベント(connecting、live、stopped)と、ImageBitmap を含む video-frame イベントを追加します。各ビットマップは、イベントリスナーの中で 同期的に 自分の canvas へコピーしてください。サンドボックスはその直後にビットマップを閉じて、受け渡しの完了を通知します。ビットマップを持ち続けたり、フレームを React の state に入れたり、保存したりしないでください。video-status: stopped で静止画に戻します。セッションのトークン、割り込み、空間再生、フレームの流量制御、後片付けはホストが受け持ち、カードがアバターの認証情報や接続先を自由に選ぶことはできません。アバターの再生状態は返ってくるストリームに合わせて変わるので、AI の生成が終わっても、画面上の話す番が途中で終わることはありません。
プレイヤーがはっきり操作しない限り、キャラクターにせりふを最後まで言わせたいときは、手動の割り込みを使います。
// In the click handler that starts the call:
const { intent } = await api.realtimeVoice.prepare();
await api.realtimeVoice.start({ intent, instructions: "Your character direction.", interruptionMode: "manual" });
// In the player's Interrupt button/key handler:
api.realtimeVoice.interrupt();手動モードでは、せりふとせりふのあいだもマイクは開いたままです。生成待ち、アバターの音声待ち、聞こえている出力やそのエコーの残りのあいだに始まった、または続いていた入力は、最終的な音声認識の結果があとから届いたり、キャラクターの言葉と違っていたりしても、ユーザーの文字起こしイベントとネイティブの会話履歴から除外されます。想定どおりに保護された入力では、言葉が重なったというヒントは出ません。ネイティブの削除の確認は、引き続きそのあとの返答を区切ります。明示的な割り込みは、入力バッファのクリアが確認されたあとでだけ新しい録音を受け付けるので、その区切りのあとでもう一度話してください。新しい出力のターンより前に終わった入力は、これまでどおりに扱われます。ミュート、一時停止、切断は、録音と再生に対してふだんどおりに効きます。このオプションが変えるのは受け付けの方針で、マイクのハードウェア、VAD、声、音響モデルは変わりません。
イベントには status(connecting、connected、stopped、error)、transcript(role、一定の id、累積の text、final)、範囲の決まった確認の tool リクエストがあります。input は文字起こしの ID と speaking、transcribing、discarded を使います。discarded では送信待ちの発言の状態を消してください。そうしないと、文字起こしに失敗したときに送信がいつまでも待ったままになります。activity は listening、thinking、speaking で、speaking は生成の完了ではなく、実際の出力バッファの再生に合わせて変わります。input-level はマイクの入力の音量(0〜1)、level は出力の音量(0〜1)です。それぞれの話し手に合ったメーターを使ってください。input-hint はプレイヤー向けの短いメッセージ、capture-settings はブラウザが適用したエコーキャンセルの種類を伝えます。
通話は、プレイヤーがその場を離れたとき、タブを隠したとき、許可を失ったとき、明示的に止めたとき、5 分に達したときに終わります。スポンサー枠のテストでは、さらにサーバー側の切断、アカウントごとに同時 1 通話、直近 24 時間での開始回数の上限(プレイヤーごとに 12 回、全体で 60 回)がかかります。この API は、長押しで話す api.voice や読み上げの api.tts とは別物です。
音声の出力は、同意のクリックの中でロックが解除され、マイクの録音前に確認されます。再開の失敗、停止状態のまま、2 秒のロック解除のタイムアウトのどれかが起きると、start は reject してリソースを閉じます。これで確かめられるのはブラウザ側の出力の準備だけで、ハードウェアの音量はプレイヤーが操作します。
場面の通知には id(1〜160 文字の英字、数字、アンダースコア、コロン、ハイフン)と kind があり、それぞれ次の追加フィールドをちょうど持ちます。
| kind | 追加フィールド |
|---|---|
return、seen、overdue、writing、caught、knock、power-restored、clearance | なし |
bulletin | text:空でない放送の文章。600 文字まで |
power-cut | seconds:3 から 12 までの有限の数。観察者はそのあいだ見えなくなる |
begin-inspection | focus:desk、door、bed のどれか。line:空でない公開のアナウンス。240 文字まで |
search | focus:desk、door、bed のどれか |
follow-up | evidenceId:それより前の、住人の公開の発言の ID。line:空でない質問ひとつ。240 文字まで |
余分なフィールドがあると拒否されます。通知は接続ごとに重複が取り除かれ、受け付けるのは 10 秒あたり 8 件、ID は 128 個までです。出力をミュートしているあいだは新しい通知が抑えられます。話している最中の音声や待機中の応答は、ふだんの順序のままです。放送の文章は公開の放送として読み上げられ、それ以外のイベントには、その確定したイベントへの短い反応が返ります。ネイティブの start の instructions は引き続き 12,000 文字まで、updateContext は 4,000 文字までです。
start({ instructions, context }) も同じコンテキストの形を受け付けます。物理的な場面が変わっていくときは、構造化したスナップショットを使ってください。一定の state のキーで今の公開の事実を表し、変わらないイベント ID で記録された観察を識別します。事実が変われば、古い値は新しい値に置き換わります。イベントを省いたときは、履歴の容量の都合で外されたという意味で、起きなかったという意味ではありません。訂正した証言や新しい証言に同じ ID を使い回さないでください。この役者が知ってよい情報だけを入れてください。非公開のページや見えていない行動を、この API が代わりに取り除くことはありません。
ホストは、接続時と再接続時にスナップショット全体を送ります。ホストが有効にした GPT-Live の経路では、そのあとのスナップショットは、変わった事実とまだ届けていないイベント ID だけを、再生の静かな区切りで送ります。待機中のイベントは state がまとめられても消えず、抜粋を変えても古いイベントが再送されることはありません。古い文字列の形と Realtime の経路では、引き続きコンテキスト全体を置き換えます。上限:state のフィールド 16 個、スナップショットあたりイベント 64 件、平たくして 4,000 文字、起動時の instructions とコンテキストの区切りと本文の合計で 12,000 文字。キーと ID は転送用のメタデータで、話す内容ではありません。待機中のイベントや接続の履歴の容量を超える送り手があると、イベントを黙って失うのではなく、目に見える形で通話を終わらせます。
SDK が渡すのは通知の ID と kind だけです。unperson-room 変数のない既存のカードは、文章を含まない固定の return、seen、overdue、writing、caught の通知を送り続けてかまいません。これらはカードが報告した観察で、ホストがそれを物語の中の事実として確定させるわけではありません。Unperson room のスキーマでは、ホストが確認済みの保存状態から、対応するすべてのイベントを組み立てます。SDK に文章を渡したからといって、証拠をでっち上げてよいわけではありません。フォローアップは、同じ試行の中でそれより前に受け取った住人の発言を引用する必要があります。ルーム内の時間で 45 秒たつか、フェーズや試行が変わるか、より新しい発言を受け取ると期限切れになります。マイクから新しい発言が入ったときも、未処理のフォローアップはすぐ無効になります。ほかのルームのスキーマで、放送、確認、証拠にひもづくフォローアップのような内容のある通知を参照するには、ホスト側のアダプターが必要です。
UI/ナビゲーション
| メソッド | 何をするか |
|---|---|
toggleImmersive() | 没入(全画面)モードを切り替えます |
openPersonaManager() | プレイヤーのペルソナ管理を開きます。ワールドを離れずにペルソナを切り替え / 作成 / 編集できます(入力欄の「+」メニューから開くのと同じパネル)。プレイ中に切り替えると、このセッションに保存されたペルソナが変わり、次のメッセージから反映されます |
getPersonaProfile() | プレイヤーが取り込むことを選んだときに、セッションで選ばれているペルソナを読みます。Promise<{ name, appearance, personality, backstory, entries? } | null> を返します(entries は [{ title, content }])。ペルソナが選ばれていなければ null、失敗したら reject します。非公開のメモは含まれません。プレイ中ずっと同じ人物像を保ちたいなら、自分のデータにコピーを保存してください |
openModelPicker(options?) | チャットのモデル選択を開きます。自分の ai.complete 呼び出し用に選ばせるときは { purpose: "side-completion" } を渡します。そのときはチャット専用のお試しモデルが外されます |
openSessionManager() | セッション / ブランチの管理を開きます(ブランチの切り替え、名前の変更、削除) |
sharePlaythrough() | このセッションをプレイ記録として共有するダイアログを開きます |
openSupport() | 「クリエイターを支援する」ダイアログ(ドルでのチップ、またはマッシーの贈与)を、プレイ中のワールドにひもづけて開きます。全画面のカスタムUIのカードでは、カード自身の画面がプレイ画面のヘッダーのハートボタンを覆ってしまうので、そのためのものです。Promise<{opened, reason}> を返します。reason は "self"(自分が作者)、"signed-out"、"unavailable"(エディタのプレビュー)のどれかです。await して reason を表示してください。最初にこのボタンを押すのは自分のカードを試している作者本人で、何も起きないと壊れているように見えます |
openCreditTopUp() | マッシーのチャージ画面を開きます |
fetchAsset(ref) | 自分のアセットを生のバイト列として読みます。引数はアセット ID("@asset:<uuid>" か uuid だけ)で、URL は使えません。Promise<{ok, bytes?, contentType?, error?}> を返します。error は "bad-ref"、"http-404"、"too-large"(上限 32MB)、"unavailable"、またはネットワークのメッセージです。サンドボックスは自分では何も取得できず、ブラウザが特別な読み込み経路を用意しているのは画像とメディアだけです(<model> タグはありません)。そのため .glb、大きな JSON の表、スプライトアトラスを読み込むには、この方法しかありません。bytes に触れる前に ok を確認してください。エディタのプレビューでも動きます |
copyToClipboard(text) | クリップボードにコピーします(navigator.clipboard.writeText の代わり) |
navigate(path) | 親に "/app/hub" のようなパスへの移動を頼みます(window.location = ... の代わり) |
showToast(message, type?) | 親の UI にトーストを出します。type:"success"、"error"、"info"(デフォルト) |
ブラウザ内ストレージ(ワールド単位)
localStorage の代わりです。worldId ごとに分かれていて、ワールド同士でキーを読み合うことはできません。端末内の設定やキャッシュのためのもので、デバイス間で同期されず、チェックポイント、ブランチ、共有スナップショットにも入りません。ブラウザの sessionStorage の別名もこのローカルの API を使います。下の api.sessionStorage とは別物です。
| メソッド | 何をするか |
|---|---|
storage.get(key) | 読み取り。Promise<string | null> を返します |
storage.set(key, value) | 書き込み(文字列のみ)。Promise<void> を返します |
storage.remove(key) | 削除。Promise<void> を返します |
複雑なデータは、出し入れのときに JSON.stringify / JSON.parse してください。
セッションのクラウドJSONストレージ
api.sessionStorage は、プレイセッションに属し、デバイスをまたいでついてきてほしい小さな JSON のレコードに使います。値は AI のゲーム変数とは別に保存されます。すべてのメソッドは { value, version, exists } を含む Promise を返します。一度も書いていないキーは { value: null, version: 0, exists: false } を返します。削除したキーにもバージョンが残るので、null だから存在しないと決めつけず、exists を確認してください。
| メソッド | 何をするか |
|---|---|
sessionStorage.get(key) | 今のレコードを読みます。共有リプレイでは固定されたレコードを読みます |
sessionStorage.set(key, value, { expectedVersion }) | 今のバージョンが一致したときだけ JSON の値を保存します。オプションは必須です |
sessionStorage.remove(key, { expectedVersion }) | 値を削除し、バージョン付きの削除記録を残します。オプションは必須です |
const previous = await api.sessionStorage.get("journal");
const saved = await api.sessionStorage.set(
"journal", { notes: ["Reached the village"] },
{ expectedVersion: previous.version }
);
// Keep saved.version for the next change, including remove().SESSION_STORAGE_CONFLICT が返ったら、プレイヤーの下書きを残したままレコードを読み直し、変更をすり合わせてから送り直してください。古い値のまま闇雲に再試行しないでください。保存済みと表示するのは、成功の応答を待ってからにします。api.readOnly に従ってください。共有リプレイからは書き込めません。読み書きには実際のセッションか、許可された共有リプレイが必要で、エディタやゲストのプレビューでは使えません。
キーは 1〜128 文字の ASCII の英字、数字、_、.、:、- で、先頭は英字か数字です。上限は、直列化した UTF-8 の JSON で値ごとに 32 KiB、セッション内の現在の値の合計で 256 KiB、削除記録を含めて 128 キー、セッションごとの値の履歴で 16 MiB、持ち主のすべてのセッションを合わせて 1 時間に 1,000 回の変更です。レコードは小さく保ってください。ファイル置き場ではありません。
JSON を変えるチェックポイントの復元も、同じ容量と頻度の上限を共有します。16 MiB の履歴の上限やほかの上限を超えると HTTP 413、1 時間あたりの変更回数を超えると 429 が返ります。そのときは画像やストーリーの状態も含めて復元全体が取り消され、途中までの復元にはなりません。既存の履歴は残ります。値が変わらないときは、復元で履歴が重複することはなく、これらの JSON の上限に達していても進められます。
JSON と画像の参照は、チェックポイント、同じアカウント内のブランチ、共有スナップショットにまとめて記録されます。チェックポイントを復元すると両方が戻ります。非公開のライブセッションのデータに触れられるのは持ち主だけで、ワールドの作者であってもアクセスできません。共有スナップショットは、既存の公開範囲、ワールドの公開状態、非表示・モデレーションの状態、コンテンツレベルのルールに従います。そのルールのもとでは、共有した人とカードの作者はスナップショットを特別に閲覧でき、非表示、未公開、センシティブなスナップショットも見られます。そのほかの閲覧者には該当するチェックがかかり、限定公開の共有はリンクから見られます。共有に含まれるレコードに秘密の情報を入れないでください。任意のブラウザストレージのキーが自動で移行されることはありません。
プレイヤー画像(セッション単位)
プレイヤーの非公開の画像には api.media を使います。作者のアセットには引き続き @asset: を使います。サーバーで S3 ストレージが設定されていれば、運営者が新規アップロードを止めていない限り、すべてのワールドでアップロードが使えます。
| メソッド | 何をするか |
|---|---|
media.list(offset?) | { items, hasMore, uploadsEnabled } を返します。hasMore が true なら続きのページを読み込みます |
media.pick({ entryId?, metadata? }?) | 選択画面を開いてアップロードします。{ mediaId, entryId } で解決し、キャンセルされたら null |
media.upload(file, { entryId?, filename?, metadata?, uploadId? }?) | Blob/File をアップロードし、{ mediaId, entryId } を返します。同じリクエストを再試行するときは uploadId を使い回します |
media.remove(entryId, version) | 項目のバージョンを使って、今のセッションとのひもづけを外します。{ removed } を返します |
一覧の項目には { id, entryId, filename, metadata, version, sizeBytes, url, thumbnailUrl, deleted } が入っていて、id がメディア ID です。JSON に保存するのは entryId/mediaId にして、署名付きの表示用 URL や base64 のバイト列は保存しないでください。開き直すときと、5 分の有効期限が切れる前に、list() で URL を更新します。削除済みの項目は URL が null のことがあります。セッションとのひもづけを外しても、ほかの参照が残っているあいだは、ファイルが完全に消えたり容量が空いたりはしません。
入力は静止画の JPEG/PNG/WebP で、16 MiB まで、デコード後 4,000 万画素までです。サーバーは長辺 2048 ピクセルまでの WebP の表示用画像とサムネイルを作ります。元のファイルのバックアップにはなりません。画像はアカウント共通のアセット容量を使います。保存と移行のやり方は プレイヤー画像のレシピ を参照してください。
ロアブックの検索
画面コードから、カードのロアブックのエントリとシチュエーションを読み取り専用で参照できます。サイドの LLM プロンプトを組み立てるときにエントリを調べたり選んだりする、ゲーム内の日誌ビューアを作る、デバッグパネルをつなぐ、といった用途に使えます。
| フィールド / メソッド | 何をするか |
|---|---|
entries | ReadonlyArray<SandboxEntry>。有効なすべてのエントリで、position 順に並んでいます。SandboxEntry を参照 |
getEntry(name) | 名前の完全一致(大文字と小文字を区別)でエントリを 1 つ探します。SandboxEntry | null を返します。localhost では、見つからなかったときに使える名前の一覧を 1 回だけ警告ログに出します。エントリの名前を変えてカード側の更新を忘れたときに便利です |
worldbooks | カードのシチュエーション。entry.worldbookId を worldbook.id と照らし合わせれば、エントリをシチュエーションごとに分けられます |
たいていはこれらを直接触る必要はありません。ai.complete() に includeLorebook: "matched" を渡せば、サーバーが設定を組み立ててくれます(下記)。entries / getEntry を使うのは、細かく制御したいとき、たとえば 「この NPC は tavern タグの付いたエントリしか知らない」 ようなときです。
カードの AI を呼ぶか、生のコンプリーションか
画面コードからモデルに言葉を出させる方法は 2 つあります。
api.callAi(name, input)は、キャンバスで用意したカードの AI を動かします。モデル、見えるもの、回答形式 のフィールド、再試行、クールダウン、予備のせりふはすべてその設定にあり、返答は受け取る前にチェックされます。呼び出しがゲームの一部であるとき(占い師、店主の値切り交渉、ボタンに答える NPC)はこちらを使ってください。ゲームアクション を参照。- 下の
api.ai.complete(...)は生のモデル呼び出しです。プロンプトはすべて自分のコードで書き、何もチェックされません。
生の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), // optional, per-token
model: "anthropic/claude-sonnet-4.6", // optional, defaults to selectedModel
maxTokens: 500, // optional, default 2048, max 8192
temperature: 0.7, // optional
includeLorebook: "matched", // optional — see below
})全文の入った Promise<string> を返します。呼び出しが失敗したとき(レート制限、不正なリクエスト、プロバイダーのエラー、セッションがない)も Promise は 解決 し、"[Error: <message>]" の形の文字列になります。文章を使う前に、この先頭部分を確認してください。reject するのは、クライアント側の 120 秒のタイムアウトのあとだけです。ゲストプレビューでは "" で解決します。
画像を読めるモデル向けに、メッセージの content を { type: "text", text } と { type: "image_url", image_url: { url } } の部品の配列にしたり、メッセージに attachments(pickChatImage() で得た画像)を付けたりもできます。
機械で読める返答がほしいときは、responseFormat: { type: "json_object" } を渡し、 メッセージの中でもはっきり JSON を求めてください。SDK はこのオプションを プロバイダーにそのまま渡します。対応しているかどうかは、選んだプロバイダーやモデルしだいです。ゲームのスキーマを強制するものではありません。 結果をパースし、適用する前にすべてのアクションを検証してください。 このオプションを省くと、普通のテキストのコンプリーションのままです。ほかの形式を指定すると、 推論の前に HTTP 400 で拒否されます。
context: "session" — 共有の物語コンテキスト
認証済みのセッションの今のペルソナ、有効になっている物語用のプロンプト、 プレイヤーの今の生成設定を取り込むよう指定します。
const result = await api.ai.complete({
context: "session",
includeLorebook: "matched",
responseFormat: { type: "json_object" },
messages: [
{ role: "system", content: 'Return JSON with only {"actions":[],"line":""}.' },
{ role: "user", content: sceneObservation },
],
})ペルソナの選択は、セッションがロックされていない限りアカウントに従います(ペルソナなしの 明示的なロックも含みます)。ペルソナの公開の説明フィールドはサーバーで解決され、 非公開のペルソナのメモが含まれることはありません。物語用のプロンプトは、通常のチャットと同じく、 有効なフォルダ、選んだモデル、アカウントの利用資格のルールに従います。
セッションコンテキストでは、includeLorebook を省くと "matched" になります。false にすると、 ワールドのエントリは除き、ペルソナとユーザーのプロンプトは残します。true/"all" はキーワードによる 選択を飛ばしますが、状態の条件、有効なシチュエーション、UI とのロアのひもづけには従います。照合には 渡したすべてのユーザーメッセージ を使うので、最後に修正の指示を足しても、元の場面が消えることはありません。 ネイティブのマクロは保存されたセッションの状態と今のペルソナを使い、最後のメッセージを指すマクロは渡した履歴を指します。 システムのセクション、会話例、深さ指定のエントリ、履歴のあとのロールは、 その履歴に対してネイティブと同じ位置に置かれます。呼び出し側のシステムメッセージはそれらの 物語のセクションのあとに続き、JSON モードでは、求めたオブジェクトの取り決めを守らせる指示が最後に加わります。 返ってきたスキーマの検証は、カードの側で引き続き行ってください。
worldbookIds(セッションコンテキストのみ)は、ネイティブのロアを、指定したシチュエーションと どのシチュエーションにも入っていないエントリだけに絞ります。省くと通常の選択のままで、[] ならシチュエーションの外にあるエントリだけになります。ID は存在している必要があり、有効化、起動方法、 エントリの条件は引き続き適用されます。context: "session" なしで worldbookIds を渡すと、 HTTP 400 が返ります。
今のプレイヤーの設定は親のアプリが渡します。カードが任意の設定オブジェクトを 渡すことはできません。呼び出しで明示した maxTokens/temperature が最優先で、次にプレイヤーの 設定、ワールドの設定、最後に下の生の呼び出しのデフォルト値です。出力トークンの上限 8192 と、 temperature の範囲 0〜2 は変わりません。対応しているサンプリング、推論、ストリーミングの 設定は、通常のチャットと同じように渡されます(モデルごとの繰り返しペナルティを含みます)。 セッションの設定が不正なときは HTTP 400 が返ります。
使われるのは、渡したサイド呼び出しの履歴だけです。保存されたチャット、要約、未適用の チャットの効果は取り込まれません。maxContext とワールドのロアの予算が制限するのは、任意で 照合されるエントリで、必須のロア、プリセット、呼び出し側の取り決めを切り詰めることはなく、 組み上がったプロンプトがどのプロバイダーのウィンドウにも収まることを保証するものでもありません。 返ってきた文章が自動で効果を適用したり、メッセージとして保存されたりはしません。 context を省くと、下で説明する従来の生の動きのままです。
構造化出力(任意)
JSON で出力させたいときは responseFormat を渡します。省けば従来どおりのテキストで、{ type: "json_object" } なら従来のプロバイダーごとの JSON モードです。厳密なスキーマ出力には、今のところ OpenRouter の接続(公式か BYOK)が必要です。
const text = await api.ai.complete({
messages: [{ role: "user", content: "Choose an action: wait." }],
responseFormat: {
type: "json_schema",
json_schema: {
name: "decision_v1",
strict: true,
schema: {
type: "object",
properties: { action: { type: "string", enum: ["wait"] } },
required: ["action"],
additionalProperties: false,
},
},
},
})ラッパーが受け付けるのは、ここに示したフィールドだけです。スキーマ名は /^[A-Za-z0-9_-]{1,64}$/ に一致する必要があり、strict は true、スキーマのルートは type: "object" のオブジェクトでなければなりません。直列化した形式全体は UTF-8 で 16,384 バイトまで、たどる深さは 16 まで(形式のルートが深さ 0)、たどる値は 2,048 個までです。普通の JSON の値を使ってください。循環、アクセサ、直列化のフックは拒否されます。スキーマの文字数は、既存の 50,000 文字の内容の上限と、プロンプトの料金の見積もりに含まれます。
ほかのプロバイダーのアダプターは、json_schema を HTTP 400、コード UNSUPPORTED_RESPONSE_FORMAT で拒否します:“This provider does not support JSON Schema side completions.” OpenRouter はスキーマ全体を送り、求めたパラメーターに対応したエンドポイントを要求します。公式のルーティングの設定と BYOK のアカウントのルーティングはそのまま保たれます。エンドポイントごとのスキーマのエラーは "[Error: …]" の結果として返り、空の成功やもっと弱い形式になることはありません。これを確認して、適切な代わりの処理を用意してください。
スキーマが縛るのは返答の構造であって、行動してよいという許可ではありません。状態を変える前に、返ってきた内容、今の状態、許されている行動、根拠をパースして検証してください。プロバイダー固有のスキーマのキーワードは、上流でチェックされます。
制限とコスト
| 制限 | 値 | 出どころ |
|---|---|---|
| 1 回の呼び出しあたりのメッセージ数の上限 | 50 | サーバーが HTTP 400 で拒否 |
| 内容の合計の上限 | すべてのメッセージを合わせて 50,000 文字 | サーバーが HTTP 400 で拒否 |
maxTokens のデフォルト | 2048 | 生の呼び出しのデフォルト。セッションコンテキストでは先に設定を引き継ぐ |
maxTokens の上限 | 8192 | それより大きい値は黙って切り詰められる |
temperature の範囲 | 0〜2、デフォルト 1.0 | 範囲外の値は切り詰められる |
| デフォルトのモデル | プレイヤーの selectedModel。model も selectedModel もなければ anthropic/claude-sonnet-4.6 | |
| レート制限 | サイド呼び出し専用の枠:毎分 100 回。メインのチャットの毎分の枠とは別 | 超えると HTTP 429 と RATE_LIMITED コード、Retry-After ヘッダーを返す |
| クレジット | メインのチャットと同じトークン単位の課金。BYOK のユーザーはサーバーのクレジット消費がない が、自分のプロバイダーには支払う | エンドポイント "side-completion" として記録 |
| 認証 | セッションは今のプレイヤーのものである必要がある。そうでなければ HTTP 404 で失敗 |
ai.decide — 選択式の質問
await api.ai.decide({ state, questions }) は、質問ごとにモデルに選択肢をひとつ選ばせ、文章の代わりに確率を返します。state は状況を表す小さな JSON オブジェクトです。questions は質問の名前を { type: "choice", instructions, criteria } に対応づけたもので、criteria は各選択肢のラベルを、それが当てはまるときの説明に対応づけます。
const { answers } = await api.ai.decide({
state: { suspect: "Vex", lastLine: playerText },
questions: {
lying: {
type: "choice",
instructions: "Is the player lying to the guard?",
criteria: { yes: "The statement contradicts what they did", no: "The statement fits what they did" },
},
},
})
// answers.lying → { choice: "yes", probabilities: { yes: 0.8, no: 0.2 }, confidence: 0.8 }1 回の呼び出しで質問は 8 個まで、質問ごとの選択肢は 64 個まで、選択肢のラベルは 128 文字まで、合計でおよそ 32,000 文字までです。サーバーの判定用モデルを使います。公式で使う分はプラットフォームが支払い、OpenRouter の BYOK キーを使うとプレイヤーのプロバイダーに請求されることがあります。ストーリーにも状態にも何の変化も起こしません。失敗すると reject します(15 秒の制限があります)。足りない答えを自分で埋めないでください。実際のセッションが必要です。
ai.context — サイドアクター向けの指示を組み立てる
await api.ai.context({ actor: "voice" | "director", model?, recentMessages? }) は、今の編集可能な保存済みセッションについて { instructions, receipt } を返します。有効なワールドの指示と、当てはまるユーザーのプリセットを組み立てますが、文章の生成、モデル呼び出しの課金、状態の変更はしません。実際に使うモデルを明示して渡してください。リアルタイム音声のモデルとテキストのモデルでは、当てはまるエントリの条件が違うことがあります。このメソッドでは、SDK が選択中のチャットモデルで代用することはありません。
recentMessages に入れるのは、その役者が見聞きした公開の { role: "user" | "assistant", content } の記録だけです。最大 24 件、1 件 4,000 文字まで、合計 12,000 文字まで。通常のチャット履歴やアカウントのペルソナのプロフィールは取り込まれません。 はカードの物語上のプレイヤー名になり、非公開の変数が指示の中に展開されることはありません。役者のタグでエントリの対象を絞れます。共有の有効なスタイル/システムのプリセットは両方の役者に適用されます。通常のエントリの有効化、ロアの照合、マクロは引き続き効きます。通常の ai.complete のデフォルトは変わりません。
receipt には、含まれたエントリと外されたエントリの ID、外された理由、当てはまるユーザーのプロンプトの ID のほか、モデル、ペルソナ/履歴の方針、instructions の文字数が入っています。エントリの中身は入っていません。instructions は 9,000 文字が上限で、超えたときは黙って切り詰めずに reject します。失敗を捕まえて、更新に成功するまでは、前回確認できた指示を古いものとして表示したままにしてください。旧方式の通話中なら、成功した新しい指示を realtimeVoice.updateInstructions に渡します。残高通話の最初の指示は固定です。updateContext だけでは最初の指示は置き換わりません。
includeLorebook — ワールド伝承の自動注入
context: "session" なしのサイド呼び出しは、メインのチャットのプロンプトの組み立てを通りません。includeLorebook を渡すと、サーバーがワールドのエントリから作ったシステムメッセージを先頭に加えます。
| 値 | 動き |
|---|---|
省略 / false | 注入しない(デフォルト)。翻訳、要約、分類など、ワールドのコンテキストがいらないものに使う |
true / "all" | オープニング以外の有効なエントリをすべて、position 順に注入する。結果は予測しやすいが、トークンが多くかかる |
"matched" | messages の中の 最後のユーザーメッセージ に対して、メインのチャットと同じキーワード照合を行う。alwaysSend のエントリは常に入り、キーワードで発動するエントリは関係があるときだけ加わる。キャラクターになりきるサイド呼び出しにおすすめ |
これがないと、キャラクターになりきるサイド呼び出しは、メッセージで伝えたことしか知らないので、キャラクターの口調がメインのチャットからずれていきます。"matched" なら、NPC とのスマホでのチャットにも、メインのチャットと同じワールドの設定とキャラクターのプロフィールが見えます。
// A phone chat that stays in canon
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", // server pulls Balder's profile + relevant world lore
})もっと細かく制御したいとき、たとえば特定のエントリを名前で入れたい、特定のタグのエントリだけを入れたいときは、includeLorebook を使わずに api.entries を回して、システムメッセージを自分で組み立てます。
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" モードの注意点:キーワードを探すのは最後のユーザーメッセージだけで(履歴全体ではありません)、ゲーム変数に左右される条件付きのエントリはサイド呼び出しでは発動しません(照合には空の状態しか見えません)。トークンより正確さが大事なら、true ですべてを入れてください。
コンテキスト注入
次の メインのチャットの AI のターンに、一度きり のコンテキストメッセージを差し込みます。一度使われると消え、チャットに見えるメッセージは 作られません。スマホに届いたメッセージ、舞台裏での NPC の会話、周りの変化など、メインの AI には知っていてほしいけれど、プレイヤーにはチャットの吹き出しとして見せたくないものに使います。
api.injectContext("You just received a cryptic text: 'Tonight, 9pm, usual place.'", { role: "system" })
// On the player's next message, the main AI will see this as a system message.options:{ role?: "system" \| "user" }(デフォルトは "system")。
成り立っているあいだ 毎回 の呼び出しで AI に見せたいもの(プレイヤーがどの部屋にいるか、画面に何が映っているか)には、代わりに setScene を使います。
モデルピッカー
| フィールド / メソッド | 何をするか |
|---|---|
selectedModel | 今のモデル ID |
userPlan | ユーザーのプラン |
preferredProvider | "official" か "private" |
setPreferredProvider(provider) | "official" と "private" を切り替えます。Promise<{ ok, provider?, error? }> を返します |
setModel(modelId) | モデルを切り替えます(送りっぱなし) |
getModels(provider?) | Promise<{ models, pinnedModels, recentlyUsed }> を返します。models は Array<{ id, name, provider, contextLength, supportsImages? }>。プレイヤー自身のキーで使えるモデルがほしいときは "private" を渡します |
pinModel(modelId) / unpinModel(modelId) | モデルをピン留め / 解除します。Promise<{ pinnedModels, accepted }> を返します。一覧がいっぱいでピン留めが断られたときは accepted が false です |
modelFallback | 選んだモデルが使えず、ホストが代わりのモデルを提案しているときに表示するお知らせ。それ以外は null。<Chat> と <MessageInput> はもう対応しています |
API には、組み込みのチャットが自分のパネルで使うフィールドとメソッドもあります。モデルの組み合わせ(mixMode、modelPool、setMixMode、addToPool、removeFromPool、setPoolWeight、togglePoolLock)、プレイヤーのパソコン上のローカルモデル(localBridge、reconnectLocalBridge、resolveModelFallback、cancelModelFallback)、プレイヤーのプロンプト(playerPrompts、togglePlayerPrompt)、セッションのメモリーと要約(memorySummaryEnabled、getSessionMemory、getSessionSummary と関連のセッター)、Lore Shift(getLiveCanon など)、状態ガード(getStateGuardSettings、setStateGuardSettings)です。形は組み込みの UI に合わせたもので、ここでは扱いません。
アセット
| メソッド | 何をするか |
|---|---|
resolveAssetUrl(ref) | @asset:<id> の参照を /cdn/<id> の URL に変換します。文字列を変換するだけで、通信はしません。HTTP/HTTPS の URL はそのまま返ります。<img src="@asset:…"> や、インラインスタイルと <style> タグの中の @asset: は自動で解決されます。URL を文字列として使いたいときに呼びます |
マークダウン
| メソッド | 何をするか |
|---|---|
renderMarkdown(text) | マークダウンを 安全な HTML に変換します(HTML エンティティはエスケープ、危険なタグは除去、書式は保持)。結果はカスタムの吹き出しの中で dangerouslySetInnerHTML に渡します。下の例を参照 |
<div dangerouslySetInnerHTML={{ __html: api.renderMarkdown(msg.rawContent) }} />コンポーネント
<Chat>
プラットフォームのチャット一式です。props なしなら、デフォルトのチャットになります。
含まれるもの:メッセージ一覧、自動スクロール、ストリーミングのカーソル、スワイプの操作、メッセージの操作(編集/削除/再生成)、入力バー、選択肢ボタン、モデル選択、読み取り専用モード、オープニングの仮表示。
<Chat renderBubble={(msg) => <MyBubble {...msg} />} />Props
| Prop | 型 | 説明 |
|---|---|---|
renderBubble? | (props: BubbleProps) => ReactNode | メッセージの吹き出しの見た目を変えます。省くと、デフォルトのマークダウン表示になります |
className? | string | 外側のコンテナに足す CSS クラス |
children? | ReactNode | メッセージ一覧の 上 に表示する内容(固定の HUD ヘッダーなど) |
design?、fontMap? | object | プレイヤー画面 のエディタで決めたメッセージの見た目(メッセージの見た目 とその細かい調整)。renderBubble がないときだけ使われます。ビジュアルエディタが埋めるもので、ふつうは自分で書きません |
BubbleProps
renderBubble のコールバックが受け取る msg オブジェクト:
| フィールド | 型 | 意味 |
|---|---|---|
contentHtml | string | 描画済みの安全な HTML(マークダウンは変換済み)。ふつうは dangerouslySetInnerHTML に渡します |
content | string | [var: op value] の指示を取り除いたマークダウンの文章。自分で正規表現をかけたり、一部を renderMarkdown に渡したりするのに使います |
rawContent | string | 描画前の生のマークダウンの文章(指示の文字列も含む) |
role | "user" | "assistant" | "system" | メッセージの送り主 |
messageIndex | number | 一覧の中の位置(0 = 最初で、ふつうはオープニング) |
isStreaming | boolean | このメッセージがストリーミング中なら true |
stateSnapshot | Record<string, unknown> | null | このメッセージが生成された時点のゲーム状態(「あのときの HP や場所は?」を知りたいときに便利) |
variables | Record<string, unknown> | 今(最新)のゲーム変数 |
renderMarkdown | (text) => string | 補助関数:任意のマークダウンの文章を安全な HTML にします |
<MessageList>
メッセージの流れだけです(スクロール、ストリーミングのカーソル、スワイプの操作付き)。入力バーは ありません。
<MessageList />renderBubble は受け取りません。吹き出しを変えたいなら <Chat renderBubble={...} /> を使うか、<MessageList> を使わずに api.messages を直接読みます(ビジュアルノベルの作り方)。
<MessageInput>
入力バーだけです(モデル選択、選択肢ボタン、続き/やり直しのメニュー、ストリーミングの状態付き)。
<MessageInput />api.readOnly が true のときは自動で隠れます。
<ChatCanvas>
<Chat /> の別名で、古いカードが動き続けるように残してあります。新しいコードでは <Chat /> を使ってください。
モデルピッカーのコンポーネント
| コンポーネント | 何をするか |
|---|---|
<ModelTrigger onClick={...} /> | チャットの入力欄にあるモデルボタン。今のモデル名とプレイヤーの残高を表示します。Props:onClick(必須)、model?、provider?、showBalance?(デフォルト true)、compactBalance?、className? |
<ModelPickerModal open={...} onClose={...} /> | モデル選択。実際のセッションで open と onClose だけを渡すと、チャットと同じモデル選択が開きます(api.openModelPicker() を呼んでもかまいません)。onSelectModel(modelId)、title、subtitle を渡すと、チャットのモデルではなく、自分用の選択に使えます |
<SessionMemoryModal open={...} onClose={...} /> | チャットのセッションメモリーのダイアログ。セッションメモリーの拡張機能が必要です。ボタンを出す前に api.memorySummaryEnabled を確認してください |
<MessageInput> を使わない全画面の画面コードには、自分で足さない限りモデルボタンがありません。
var [pickerOpen, setPickerOpen] = React.useState(false)
// ...
<ModelTrigger onClick={function () { setPickerOpen(true) }} />
<ModelPickerModal open={pickerOpen} onClose={function () { setPickerOpen(false) }} />ロアのコントロール
画面コードの中のコントロールがオンのあいだだけ、ロアブックのエントリを AI に送ります。プレイヤーにエントリの文章が見えることはありません。
| コンポーネント | 何をするか |
|---|---|
<LoreSlot id="slot-id" /> | 何も表示しません。マウントされているあいだ、slot-id にひもづいたエントリが AI に送られ、アンマウントすると止まります。条件付きでマウントします({open && <LoreSlot id="x" />}) |
<LoreButton slotId label icon? description? variant? className? /> | スロットをオン/オフするボタンひとつ。variant:"chip"(小さな丸いボタン、デフォルト)か "card"(スイッチと description の付いた横幅いっぱいの行) |
<LoreSwitch slotId onLabel? offLabel? onIcon? offIcon? className? /> | 1 つのスロットに 2 つのボタン。片方でオン、もう片方でオフにします。ラベルのデフォルトは中国語(开启 / 关闭)なので、自分のラベルを渡してください |
<LoreGroup slots={[{ id, label, icon?, description? }]} variant? className? /> | 一度にオンにできるのが最大ひとつの選択肢の並び。オンになっているものをもう一度選ぶとオフになります |
<LorePanel title? slots? defaultOpen? className?>…</LorePanel> | ロアのボタンを入れる、折りたためるパネル。slots には「いくつオンか」のバッジで数えるスロット id を並べます。title のデフォルトは中国語(知识库)なので、自分のタイトルを渡してください |
LoreButton、LoreSwitch、LoreGroup は、状態を __lore_<slotId> という名前の変数に覚えておくので、選んだ状態は再読み込み、チェックポイント、ブランチをまたいで残ります。
コードにコントロールを置くと、そのスロット id に対応するエントリが作られ、ロアブック(パネル → ロアブック)の フロントエンド連動 に並びます。AI に読ませたい内容はそこに書きます。ひもづけには変数の条件を重ねることもでき、その場合はコントロールがオン かつ 条件が成り立っているときに送られます。api.loreUiBindings でひもづけの一覧が読めます。
ビヘイビアのコード(ctx)
ビヘイビア の効果には コード を入れられます。ビヘイビアが発動したときに、カードのサンドボックスの中で、実際の API に対して動きます。動くのはプレイヤーがゲームを開いているあいだだけです。コードの本体は async 関数なので、await が使えます。渡されるのはオブジェクトひとつ、ctx です。
| フィールド / メソッド | 何をするか |
|---|---|
ctx.vars | 今の値のコピー。キーは変数 ID |
ctx.get(name) | 値をひとつ、表示名か ID で取得 |
ctx.set(name, value) | 値を書き込む |
ctx.add(name, n) | 数値に n を足す |
ctx.push(name, item) | リストに項目を追加する |
ctx.toast(text) | プレイヤーにお知らせを出す |
ctx.say(text) | プレイヤーとしてせりふを送る |
ctx.callAi(ai, input?) | カードの AI を呼ぶ。api.callAi と同じように解決する |
ctx.random(a, b) | a から b まで(両端を含む)の整数 |
ctx.event | { reactionId }:発動したビヘイビア |
書き込みはコードの実行中にためておかれ、終わったときにまとめて保存されます(patchVariables を通します)。コードが例外を投げると、エラーはブラウザのコンソールに記録され、エラーの前に書き込んだ値は保存されます。
// Draw two cards from the deck
const deck = [...(ctx.get("deck") || [])]
const hand = []
for (let i = 0; i < 2 && deck.length; i++) {
hand.push(deck.splice(ctx.random(0, deck.length - 1), 1)[0])
}
ctx.set("deck", deck)
ctx.set("hand", hand)
ctx.toast("You drew " + hand.join(" and "))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フォントは非同期で読み込まれます。読み込み中、フックは options.fallback(デフォルトは "serif")を返します。読み込みが終わると再レンダリングが起き、ファミリー名の完全な文字列(名前がぶつからないよう接尾辞付き)が返ります。
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
stateChanges?: Record<string, unknown> | null // diff of variable updates from this message
stateSnapshot?: Record<string, unknown> | null // full state at message generation
swipes?: Array<{ content?, rawContent?, createdAt, model?, tokenCount?, refusal? }>
// alternative AI replies; only the active one keeps its text
activeSwipeIndex?: number
model?: string | null
tokenCount?: number | null
generationTimeMs?: number | null
creditCost?: number | null
compacted?: boolean // hidden in the "older messages" section
attachments?: Array<{ type, mimeType, name, url }> | null
createdAt: string // ISO-8601
}サンドボックスの中のスワイプは自分の stateSnapshot を持たず、表示中のもの以外のスワイプには content も rawContent もありません。
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[]
worldbookId?: string // the scenario it belongs to; matches api.worldbooks IDs
folderId?: string // the folder you filed it in, if any
audience?: "ai" | "player" | "both"
conditions?: Condition[] // variable conditions, if it's conditional lore
conditionLogic?: "all" | "any"
portrait?: string | null // character portrait as an absolute URL
portraitVideo?: { idle: string | null; speaking: string | null } | null
}エンジン内部の 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 // the session you're in
parent: BranchNode | null // the branch you forked from, or null at the root
siblings: BranchNode[] // other branches forked from the same parent, oldest first
children: BranchNode[] // branches forked off `current`, oldest first
}ブロックされたブラウザAPI
コードは、allow-same-origin の ない、クロスオリジンの sandbox="allow-scripts" の iframe の中で動き、コンテンツセキュリティポリシーは connect-src 'none' です。つまり:
- 親のアプリの Cookie / localStorage には触れられない
- 自分でネットワークリクエストを送れない(
fetch、XMLHttpRequest、WebSocket) window.parentを直接操作できない
古いコードが落ちないよう、一部のブラウザ API はサンドボックスが置き換えています。
置き換え
| 書いたもの | 実際に起きること |
|---|---|
fetch('/api/...') | ホストは応答しません。Promise はデータのない空の応答で解決します。下の SDK のメソッドを使ってください |
fetch('/cdn/...') | ブラウザに渡されますが、ブラウザがブロックします(connect-src 'none')。アセットのバイト列を読むには api.fetchAsset() を使います |
fetch('any other URL') | 拒否(例外を投げる) |
localStorage.getItem/setItem/removeItem/clear | 書き込みはホストを通してワールドごとに保存されますが、getItem で見えるのはページを読み込んでから書いた値だけです。保存済みの値を読むには 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 を使う
| こう書かずに | こう書く |
|---|---|
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
ネットワークにも親のオリジンにも触れないものは、普通のブラウザと同じように動き、SDK で包む必要はありません。
| API | カードでのよくある使い方 |
|---|---|
<input type="file"> + FileReader | プレイヤーが選んだファイルを読む。プレイヤーの画像を残すには api.media でアップロードします。レシピ:プレイヤーがアップロードする画像 を参照 |
URL.createObjectURL / revokeObjectURL | Blob の一時的なメモリ上の URL を作る(保存前のプレビューなど) |
<canvas> + getContext("2d") + toDataURL / toBlob | アップロードの前に画像を縮小、切り抜き、合成する |
<img>、<audio>、<video> | https: の URL、@asset:... の参照、data:/blob: の URL を表示する |
<iframe> | *.pages.dev、*.vercel.app、*.netlify.app、*.github.io でホストされたページを埋め込む |
IntersectionObserver、ResizeObserver、matchMedia、requestAnimationFrame | 標準的なレイアウト / アニメーションの部品 |
crypto.randomUUID、crypto.subtle | クライアント側の状態のハッシュや ID の生成 |
WebAudio(AudioContext) | 軽い音の合成や解析 |
| ポインターロック | 一人称視点の操作(requestPointerLock) |
一目で:API全体
1 枚の表に、ひととおりまとめました。
useYumina()
├── State reads
│ ├── variables, globalVariables
│ ├── worldName, worldId, worldCover, background, sessionId
│ ├── currentUser (account), user (persona-aware)
│ ├── mode, capabilities, language
│ ├── messages, hasEarlierMessages, isLoadingEarlier
│ ├── isStreaming, streamingContent, streamingReasoning
│ ├── pendingChoices, error, errorCode, sendFailureNonce, composerSendKey
│ ├── readOnly, greetingContent, canvasMode, speakerBubbles
│ ├── checkpoints
│ ├── entries, worldbooks, loreUiBindings
│ └── selectedModel, userPlan, preferredProvider, balance
├── Game actions
│ ├── sendMessage(text, attachments?)
│ ├── pickChatImage() → Promise<image | null>
│ ├── setVariable(id, value)
│ ├── patchVariables(values) → Promise<void>
│ ├── executeAction(actionId)
│ ├── executeActionAndWait(actionId, params?) → Promise<{applied, variables, firedIds}>
│ ├── callAi(ai, input?) → Promise<{text, fields, fallback} | null>
│ ├── onAiOutput(channel?, cb) → unsubscribe
│ ├── setScene(scene, { events? })
│ ├── onStoryEvent(cb) → unsubscribe
│ ├── switchGreeting(index) → Promise<void>
│ ├── clearPendingChoices()
│ └── setComposerDraft(text) // prefill, no send
├── Chat control
│ ├── editMessage(id, content, options?) → Promise<boolean>
│ ├── deleteMessage(id) → Promise<boolean>
│ ├── regenerateMessage(id)
│ ├── continueLastMessage()
│ ├── stopGeneration()
│ ├── restartChat()
│ ├── swipeMessage(id, direction) → Promise
│ └── loadEarlierMessages() → Promise<boolean>
├── Sessions / branching
│ ├── revertToMessage(id) → Promise<void>
│ ├── branchFromMessage(id) → Promise<string | null>
│ ├── getBranchContext() → Promise<BranchContext>
│ ├── createSession(worldId) → Promise<string>
│ ├── deleteSession(id) → Promise<void>
│ └── listSessions(worldId) → Promise<Array>
├── Checkpoints
│ ├── saveCheckpoint() → Promise<void>
│ ├── loadCheckpoints() → Promise<void>
│ ├── restoreCheckpoint(id) → Promise<void>
│ └── deleteCheckpoint(id) → Promise<void>
├── Audio
│ ├── playAudio(trackId, opts?)
│ ├── stopAudio(trackId?, fadeDuration?)
│ ├── pauseAudio(trackId)
│ ├── resumeAudio(trackId)
│ ├── onAudioEnded(cb) → unsubscribe
│ ├── setAudioVolume(type, volume)
│ └── getAudioVolume(type) → number
├── Voice
│ ├── tts.speak / stop / onPlaybackFrame / setPrefs / preview, ttsState
│ ├── voice.prepare / record / stop / cancel / setPrefs, voiceInputState
│ └── realtimeVoice.getConfig / finish / prepare / start / interrupt / stop / setMuted / updateContext /
│ updateInstructions / reactToScene / cancelSceneReaction / resolveTool /
│ setSpatial / onEvent
├── UI / navigation
│ ├── toggleImmersive()
│ ├── openPersonaManager(), getPersonaProfile() → Promise
│ ├── openModelPicker(options?), openSessionManager(), sharePlaythrough()
│ ├── openSupport() → Promise<{opened, reason}>, openCreditTopUp()
│ ├── fetchAsset(ref) → Promise<{ok, bytes?, contentType?, error?}>
│ ├── copyToClipboard(text)
│ ├── navigate(path)
│ └── showToast(message, type?)
├── Storage
│ ├── storage.get(key) → Promise<string | null>
│ ├── storage.set(key, value) → Promise<void>
│ └── storage.remove(key) → Promise<void>
├── Cloud session JSON
│ ├── sessionStorage.get(key) → Promise<{value, version, exists}>
│ ├── sessionStorage.set(key, value, {expectedVersion}) → Promise<{value, version, exists}>
│ └── sessionStorage.remove(key, {expectedVersion}) → Promise<{value, version, exists}>
├── Session images
│ ├── media.list(offset?) → Promise<{items, hasMore, uploadsEnabled}>
│ ├── media.pick(options?) → Promise<{mediaId, entryId} | null>
│ ├── media.upload(file, options?) → Promise<{mediaId, entryId}>
│ └── media.remove(entryId, version) → Promise<{removed}>
├── Lorebook
│ ├── entries (ReadonlyArray<SandboxEntry>) // sorted by position, enabled only
│ ├── worldbooks
│ └── getEntry(name) → SandboxEntry | null
├── AI
│ ├── ai.complete({ messages, onDelta?, model?, maxTokens?, temperature?, context?, worldbookIds?, includeLorebook?, responseFormat? }) → Promise<string>
│ │ // includeLorebook: true | "all" | "matched" — auto-inject world lore
│ ├── ai.decide({ state, questions }) → Promise<{ answers }>
│ └── ai.context({ actor, model?, recentMessages? }) → Promise<{ instructions, receipt }>
├── Context injection
│ └── injectContext(message, { role? })
├── Model picker
│ ├── setModel(modelId), setPreferredProvider(provider) → Promise
│ ├── getModels(provider?) → Promise<{ models, pinnedModels, recentlyUsed }>
│ ├── pinModel(id), unpinModel(id) → Promise<{ pinnedModels, accepted }>
│ └── modelFallback
├── Assets
│ └── resolveAssetUrl(ref) → string
└── Markdown
└── renderMarkdown(text) → string // safe HTML
Sandbox globals (no import)
├── React
├── useYumina, useAssetFont
├── Icons (every Lucide icon)
├── Chat, MessageList, MessageInput, ChatCanvas (alias of Chat)
├── ModelTrigger, ModelPickerModal, SessionMemoryModal
├── LoreSlot, LoreButton, LoreSwitch, LoreGroup, LorePanel
└── Tailwind utility classes (CSS-level)
Behavior code (ctx)
└── vars, get, set, add, push, toast, say, callAi, random, event
Blocked / replaced
├── fetch(...) → blocked; use SDK methods / fetchAsset
├── localStorage / sessionStorage → write-through only; read with api.storage
├── window.location → synthetic + navigate
└── navigator.clipboard → copyToClipboard
Browser APIs that work as-is
├── <input type="file"> + FileReader // then api.media.upload
├── <canvas>, URL.createObjectURL // image processing
├── IntersectionObserver, ResizeObserver, matchMedia, rAF
├── crypto.randomUUID, crypto.subtle
└── WebAudio (AudioContext)