# 世界 Schema Yumina 世界以一个名为 `WorldDefinition` 的 JSON 对象存储。以下是完整的 Schema。 ## 顶层结构 ```json { "id": "uuid", "version": "20.0.0", "name": "World Name", "description": "Short description", "author": "Creator Name", "language": "en", "entries": [], "variables": [], "rules": [], "reactions": [], "rootComponent": null, "components": [], "audioTracks": [], "bgmPlaylist": null, "conditionalBGM": [], "customUI": [], "entryFolders": [], "customTags": [], "customTagColors": {}, "editorMode": "advanced", "settings": { "maxTokens": 12000, "maxContext": 200000, "temperature": 1.0, "topP": 1, "frequencyPenalty": 0, "presencePenalty": 0, "playerName": "User", "lorebookScanDepth": 2, "lorebookRecursionDepth": 0 } } ``` ## 字段参考 ### 身份信息 | 字段 | 类型 | 描述 | |------|------|------| | `id` | string | UUID,自动生成 | | `version` | string | Schema 版本,当前为 `"20.0.0"` | | `name` | string | 世界名称(1-200 字符) | | `description` | string | 简短描述(0-10,000 字符) | | `author` | string | 创作者显示名 | | `language` | string(可选) | BCP 47 语言代码(`"en"`、`"zh"`、`"ja"` 等)。存在于 TypeScript 接口中,但在 Zod Schema 中为可选。 | ### 内容 | 字段 | 类型 | 描述 | |------|------|------| | `entries` | WorldEntry[] | AI 在生成时读取的所有内容——角色、设定、规则、开场白 | | `variables` | Variable[] | 带有行为规则的游戏状态定义 | | `rules` | Rule[] | WHEN/IF/THEN 自动化触发器 | | `reactions` | Reaction[](可选) | 基于事件模式的规则(较新的系统)。可以为 undefined。 | ### 展示层 | 字段 | 类型 | 描述 | |------|------|------| | `rootComponent` | RootComponent \| null | 自定义 UI 虚拟文件系统(React/TSX) | | `components` | GameComponent[] | 内置 UI 组件(状态栏等) | | `audioTracks` | AudioTrack[] | BGM、音效和环境音轨 | | `bgmPlaylist` | BGMPlaylist \| null | 自动播放音乐配置 | | `conditionalBGM` | ConditionalBGM[] | 基于状态触发的音乐 | | `customUI` | CustomUIComponent[] | 旧版 UI 系统(请改用 rootComponent) | ### 组织结构 | 字段 | 类型 | 描述 | |------|------|------| | `entryFolders` | EntryFolder[] | 用于组织条目的文件夹结构 | | `customTags` | string[] | 创作者自定义的条目标签 | | `customTagColors` | Record\ | 自定义标签的 Tailwind 颜色类 | | `editorMode` | `"simple"` \| `"advanced"` | 编辑器复杂度级别 | ### 设置 | 字段 | 类型 | 默认值 | 描述 | |------|------|--------|------| | `maxTokens` | number | 12000 | 每次 AI 回复的最大 token 数 | | `maxContext` | number | 200000 | 最大上下文窗口大小 | | `temperature` | number | 1.0 | AI 创造力(0.0-2.0) | | `topP` | number | 1 | 核采样阈值 | | `frequencyPenalty` | number | 0 | 重复 token 惩罚 | | `presencePenalty` | number | 0 | 已使用 token 惩罚 | | `playerName` | string | "User" | 默认的 `{{user}}` 替换值 | | `lorebookScanDepth` | number | 2 | 扫描最近多少轮消息以匹配关键词 | | `lorebookRecursionDepth` | number | 0 | 被触发的条目再次触发其他条目的递归层数 | | `topK` | integer(可选) | — | 限制候选 token 数量(最小值:0) | | `minP` | float(可选) | — | 最小概率阈值(0-1) | | `structuredOutput` | boolean(可选) | false | 强制 JSON 输出格式 | --- # 条目与分区 条目是 AI 在生成每次回复时读取的独立内容块。每个条目指定了它包含**什么**内容、AI **何时**看到它、以及它被放置在提示词中的**哪个位置**。 ## 条目 Schema ```json { "id": "unique-kebab-case-id", "name": "Display Name", "content": "The text content the AI reads...", "role": "character", "apiRole": "system", "section": "system-presets", "position": 0, "enabled": true, "keywords": [], "conditions": [], "conditionLogic": "all", "depth": null, "matchWholeWords": false, "secondaryKeywords": [], "secondaryKeywordLogic": "AND_ANY", "preventRecursion": false, "excludeRecursion": false, "tags": [], "folderId": null } ``` ## 分区 条目被分为四个投递区域: ### `system-presets` — 始终发送 每次 AI 调用都会包含。用于存放必要的世界内容。 - `alwaysSend`:true(自动) - 按 `position` 升序排列 - 放置在 AI 上下文的顶部 ### `examples` — 对话示例 解析为用户/助手消息对,带有 `[Example Chat]` 标记。向 AI 展示角色应该如何说话。 - `alwaysSend`:true(自动) - 内容格式:示例之间使用 `` 分隔符,各行与角色名交替排列 ### `chat-history` — 关键词触发,深度注入 仅当关键词匹配到近期消息时才包含。在对话历史中的特定深度位置注入。 - `alwaysSend`:false(自动) - `depth`:从末尾算起的消息数,该条目在此位置插入(0 = 紧接最新消息之前) - `keywords`:触发词数组(任何匹配即激活) ### `post-history` — 最终强调 放置在所有聊天消息之后——AI 生成前的最后一段内容。获得最多的注意力。 - `alwaysSend`:true(自动) - 常见用途:输出格式指令、风格强制、CoT 绕过 ## 条目角色 | 角色 | 用途 | |------|------| | `system` | 叙述者指令、游戏规则、世界机制 | | `character` | 角色描述和性格 | | `personality` | 角色性格特征(与描述分开) | | `scenario` | 场景设定、情境、场景上下文 | | `lore` | 世界历史、阵营、背景知识 | | `plot` | 故事推进、任务线索、事件 | | `style` | 写作风格、语气、排版 | | `example` | 对话示例(与 `examples` 分区搭配使用) | | `greeting` | 会话开始时的第一条消息(特殊处理) | | `custom` | 不属于以上任何类别的内容 | ## API 角色 `apiRole` 字段控制条目以什么 LLM 消息角色发送: | apiRole | 发送为 | 使用场景 | |---------|--------|---------| | `system`(默认) | 系统消息 | 大多数条目 | | `user` | 用户消息 | CoT 绕过提示、伪造用户上下文 | | `assistant` | 助手消息 | 示例回复、语气设定 | ## 关键词匹配 用于 `chat-history` 条目: ### 主关键词 字符串数组。**任何**匹配即触发该条目。 ```json "keywords": ["tavern", "inn", "bar", "drink"] ``` ### 次关键词 带逻辑运算符的附加过滤器: | 逻辑 | 含义 | |------|------| | `AND_ANY` | 主关键词匹配 且 至少一个次关键词匹配 | | `AND_ALL` | 主关键词匹配 且 所有次关键词都匹配 | | `NOT_ANY` | 主关键词匹配 且 没有任何次关键词匹配 | | `NOT_ALL` | 主关键词匹配 且 并非所有次关键词都匹配 | ### 递归控制 - `preventRecursion: true` — 该条目的内容不会触发其他条目 - `excludeRecursion: true` — 该条目不能被其他条目的内容触发(只能被玩家/AI 消息触发) ## 条件 条目可以设置基于状态的条件,只有条件为真时才会被包含: ```json "conditions": [ { "variableId": "story-phase", "operator": "eq", "value": "act2" } ], "conditionLogic": "all" ``` 运算符:`eq`、`neq`、`gt`、`lt`、`gte`、`lte`、`contains` ## 官方预设 Yumina 包含 5 个可选的内置预设,创作者可以启用: | 预设 | 分区 | 位置 | 用途 | |------|------|------|------| | Fiction Mode | system-presets | 0 | 自然地与所有内容互动 | | Task | system-presets | 1 |「你是叙述者」指令 | | Instructions | system-presets | 2 | 展示而非讲述、场景中段结束 | | Style | system-presets | 3 | 角色拥有独立的声音 | | CoT Bypass | post-history | 0 | 后置历史越狱(user 角色) | ## 提示词组装顺序 引擎收集条目并按以下顺序组装: 1. **System Presets** 条目(按 position 升序排列) 2. **活跃人设**块(玩家姓名、外貌、背景故事) 3. **静态格式**块(行为规则、指令语法、音轨列表) 4. **Example** 条目(解析为对话对) 5. *— 缓存断点 —* 6. **已触发**的 system-block 条目(关键词匹配) 7. `[Start a new Chat]` 标记 8. **故事压缩**摘要(压缩的旧消息) 9. **会话记忆**块 10. **聊天历史**与深度注入的条目 11. **待处理上下文**效果(来自上一轮规则) 12. **动态格式**块(当前变量值) 13. **Post-history** 条目(按 position 排列) 越靠近底部的内容获得越多的 AI 注意力。 --- # 变量与指令 变量是世界的游戏状态。AI 每轮读取当前值,并在其回复中写入方括号指令来更新它们。 ## 变量 Schema ```json { "id": "health", "name": "Health", "type": "number", "defaultValue": 100, "min": 0, "max": 100, "description": "Player's physical health", "category": "stat", "behaviorRules": "0 = death. 1-20 = critical. 20-50 = wounded. 50-80 = bruised. 80-100 = healthy. Decrease on physical damage: punch -5 to -10, slash -15 to -25, fall -20 to -40. Rest +5, healing +10 to +30. Max change per turn: 30." } ``` ## 变量类型 ### `number` 带可选最小/最大边界的数值。 **指令语法:** ``` [health: set 50] → 设置为 50 [health: +10] → 加 10(别名:add) [health: -15] → 减 15(别名:subtract) [health: *2] → 乘以 2(别名:multiply) [gold: 100] → 隐式设置(无运算符) ``` ### `string` 文本值。 **指令语法:** ``` [location: set "dark forest"] [mood: set "suspicious"] [notes: append " Found a clue."] ``` ### `boolean` 布尔标志(true/false)。 **指令语法:** ``` [has-key: toggle] → 翻转 true ↔ false [met-elder: set true] [quest-active: set false] ``` ### `json` 复杂结构——对象和数组。支持嵌套点路径更新。 **指令语法:** ``` [inventory: push {"name": "Iron Sword", "damage": 10}] [inventory: delete 0] [inventory: set [{"name": "Potion", "qty": 3}]] [npcs.aria.affinity: +5] [npcs.aria.mood: set "happy"] [npcs: merge {"aria": {"trust": 80}}] [quest-log: push {"id": "q1", "status": "active"}] [config: delete "deprecated-key"] ``` **点路径操作:** - `[root.nested.field: op value]` — 更新 JSON 变量中的嵌套字段 - 如果中间对象不存在,会自动创建 - 数组索引访问:`[inventory.0.durability: -1]` ## 所有操作 | 操作 | 适用类型 | 语法 | 行为 | |------|---------|------|------| | `set` | 全部 | `[id: set value]` 或 `[id: value]` | 替换值 | | `add` / `+` | number | `[id: +10]` 或 `[id: add 10]` | 增加 | | `subtract` / `-` | number | `[id: -5]` 或 `[id: subtract 5]` | 减少 | | `multiply` / `*` | number | `[id: *2]` 或 `[id: multiply 2]` | 倍增 | | `toggle` | boolean | `[id: toggle]` | 翻转 true/false | | `append` | string | `[id: append " text"]` | 拼接 | | `merge` | json(对象) | `[id: merge {"key": "val"}]` | 浅合并 | | `push` | json(数组) | `[id: push "item"]` 或 `[id: push {...}]` | 追加到数组 | | `delete` | json | `[id: delete "key"]` 或 `[id: delete 0]` | 删除键/索引 | ## 音频指令 音频通过单独的指令格式触发: ``` [audio: track-id play] [audio: track-id stop] [audio: track-id crossfade 2.5] [audio: track-id volume 0.5] [audio: track-id play chain:next-track] ``` ## 行为规则 `behaviorRules` 字段是纯文本,会被注入到 AI 的系统提示中作为 `` 块。它教导 AI 何时以及如何更新变量。 **有效的行为规则应包含:** - 每个数值范围在叙事中的含义 - 什么事件触发变化(以及变化方向) - 幅度指导(变化多少) - 限制(每轮最大变化量、绝对边界) - 与其他变量的关联关系 **示例 — 复杂 JSON 变量:** ``` "behaviorRules": "Array of party members. Push new object when recruiting: {\"name\": \"...\", \"class\": \"...\", \"trust\": 50}. Update trust via dot-path: [allies.0.trust: +5]. Remove via index: [allies: delete 0]. Trust < 20 = may betray." ``` ## 变量分类 > **注意:** `category` 被 Studio AI 工具接受,用于组织目的,但它**不是**世界 Schema 的一部分,在验证时会被剥离。它不会出现在 Zod Schema 中。 | 分类 | 典型用途 | |------|---------| | `stat` | 生命值、法力值、耐力、等级 | | `inventory` | 物品、装备、资源 | | `resource` | 金币、能量、食物、材料 | | `flag` | 任务完成、已见 NPC、已发现地点 | | `relationship` | NPC 好感度、阵营声望、信任度 | | `custom` | 其他任何内容 | ## ID 规范 - 使用 **kebab-case**:`player-health`、`day-count`、`npc-trust` - ID 在世界内必须唯一 - ID 会成为宏名称:变量 `player-health` 可在条目中通过 `{{player-health}}` 访问 - 避免使用保留字:`audio`、`user`、`char`、`time`、`date` --- # 规则与响应 规则在回合之间基于事件自动触发。它们处理精确的游戏机制,无需 AI 参与。 ## 规则 Schema ```json { "id": "low-health-warning", "name": "Low Health Warning", "description": "Warn the player when health is critical", "trigger": { "type": "variable-crossed", "variableId": "health", "direction": "drops-below", "threshold": 20 }, "conditions": [], "conditionLogic": "all", "actions": [ { "type": "notify-player", "style": "warning", "message": "Your vision blurs. You're barely standing." }, { "type": "inject-directive", "directiveId": "critical-health", "content": "The player is near death. Describe their physical deterioration — stumbling, blurred vision, trembling hands.", "position": "after_char", "persistent": true } ], "priority": 50, "enabled": true, "cooldownTurns": null, "maxFireCount": null } ``` ## 触发器类型 | 类型 | 字段 | 触发时机 | |------|------|---------| | `variable-crossed` | `variableId`、`direction`(`rises-above` \| `drops-below`)、`threshold` | 变量越过阈值时 | | `state-change` | `variableId`(可选) | 任何变量发生变化时(或指定变量变化时) | | `turn-count` | `atTurn`(number)、`everyNTurns`(number) | 在特定回合数时,或每隔 N 回合 | | `session-start` | — | 新会话的第一条消息 | | `keyword` | `keywords[]` | 玩家消息包含任何关键词 | | `ai-keyword` | `keywords[]` | AI 回复包含任何关键词 | | `every-turn` | — | 每个回合之后 | | `action` | `actionId`(string) | 特定动作被执行时(例如来自自定义 UI 按钮) | | `manual` | — | 仅在被另一个规则触发时才执行 | ## 条件 可选的状态检查,规则触发前必须通过: ```json "conditions": [ { "variableId": "story-phase", "operator": "eq", "value": "act2" }, { "variableId": "health", "operator": "gt", "value": 0 } ] ``` 运算符:`eq`、`neq`、`gt`、`lt`、`gte`、`lte`、`contains` `conditionLogic`:`"all"`(AND)或 `"any"`(OR) ## 动作类型 ### `modify-variable` 修改变量的值。 ```json { "type": "modify-variable", "variableId": "hunger", "operation": "subtract", "value": 5 } ``` 操作:`set`、`add`、`subtract`、`multiply`、`toggle`、`append`、`merge`、`push`、`delete` ### `inject-directive` 为后续回合向 AI 的提示词中添加指令。 ```json { "type": "inject-directive", "directiveId": "romance-mode", "content": "The character has developed feelings. Write romantic tension naturally.", "position": "after_char", "persistent": true } ``` 位置选项:`top`、`before_char`、`after_char`、`auto`、`depth`、`bottom` `persistent: true` 使指令持续有效直到被明确移除。`persistent: false`(默认)= 一次性,一轮后移除。 `duration`:可选数值。设置后,指令会在 N 轮后自动移除。 ### `remove-directive` 移除之前注入的指令。 ```json { "type": "remove-directive", "directiveId": "romance-mode" } ``` ### `notify-player` 显示弹窗通知。 ```json { "type": "notify-player", "style": "warning", "message": "You're running low on supplies." } ``` 样式:`info`、`achievement`、`warning`、`danger` ### `play-audio` 触发音频音轨。 ```json { "type": "play-audio", "trackId": "bgm-battle", "action": "play" } ``` ### `toggle-entry` 启用或禁用世界书条目。 ```json { "type": "toggle-entry", "entryId": "secret-lore", "enabled": true } ``` ### `toggle-rule` 启用或禁用另一条规则(链式反应)。 ```json { "type": "toggle-rule", "ruleId": "phase-2-triggers", "enabled": true } ``` ### `send-context` 在下一轮向 AI 发送一次性上下文消息。 ```json { "type": "send-context", "message": "The player just triggered a hidden event. React accordingly.", "role": "system" } ``` `role`:`"system"`(默认)或 `"user"` — 决定上下文如何注入到对话中。 ## 规则选项 | 字段 | 类型 | 描述 | |------|------|------| | `priority` | number | 值越高越先触发,当多条规则同时触发时生效(默认:0) | | `cooldownTurns` | number \| null | 两次触发之间的最小回合数 | | `maxFireCount` | number \| null | 该规则总共可触发的次数(null = 无限) | | `enabled` | boolean | 可被其他规则切换 | ## Reactions(事件模式系统) Reactions 是一套更新、更灵活的规则系统,基于通用事件模式: ```json { "id": "zone-enter-forest", "name": "Enter Forest", "when": { "eventType": "spatial:zone-enter", "zone": "forest" }, "conditions": [], "conditionLogic": "all", "then": [ { "type": "set", "path": "weather", "value": "foggy" }, { "type": "set", "path": "@audio.ambient", "value": "forest-ambient", "operation": "set" }, { "type": "emit", "event": { "type": "spatial:ambience-changed" } } ] } ``` ### 系统效果路径 Reactions 可以通过 `@` 路径操作系统功能: | 路径 | 效果 | |------|------| | `@audio.bgm` | 播放 BGM 音轨 | | `@audio.sfx` | 播放音效 | | `@audio.ambient` | 播放环境音 | | `@audio.stop` | 停止音轨 | | `@prompt.directive.` | 注入/移除指令 | | `@prompt.entry.` | 切换条目可见性 | | `@prompt.context` | 一次性上下文消息 | | `@rules.disabled.` | 切换规则的启用/禁用状态 | | `@ui.notification` | 显示弹窗通知 | | `@ai.request` | 触发 AI 生成 | | `@ai.context` | 为下一条 AI 消息添加上下文 | ## 每轮生成的事件 在 AI 回复之后,以下事件会被发出并与所有规则/reactions 进行匹配: 1. `message:user` — 玩家的消息(包含用于关键词匹配的内容) 2. `message:ai` — AI 的回复(包含用于关键词匹配的内容) 3. `turn:complete` — 回合结束(包含回合计数) 4. `state:changed` — 每个发生变化的变量各一个事件(包含 variableId、oldValue、newValue) --- # 宏 宏是条目内容中的 `{{placeholder}}` 占位符,在运行时被替换。它们适用于所有分区的所有条目类型。 ## 内置宏 ### 身份 | 宏 | 替换为 | |----|--------| | `{{char}}` | 角色名(来自世界设置) | | `{{user}}` | 玩家名(感知人设:活跃人设名 → 账户用户名 → "Player") | ### 人设 | 宏 | 替换为 | |----|--------| | `{{persona}}` | 所有人设字段合并 | | `{{persona_name}}` | 活跃人设的名称 | | `{{persona_appearance}}` | 活跃人设的外貌描述 | | `{{persona_personality}}` | 活跃人设的性格描述 | | `{{persona_backstory}}` | 活跃人设的背景故事 | ### 时间 | 宏 | 替换为 | |----|--------| | `{{time}}` | 当前时间(HH:MM 格式) | | `{{date}}` | 当前日期(人类可读格式) | | `{{weekday}}` | 当前星期几 | | `{{isodate}}` | ISO 8601 日期 | | `{{isotime}}` | ISO 8601 时间 | | `{{idle}}` | 距上次玩家消息的时间(人类可读格式,例如「5 分钟」) | ### 游戏状态 | 宏 | 替换为 | |----|--------| | `{{turnCount}}` | 当前回合数 | | `{{model}}` | 当前 LLM 模型 ID | | `{{lastMessage}}` | 最近一条消息的内容 | | `{{lastUserMessage}}` | 最近一条玩家消息的内容 | | `{{lastCharMessage}}` | 最近一条 AI 消息的内容 | ### 随机化 | 宏 | 替换为 | |----|--------| | `{{random::a::b::c}}` | 从选项中随机选择(每轮重新随机) | | `{{pick::a::b::c}}` | 确定性选择(有种子,在同一轮内稳定) | | `{{roll::NdS}}` | 掷骰子结果,例如 `{{roll::2d6+1}}` | ### 实用工具 | 宏 | 替换为 | |----|--------| | `{{// comment}}` | 被移除(条目中的不可见注释) | | `{{trim}}` | 折叠周围的空白字符 | ### 变量宏 任何变量 ID 都可以作为宏使用: ``` {{health}} → "health" 变量的当前值 {{location}} → "location" 变量的当前值 {{inventory}} → "inventory" 变量的 JSON.stringify 结果 ``` 如果宏不匹配任何内置名称或变量 ID,它将原样保留(字面值 `{{unknown}}`)。 --- # 自定义 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 ; } ``` 自定义消息气泡: ```tsx import Bubble from "./bubble"; export default function App() { return ; } ``` 完整应用模式: ```tsx export default function App() { var api = useYumina(); return (
HP: {api.variables.health}
); } ``` ## useYumina() — 完整 API 参考 ### 状态读取 ```typescript interface SandboxedYuminaAPI { // 游戏状态 variables: Record; globalVariables: Record; // 世界信息 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; 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; deleteMessage(messageId: string): Promise; regenerateMessage(messageId: string): void; continueLastMessage(): void; stopGeneration(): void; restartChat(): void; swipeMessage(messageId: string, direction: "left" | "right"): Promise>; setComposerDraft(text: string): void; clearPendingChoices(): void; ``` ### 会话管理 ```typescript revertToMessage(messageId: string): Promise; branchFromMessage(messageId: string): Promise; getBranchContext(): Promise; createSession(worldId: string): Promise; deleteSession(sessionId: string): Promise; listSessions(worldId: string): Promise>>; navigate(path: string): void; ``` ### 存档点 ```typescript saveCheckpoint(): Promise; loadCheckpoints(): Promise; restoreCheckpoint(checkpointId: string): Promise; deleteCheckpoint(checkpointId: string): Promise; ``` ### 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; ``` 直接调用 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; storage.set(key: string, value: string): Promise; storage.remove(key: string): Promise; ``` ### 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 | null; variables: Record; 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 | null; stateSnapshot?: Record | null; swipes?: Array<{ content: string; stateSnapshot?: Record | 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.useState`、`React.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 在沙箱中完全可用——使用任何工具类(`flex`、`gap-4`、`text-white`、`bg-[#1a1a2e]` 等)。行内样式同样有效: ```tsx // Tailwind 类(推荐)
// 行内样式(也可以) 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 ( <> ); } ``` --- # 音频 音频音轨提供背景音乐、音效和环境音。它们可以通过 AI 指令、规则或自动播放列表触发。 ## AudioTrack Schema ```json { "id": "bgm-tavern", "name": "Tavern Theme", "type": "bgm", "url": "@asset:uuid-here", "loop": true, "volume": 0.4, "fadeIn": 2, "fadeOut": 1.5, "maxDuration": null } ``` | 字段 | 类型 | 描述 | |------|------|------| | `id` | string | 唯一的 kebab-case ID | | `name` | string | 显示名称 | | `type` | `"bgm"` \| `"sfx"` \| `"ambient"` | 音轨类型 | | `url` | string | 音频来源 — `@asset:{id}` 或直接 URL | | `loop` | boolean | 是否循环播放(通常:BGM/环境音为 true,音效为 false) | | `volume` | number | 0.0 到 1.0 | | `fadeIn` | number \| null | 淡入时长,单位秒 | | `fadeOut` | number \| null | 淡出时长,单位秒 | | `maxDuration` | number \| null | N 秒后自动停止 | ## AI 音频指令 AI 通过方括号指令触发音频: ``` [audio: bgm-tavern play] [audio: sfx-sword play] [audio: bgm-tavern stop] [audio: bgm-battle crossfade 2.5] [audio: ambient-rain volume 0.3] [audio: sfx-magic play chain:bgm-ambient] ``` | 动作 | 描述 | |------|------| | `play` | 开始播放音轨 | | `stop` | 停止音轨 | | `crossfade ` | 从当前 BGM 交叉淡入到此音轨 | | `volume <0-1>` | 更改音轨音量 | | `play chain:` | 播放此音轨,然后自动播放下一首 | ## BGM 播放列表 自动播放一系列 BGM 音轨: ```json { "bgmPlaylist": { "tracks": ["bgm-explore-1", "bgm-explore-2", "bgm-explore-3"], "playMode": "shuffle", "gapSeconds": 0, "autoPlay": true, "waitForFirstMessage": true } } ``` | 字段 | 类型 | 描述 | |------|------|------| | `tracks` | string[] | 要循环播放的音轨 ID 数组 | | `playMode` | `"loop"` \| `"shuffle"` \| `"sequential"` | 播放顺序模式 | | `gapSeconds` | number | 音轨之间的静音间隔(默认:0,最大:30) | | `autoPlay` | boolean | 是否自动开始播放(默认:true) | | `waitForFirstMessage` | boolean | 等到玩家发送第一条消息后再开始播放 | ## 条件 BGM 当条件满足时播放特定音轨。支持变量检查、关键词匹配、回合计数和会话开始触发器。 ```json { "conditionalBGM": [ { "id": "combat-music", "name": "Combat Music", "targetTrackId": "bgm-battle", "triggerType": "variable", "conditions": [ { "variableId": "in-combat", "operator": "eq", "value": true } ], "conditionLogic": "all", "priority": 10, "fadeInDuration": 1, "fadeOutDuration": 2, "stopPreviousBGM": true, "fallback": "default" } ] } ``` | 字段 | 类型 | 描述 | |------|------|------| | `id` | string | 唯一标识符 | | `name` | string | 显示名称 | | `targetTrackId` | string | 触发时播放的音轨 | | `triggerType` | `"variable"` \| `"ai-keyword"` \| `"keyword"` \| `"turn-count"` \| `"session-start"` | 触发条件 BGM 的事件类型 | | `conditions` | Condition[] | 要检查的变量条件 | | `conditionLogic` | `"all"` \| `"any"` | 多个条件的组合方式(默认:`"all"`) | | `keywords` | string[] | 要匹配的关键词(用于 `keyword` / `ai-keyword` 触发器) | | `matchWholeWords` | boolean | 仅匹配完整单词(用于关键词触发器) | | `atTurn` | number | 在特定回合触发(用于 `turn-count` 触发器) | | `everyNTurns` | number | 每隔 N 回合触发(用于 `turn-count` 触发器) | | `priority` | number | 多个触发器匹配时,优先级高的胜出 | | `fadeInDuration` | number | 淡入时长,单位秒 | | `fadeOutDuration` | number | 淡出时长,单位秒 | | `stopPreviousBGM` | boolean | 是否先停止当前播放的 BGM | | `fallback` | `"default"` \| `"previous"` \| trackId 字符串 | 条件变为 false 时播放什么 | 条件运算符使用缩写形式:`eq`、`neq`、`gt`、`lt`、`gte`、`lte`、`contains`。 当条件变为 true 时,音轨开始播放。当条件变为 false 时,音轨停止(如果配置了淡出则带淡出效果),并根据 `fallback` 设置回退。