Skip to content

自定义 UI (rootComponent)

rootComponent 是一个 React/TSX 文件的虚拟文件系统,运行在沙箱化的 iframe 中。它提供世界的视觉层,拥有对游戏状态、聊天控制、会话管理、AI 补全和音频的完整访问权限。

rootComponent Schema

json
{
  "rootComponent": {
    "id": "uuid",
    "name": "My World UI",
    "entryFile": "index.tsx",
    "files": {
      "index.tsx": "export default function App() { ... }",
      "bubble.tsx": "export default function Bubble({ content, role }) { ... }"
    },
    "updatedAt": "2024-01-01T00:00:00Z"
  }
}

入口文件

index.tsx 必须导出一个默认组件:

tsx
export default function App() {
  return <Chat />;
}

自定义消息气泡:

tsx
import Bubble from "./bubble";

export default function App() {
  return <Chat renderBubble={Bubble} />;
}

完整应用模式:

tsx
export default function App() {
  var api = useYumina();
  
  return (
    <div style={ {display: "flex", flexDirection: "column", height: "100vh"} }>
      <header>HP: {api.variables.health}</header>
      <MessageList />
      <MessageInput />
    </div>
  );
}

useYumina() — 完整 API 参考

状态读取

typescript
interface SandboxedYuminaAPI {
  // 游戏状态
  variables: Record<string, unknown>;
  globalVariables: Record<string, unknown>;
  
  // 世界信息
  worldName: string;
  worldId: string;
  sessionId: string;
  
  // 用户身份
  currentUser: { id: string; name?: string; image?: string | null } | null;
  user: { name: string; avatar: string | null };  // 感知人设
  
  // 聊天状态
  messages: SandboxMessage[];
  isStreaming: boolean;
  streamingContent: string;
  streamingReasoning: string;
  pendingChoices: string[];
  error: string | null;
  readOnly: boolean;
  
  // 世界书
  entries: ReadonlyArray<SandboxEntry>;
  getEntry(name: string): SandboxEntry | null;
  
  // 会话
  checkpoints: Array<{ id: string; name: string; messageCount: number; createdAt: string }>;
  greetingContent: string | null;
  mode: "session" | "guest-preview";
  capabilities: {
    canSendMessage: boolean;
    canPersistSession: boolean;
    canUseSessionApis: boolean;
    requiresAuth: boolean;
  };
  
  // UI 状态
  canvasMode: "chat" | "custom" | "fullscreen";
  selectedModel: string;
  userPlan: string;
  preferredProvider: "official" | "private";
  language: string;
  bgmVolume: number;
  sfxVolume: number;
}

聊天动作

typescript
sendMessage(text: string): void;
editMessage(messageId: string, content: string): Promise<boolean>;
deleteMessage(messageId: string): Promise<boolean>;
regenerateMessage(messageId: string): void;
continueLastMessage(): void;
stopGeneration(): void;
restartChat(): void;
swipeMessage(messageId: string, direction: "left" | "right"): Promise<Record<string, unknown>>;
setComposerDraft(text: string): void;
clearPendingChoices(): void;

会话管理

typescript
revertToMessage(messageId: string): Promise<void>;
branchFromMessage(messageId: string): Promise<string | null>;
getBranchContext(): Promise<BranchContext>;
createSession(worldId: string): Promise<string>;
deleteSession(sessionId: string): Promise<void>;
listSessions(worldId: string): Promise<Array<Record<string, unknown>>>;
navigate(path: string): void;

存档点

typescript
saveCheckpoint(): Promise<void>;
loadCheckpoints(): Promise<void>;
restoreCheckpoint(checkpointId: string): Promise<void>;
deleteCheckpoint(checkpointId: string): Promise<void>;

AI 补全

typescript
ai.complete(params: {
  messages: Array<{ role: string; content: string }>;
  onDelta?: (text: string) => void;
  model?: string;
  maxTokens?: number;
  temperature?: number;
  includeLorebook?: boolean | "all" | "matched";
}): Promise<string>;

直接调用 LLM,支持可选的流式输出和世界书注入。适用于 NPC 生成器、动态描述、提示系统,或主聊天流程之外的任何 AI 逻辑。

游戏动作

typescript
setVariable(id: string, value: unknown, options?: {
  scope?: string;
  targetUserId?: string;
}): void;
executeAction(actionId: string): void;
injectContext(message: string, options?: { role?: "system" | "user" }): void;

音频

typescript
playAudio(trackId: string, opts?: {
  volume?: number;
  fadeDuration?: number;
  chainTo?: string;
  maxDuration?: number;
  duckBgm?: boolean;
}): void;
stopAudio(trackId?: string, fadeDuration?: number): void;
setAudioVolume(type: "bgm" | "sfx", volume: number): void;
getAudioVolume(type: "bgm" | "sfx"): number;

存储(世界级别,持久化)

typescript
storage.get(key: string): Promise<string | null>;
storage.set(key: string, value: string): Promise<void>;
storage.remove(key: string): Promise<void>;

UI 控制

typescript
toggleImmersive(): void;
switchGreeting(index: number): void;
copyToClipboard(text: string): void;
showToast(message: string, type?: "success" | "error" | "info"): void;
resolveAssetUrl(ref: string): string;
renderMarkdown(text: string): string;

模型选择

typescript
setModel(modelId: string): void;
getModels(): Promise<{
  models: Array<{ id: string; name: string; provider: string; contextLength: number }>;
  pinnedModels: string[];
  recentlyUsed: string[];
}>;
pinModel(modelId: string): void;
unpinModel(modelId: string): void;
setPreferredProvider(provider: "official" | "private"): Promise<{
  ok: boolean;
  provider?: string;
  error?: string;
}>;

内置组件

作为全局变量可用——无需导入。import 语句会在编译时被静默剥离,因此两种写法都可以,但组件会被自动注入到作用域中:

tsx
// 这些已经在作用域中——直接使用即可:
// Chat, MessageList, MessageInput, ChatCanvas,
// ModelPickerModal, ModelTrigger, useAssetFont, Icons

// import 语句无害(编译时被剥离)但不必要:
// import { Chat } from "yumina/Chat";  ← 可用但不需要

Chat Props

typescript
interface ChatProps {
  renderBubble?: (props: BubbleProps) => React.ReactNode;
  className?: string;
  children?: React.ReactNode;
}

BubbleProps

typescript
interface BubbleProps {
  contentHtml: string;
  content: string;
  rawContent: string;
  role: "user" | "assistant" | "system";
  messageIndex: number;
  isStreaming: boolean;
  stateSnapshot: Record<string, unknown> | null;
  variables: Record<string, unknown>;
  renderMarkdown: (text: string) => string;
}

SandboxMessage

typescript
interface SandboxMessage {
  id: string;
  sessionId: string;
  role: "user" | "assistant" | "system";
  content: string;
  status?: "complete" | "streaming" | "failed";
  errorMessage?: string | null;
  stateChanges?: Record<string, unknown> | null;
  stateSnapshot?: Record<string, unknown> | null;
  swipes?: Array<{ content: string; stateSnapshot?: Record<string, unknown> | null }>;
  activeSwipeIndex?: number;
  model?: string | null;
  tokenCount?: number | null;
  generationTimeMs?: number | null;
  compacted?: boolean;
  attachments?: Array<{ type: string; mimeType: string; name: string; url: string }> | null;
  createdAt: string;
}

SandboxEntry

typescript
interface SandboxEntry {
  id: string;
  name: string;
  content: string;
  keywords: string[];
  position: number;
  section: "system-presets" | "examples" | "chat-history" | "post-history";
  enabled: boolean;
  role: string;
  tags?: string[];
}

全局 API(非 React)

js
window.yumina              // 与 useYumina() 相同的 API
window.yumina.onChange(cb)  // 订阅状态变化,返回取消订阅函数
window.yumina.offChange(cb) // 取消订阅
// 同时在 window 上派发 "yumina:statechange" 事件

沙箱环境

React 作为全局变量可用(无需导入)。使用 React.useStateReact.useEffect 等。

限制

  • 不能使用 fetch / XMLHttpRequest
  • 不能直接使用 localStorage / sessionStorage(请改用 storage.* API)
  • 不能操作 window.location(请使用 navigate()
  • 不能访问 window.parent
  • 不能使用 eval / new Function
  • 不能访问 cookie

兼容性垫片

没有使用 SDK 的旧版世界代码仍然可以使用:

  • fetch('/api/*') → 通过父窗口代理,带凭证
  • localStorage / sessionStorage → 世界级别作用域,代理到父窗口
  • navigator.clipboard.writeText() → 代理
  • window.location → 合成对象

样式

Tailwind CSS 在沙箱中完全可用——使用任何工具类(flexgap-4text-whitebg-[#1a1a2e] 等)。行内样式同样有效:

tsx
// Tailwind 类(推荐)
<div className="flex flex-col gap-4 p-4 bg-[#1a1a2e] text-[#e0e0e0] font-serif">

// 行内样式(也可以)
var style = {
  background: "#1a1a2e",
  color: "#e0e0e0",
  fontFamily: '"Noto Serif SC", serif',
  padding: "16px",
};

多文件结构

tsx
// index.tsx
import StatusPanel from "./status-panel";
import MapView from "./map-view";

export default function App() {
  return (
    <>
      <StatusPanel />
      <Chat />
      <MapView />
    </>
  );
}