属性
SDK 的状态模块 —— 世界、玩家、角色、关系、物品、扩展属性、存档,以及完整状态读取
SDK 提供 8 个分模块 + 1 个全量读取接口(storvia.state),用于读写游戏中的各种数据。前 5 个模块的数据会自动注入 AI 提示词,AI 每轮回复时也会自动更新。custom 默认不注入 AI,可通过创作台开启「AI 追踪」选择性注入。save 完全独立、永不注入 AI,专供游戏运行状态持久化。
| 模块 | AI 感知 | 数据结构 |
|---|---|---|
storvia.world | 自动注入 | 扁平 KV 对象 |
storvia.player.state | 自动注入 | 按分组的 KV |
storvia.characters.state | 自动注入 | 角色名 → 按分组的 KV |
storvia.relationships | 自动注入 | 关系数组 |
storvia.inventory | 自动注入 | 分组 → 物品 Record(上限 200 件) |
storvia.custom | 可选注入 | 文本/数值(可分组,上限 100 字段) |
storvia.save | 永不注入 | 任意 JSON(独立配额,≤ 256KB) |
storvia.memory | 自动注入 | 永久记忆文本 |
storvia.state | — | 一次性返回上述所有模块的合并快照 |
前 5 个模块的字段定义需要在创作台中配置(和深度创作一样),配置方法参考 深度创作。这里介绍的是如何通过 SDK 在游戏代码中读写数据。
角色数量上限:每个会话最多 10 个角色处于活跃状态、可被 AI 看到。当超过上限时,系统会按 priority 从低到高把多余角色移入休眠区(dormant)。休眠的角色不再注入 AI 提示词、AI 也不会更新它们的属性,但数据仍保留。可以通过 storvia.characters.setPriority() 调整排序,让某个角色重新进入活跃区。
storvia.world — 世界状态
存储全局环境信息。世界状态只有以下 5 个固定字段,由创作台按需启用,不支持自定义字段:
| 字段 | 说明 |
|---|---|
time | 时间(如"晚上 21:00") |
date | 日期(如"2024年3月15日") |
location | 地点(如"秦照野的直播间") |
region | 地区(如"江南水乡") |
weather | 天气(如"小雨") |
get() 返回当前存储的世界状态,update() 只接受上表中的 5 个字段,传入其他字段名会报错。
world.get()
获取当前世界状态。
const world = await storvia.world.get();
// { time: "晚上 21:00", location: "秦照野的直播间" }world.update(data)
更新世界状态。传入的字段会与已有数据合并,未传入的字段保持不变。服务端只校验字段名是否合法(仅限上表 5 个字段,传入其他字段名会报错),不检查创作台是否启用了该字段——合法字段都会合并入库。
await storvia.world.update({
time: "深夜 23:30",
location: "回到家中",
});storvia.player — 玩家
player.get()
获取玩家基本信息(名称、性别、描述、头像)。
const player = await storvia.player.get();
// { name: "小明", gender: "male", description: "冒险者", avatar: "https://..." }player.update(data)
更新用户人设。传入字段会合并到现有数据,未传入的字段保持不变。name + description 合计上限 5000 字符。
await storvia.player.update({
name: "小明",
gender: "male",
description: "22岁的新晋制作人",
avatar: "https://example.com/player.png", // 可选
});player.state.get()
获取玩家状态。不传参数返回全部分组,传分组名返回该组数据。
字段值为 RichFieldValue 对象,包含 value(当前值)、type(字段类型)以及可选的 min/max(数值范围)。可使用 SDK 导出的 extractFieldValue() 工具函数提取原始值。
作者在创作台配置初始属性值时,可以留空不填。留空的字段在游戏开局不会出现在
state 里,聊天页面也不会显示。等到 AI 在对话中首次赋值(或 SDK 调用
player.state.update())后才会开始展示。这种"渐进式揭示"适合做好感度、剧情开关等属性。
import { extractFieldValue } from "@storvia/sdk";
// 获取全部分组
const allState = await storvia.player.state.get();
// {
// "经济": {
// "花园币": { value: 500, type: "number", min: 0, max: 99999 },
// "等级": { value: 3, type: "number", min: 1, max: 100 }
// },
// "数据": {
// "粉丝": { value: 120, type: "number" }
// }
// }
// 提取原始值
const coins = extractFieldValue(allState["经济"]["花园币"]); // 500
// 获取特定分组
const economy = await storvia.player.state.get("经济");player.state.update(group, values)
更新指定分组的玩家状态。传入的字段与该组已有数据合并。只需传简单值,系统会自动保留字段的元数据(type/min/max)并做范围钳位。
await storvia.player.state.update("经济", {
花园币: 450, // 扣了 50 币,系统自动钳位到 [0, 99999]
经验: 180, // 加了 30 经验
});分组名和字段名必须和创作台中配置的完全一致(包括中英文、空格等)。
player.state.remove(group, keys)(SDK 0.9.8 起)
删除指定分组下的字段。字段不存在、分组不存在都不会报错;传空数组直接返回、不发请求。
await storvia.player.state.remove("状态", ["中毒", "灼烧"]);删光组内字段后,分组本身仍然保留(面板上会剩一个空分组)。要连分组一起去掉用下面这个。
player.state.removeGroup(group)(SDK 0.9.8 起)
删除整个分组,连同组内所有字段。分组不存在时是 no-op。
await storvia.player.state.removeGroup("临时增益");删的是当前存档里的数据,不是创作台的字段定义。所以创作台配过的字段/分组删掉后还会回来,有两条路径:① AI 在后续某轮里又输出了这个字段;② 玩家点「同步最新设定」时,配置里存在而存档里缺失的分组会按初始值重建。
这两个方法适合清理运行时产生的临时字段 / 分组,不适合用来"关掉"创作台配置的属性。删除本身不可撤销。
storvia.characters — 角色
characters.list()
获取所有角色列表。预设角色的 description 恒返回空字符串(作者人设仅 AI 可见,不对玩家暴露),用户可通过 userDescription 补充。创作台里为预设角色上传的头像会随 avatar 字段返回完整可访问的 URL。
const chars = await storvia.characters.list();
// [{
// name: "秦照野", // 角色名
// avatar: "https://...", // 头像
// description: "", // 预设角色固定为空字符串(作者人设仅 AI 可见);自建角色为用户撰写
// introduction: "对外介绍文本", // 仅预设角色有意义,作者资产,SDK 只读
// visibleAtStart: true, // 开局是否出场,仅预设角色有意义,作者资产,SDK 只读
// userDescription: "对我温柔一些", // 用户补充人设
// gender: "male", // "male" | "female" | "other";老数据可能没有
// isPreset: true // 是否为作者预设角色
// }, ...]characters.get(name)
获取单个角色 信息(含人设)。
const char = await storvia.characters.get("秦照野");
// { name, avatar, description, introduction, visibleAtStart, userDescription, gender, isPreset }characters.add(character)
添加新 角色。gender 必填,取值 'male' | 'female' | 'other'。
await storvia.characters.add({
name: "新角色",
description: "一个神秘的路人",
gender: "other", // 必填
avatar: "img/stranger.png", // 可选
});characters.update(name, data)
更新 角色信息。规则:
- 预设角色(
isPreset: true):description(作者设定)不可修改;userDescription(用户补充)、avatar、gender可修改 - 非预设角色:
description、userDescription、avatar、gender均可修改 introduction/visibleAtStart是作者预设资产,所有角色都不可通过 SDK 修改(仅创作台配置 + 平台同步)- 所有非预设角色的
description+ 所有角色的userDescription合计上限 10000 字符
// 为预设角色添加用户补充人设
await storvia.characters.update("秦照野", { userDescription: "对我温柔一些" });
// 非预设角色可改 description
await storvia.characters.update("路人甲", { description: "神秘的陌生人" });
// 改头像
await storvia.characters.update("秦照野", { avatar: "img/new-avatar.png" });
// 改性别
await storvia.characters.update("秦照野", { gender: "male" });characters.remove(name)
删除角色。
await storvia.characters.remove("新角色");预设角色不可删除——调用会返回错误(HTTP 400,PRESET_CHARACTER_LOCKED)。
删除非预设角色时会级联清理:该角色在 game_state 中的属性状态、以及所有与其相关的
relationships 会一并移除。
characters.merge(source, target)
把 source 合并进已有角色 target,用于同一个人被写成两个名字的去重。
AI 有时会在剧情推进中换个称呼(先叫「老维」,后面写成「维克多」),系统会当成两个角色分别建档、数值各记一套。这时用 merge 合回去:
await storvia.characters.merge("维克多", "老维");
// 「维克多」消失,它的数据全部并进「老维」服务端会做四件事:
source的属性并入target(同名字段以source的值为准)source的名字记进target的别名——以后 AI 再用旧名字,系统能认回同一个角色- 所有指向
source的关系边重指到target(去掉自环、去重) - 删除
source
- 预设角色不可作为
source被合并掉(作者资产),会返回PRESET_CHARACTER_LOCKED target必须已存在(活跃或休眠都行),否则 404- 不能合并到自身(400)
- 合并不可撤销,建议先让玩家确认
characters.detail(name)
获取 角色完整信息(人设 + 属性状态)。属性值为 RichFieldValue 格式。
const detail = await storvia.characters.detail("秦照野");
// {
// name: "秦照野",
// avatar: "https://...",
// description: "", // 预设角色固定为空;自建角色为用户撰写
// introduction: "对外介绍文本", // 仅预设角色有意义,只读
// visibleAtStart: true, // 仅预设角色有意义,只读
// userDescription: "用户补充",
// gender: "male",
// isPreset: true,
// state: {
// "档案": {
// "心情": { value: "happy", type: "text" },
// "好感度": { value: 45, type: "number", min: 0, max: 100 }
// }
// }
// }characters.state.get(name)
获取角色的属性状态。不传 group 返回全部分组,传 group 返回该组数据(与 player.state.get() 对称)。
// 获取全部分组
const all = await storvia.characters.state.get("秦照野");
// {
// "档案": {
// "心情": { value: "happy", type: "text" },
// "好感度": { value: 45, type: "number", min: 0, max: 100 }
// },
// "身份": {
// "粉丝牌": { value: "真爱粉", type: "text" }
// }
// }
// 获取单个分组
const profile = await storvia.characters.state.get("秦照野", "档案");characters.state.update(name, group, values)
更新角色指定分组下的字段。传入字段与该分组已有数据合并。只需传简单值,系统自动保留字段元数据(type/min/max)并钳位。
await storvia.characters.state.update("秦照野", "档案", {
心情: "happy",
好感度: 50, // 自动钳位到 [0, 100]
});分组名和字段名必须和创作台中配置的完全一致(包括中英文、空格等)。
characters.state.remove(name, group, keys)(SDK 0.9.8 起)
删除该角色指定分组下的字段。角色、分组、字段任一不存在都不报错;传空数组直接返回、不发请求。同名分组在别的角色身上不受影响。
await storvia.characters.state.remove("秦照野", "状态", ["中毒"]);characters.state.removeGroup(name, group)(SDK 0.9.8 起)
删除该角色的整个属性分组。分组不存在时是 no-op。
await storvia.characters.state.removeGroup("秦照野", "临时增益");与玩家属性同理:删的是当前存档里的数据,不是创作台的字段定义——创作台配过的分组,AI 再次输出或玩家「同步最新设定」时会重新出现。
characters.setPriority(name, value)
调整 角色在场景中的重要程度。范围 0-100(超出自动钳位到 0 或 100),越高表示越不容易被挤出场景。
当会话中的角色 数量超过上限时,系统会把 priority 最低的角色移入休眠池。
预设角色也可以改——服务端只对 remove() / 角色合并上锁(返回 PRESET_CHARACTER_LOCKED),priority 不锁。
调整 priority 只有 setPriority 这一个入口——characters.state.update() 无法修改它。
开局的 priority 是系统按名次分配的,不是统一值:预设角色从 90 起按名次递减、最低 60(先排「立即出场」的,再排休眠的,保证开局台上每个角色都高于台下任何一个)。玩家在聊天页手动新建的角色默认 90。
另外 AI 自己能调到的上限是 90,只有 SDK / 玩家手动才能设到 91–100。这是刻意留的一档:设成 100 的角色,AI 无论怎么调都挤不掉。
// 让某个角色 变得重要
await storvia.characters.setPriority("路人甲", 80);
// 让 角色退居二线(下次触发上限裁剪时会被优先移除)
await storvia.characters.setPriority("过客", 10);storvia.relationships — 关系
关系是两个角色之间的定向连接,由 from + to 唯一标识。每条关系可以有自定义字段(对应创作台配置的关系字段)。
玩家本人在 from / to 里有一个固定标识,与你通过 storvia.player.update({ name: "..." })
设置的实际玩家名无关(玩家改名不会让关系断掉)。所有 relationships.list/get/add/update/remove 都遵循这个约定:
- 传参:写
"玩家"、"player"、"user"(不区分大小写)都认作玩家,推荐用 SDK 导出的常量PLAYER_ID - 返回值:玩家一律是
"玩家",判断时写r.from === PLAYER_ID,不要写死字面量 - 例外:作品里真有角色叫
Player/user时,这些词指的是那个角色
import { PLAYER_ID } from "@storvia/sdk";
// UMD 引入时:window.StorviaSDK.PLAYER_ID
await storvia.relationships.update(PLAYER_ID, "秦照野", { 好感度: 50 });"player" / "user" 别名线上对任意 SDK 版本都生效;创作台预览 / 本地调试里的别名和 PLAYER_ID 常量需要 SDK 0.10.8+。
relationships.list()
获取所有关系。
const rels = await storvia.relationships.list();
// [
// { from: "玩家", to: "秦照野", "好感度": 45, "关系阶段": "朋友" },
// { from: "玩家", to: "宋亦", "好感度": 30, "关系阶段": "认识" },
// ]relationships.get(from, to)
获取两个角色之间的关系。
const rel = await storvia.relationships.get("玩家", "秦照野");
// { from: "玩家", to: "秦照野", "好感度": 45, "关系阶段": "朋友" }relationships.add(relationship)
添加新关系。
await storvia.relationships.add({
from: "玩家",
to: "新角色",
好感度: 0,
关系阶段: "陌生",
});relationships.update(from, to, data)
更新关系字段。合并更新。
await storvia.relationships.update("玩家", "秦照野", {
好感度: 50,
关系阶段: "朋友",
});relationships.remove(from, to)
删除关系。
await storvia.relationships.remove("玩家", "新角色");关系是单向的:from:"玩家" to:"秦照野" 和 from:"秦照野" to:"玩家"
是两条不同的关系。如果需要双向关系,需要分别创建和更新。
storvia.inventory — 物品栏
数据结构:Record<分组名, Record<物品名, { count, desc?, iconUrl? }>>。
创作台里配置的分组在运行时始终保留(即使清空所有物品也不会消失),和 player.state / characters.state 的行为一致。
物品总数上限 200 件,达到上限后新增会被拒绝(SDK 和 AI 都一样),需先 remove / removeBatch 释放空间。
inventory.list()
获取整个物品栏,或指定分组下的物品。创作台里为预设物品配置的图标会随 iconUrl 字段返回完整 URL;没配置图标的物品缺省 iconUrl。
// 全部
const all = await storvia.inventory.list();
// {
// "礼物": { "玫瑰花": { count: 3, desc: "送给主播的礼物", iconUrl: "https://.../rose.png" } },
// "装备": {}, // 空分组也会保留
// "仓库": { "宝箱": { count: 1, iconUrl: "https://.../chest.png" } }
// }
// 单个分组
const gifts = await storvia.inventory.list("礼物");
// { "玫瑰花": { count: 3, desc: "...", iconUrl: "https://.../rose.png" } }创作台里为预设物品配置的图标会在首次同步时写入存档,list() 会直接返回带
iconUrl 的数据。如果没有配置图标,字段为 undefined。
inventory.add(options)
参数:{ group, name, count?, desc?, iconUrl? }。
新增物品。group 必须是已存在的分组(创作时定义,或先用 ensureGroup 创建);同分组下 name 唯一。iconUrl 支持 http/https 图片地址或 Storvia 的 storage:// 路径。
await storvia.inventory.add({
group: "礼物",
name: "玫瑰花",
count: 5,
desc: "送给主播的礼物",
iconUrl: "https://example.com/rose.png", // 可选
});inventory.update(options)
参数:{ group, name, count?, desc?, iconUrl? }。
更新物品字段。group 和 name 是必填(用于定位物品),其余字段(count / desc / iconUrl)按需传入,合并更新,未传的保持不变。传空字符串不会清除 iconUrl,现阶段要"去掉图标"需要先 remove 再 add。
// 同时更新多个字段
await storvia.inventory.update({
group: "礼物",
name: "玫瑰花",
count: 10,
desc: "新描述",
iconUrl: "https://example.com/rose-v2.png",
});inventory.remove(options)
参数:{ group, name }。
删除单个物品。分组本身保留,即使变空也不会消失。
await storvia.inventory.remove({ group: "礼物", name: "玫瑰花" });inventory.removeBatch(items)(SDK 0.9.3 起)
参数:物品定位数组 [{ group, name }, ...]。
批量删除多个物品,一次请求完成。不存在的物品静默跳过(幂等);分组本身保留。传空数组直接返回,不发请求。
await storvia.inventory.removeBatch([
{ group: "礼物", name: "玫瑰花" },
{ group: "装备", name: "生锈的铁剑" },
]);inventory.move(options)
参数:{ fromGroup, toGroup, name, newName? }。
在分组间移动物品,可选择同时改名。事务完成,不会因中途失败导致物品丢失。
// 把随身的药水移到仓库
await storvia.inventory.move({
fromGroup: "随身",
toGroup: "仓库",
name: "生命药水",
});inventory.ensureGroup(group)
幂等地确保分组存在。用于运行时动态创建创作时未定义的分组。
await storvia.inventory.ensureGroup("临时物品栏");inventory.removeGroup(group, options?)
第二参数:{ cascade?: boolean }。
显式删除整个分组。默认非空分组会报错;传 cascade: true 连带物品一起删。
await storvia.inventory.removeGroup("临时物品栏"); // 仅空分组
await storvia.inventory.removeGroup("临时物品栏", { cascade: true }); // 强制删除从 SDK v0.2.0 起,物品栏的数据结构从"扁平数组 + group 字段"改为"分组 →
物品名
Record"。创作时定义的分组现在会永久保留,不再因为物品清空而消失。如果你从旧版本升级,需要更新调用方式(所有方法都改为对象参数)。
storvia.custom — 扩展属性
扩展属性存储游戏中的额外变量。字段类型只支持 文本 / 数值,可选分组(分组的子字段也只能是文本/数值)。支持两种用法:
- 纯 SDK 数据 — 不注入 AI,仅游戏代码读写(如关卡进度、计数器)
- AI 追踪数据 — inject 状态开启时字段值会注入 AI 提示词,AI 每轮自动更新
AI 追踪可以双端控制:
- 创作台:字段级 / 分组级开关,作为新增字段的初始 inject 状态
- SDK:运行时调用
setInject(key, value)或set(data, { inject: true })动态切换 - 分组的 AI 追踪是整组开关:开启后组内所有子字段都被 AI 看到
- 平台同步作者最新设定时只会为新增 key 写入作者最新的 inject 默认值;已有 key 的 inject 状态由 SDK 自由控制,同步不再覆盖
- 需要存任意 JSON、永不被 AI 看到?请用
storvia.save
限制
| 项目 | 限制 |
|---|---|
| 字段总数(平铺 + 所有分组内字段) | 100 个——全部字段共享此上限(含不注入 AI 的字段),超出后新增会被拒绝 |
| 注入 AI 的字段数(rules + state 值) | 100 个——按作者填写顺序累计,超出的不进 prompt、不进 state XML |
| 字段类型 | 仅 文本 / 数值 |
| 单字符串值长度 | 无限制 |
注入 AI 的字段数达到 100 后,AI 动态新增字段(op="add")会被拒绝;非注入字段不计入注入上限(但仍计入上方 100 个的字段总数上限)。控制"哪些字段被注入"用 inject 开关 + 创作台的「场景设定」组合(详见下方「AI 追踪」一节):把字段绑到特定场景,AI 只在该场景看到。
custom.get(key?)
获取扩展属性。不传参数返回全部,传 key 返回指定字段。
// 获取全部
const all = await storvia.custom.get();
// { chapter: 3, bgm: "rain", endings: { he: false, be: true } }
// 获取单个字段
const chapter = await storvia.custom.get("chapter");
// 3custom.set(data, options?)
深合并更新。平铺字段直接覆盖,分组字段合并子字段(不会丢失未传入的子字段)。options.inject 可一次性把这次写入的所有 key 标记为对应 inject 状态。
// 更新平铺字段:推进到第 4 章
await storvia.custom.set({ chapter: 4 });
// 更新分组内的字段(只改 he,be 保持不变)
await storvia.custom.set({ endings: { he: true } });
// 写入并同时开启 AI 追踪
await storvia.custom.set({ chapter: 4 }, { inject: true });custom.setInject(key, inject)
单独切换某个顶层 key 的 AI 追踪状态(不修改值)。分组的 setInject 等价于整组开关。
await storvia.custom.setInject("chapter", true); // 开启注入
await storvia.custom.setInject("endings", false); // 整组关闭注入custom.getInject()
读取顶层 key 的 inject 状态(同步方法)。
注意:正式模式下没有端点回读 inject 状态,getInject() 只返回本会话内通过 setInject(key, value) 或 set(data, { inject }) 显式设置过的 key;创作台预设的 inject 状态在正式模式下无法回读(仅预览模式会从快照灌入,能读到创作台的预设值)。
const injectMap = storvia.custom.getInject();
// { chapter: true, endings: false } —— 仅含本会话显式设置过的 keycustom.delete(key)
删除一个平铺字段或整个分组。
// 删除平铺字段
await storvia.custom.delete("bgm");
// 删除整个分组
await storvia.custom.delete("endings");custom.setGroupField(groupKey, fieldKey, value)
更新分组内的单个字段。如果分组不存在会自动创建。
await storvia.custom.setGroupField("endings", "se", true);custom.deleteGroupField(groupKey, fieldKey)
删除分组内的单个字段。分组本身会保留(即使字段全空),不会自动消失。如需删除整个分组请调用 custom.delete(groupKey)。
await storvia.custom.deleteGroupField("endings", "be");AI 追踪
inject 状态决定字段是否注入 AI 提示词。开启后:
- 字段的当前值会注入到 AI 提示词中,AI 能感知这个数据
- AI 每轮回复时会自动更新该字段(和角色属性、物品栏一样)
两种控制方式(任选其一或组合使用):
- 创作台静态配置——在「自定义属性」区域,每个平铺字段或分组有一个「AI」开关。分组开关是整组开关,开启后组内所有子字段都注入。
- SDK 运行时切换——
storvia.custom.setInject(key, value)或set(data, { inject: true })动态切换。
配合「场景设定」:创作台为字段或分组配置场景设定后,字段只在指定场景下注入(如 viewer_count 只在直播场景注入)。SDK 只能切 inject 开关,场景设定始终来自创作台配置。
设定同步时的行为:作者更新设定后平台会把最新配置同步到玩家存档,这个同步是增量的——只会为新增 key 写入作者最新的 inject 默认值。已有 key(不论是预设世界书还是 AI/SDK 运行时新增)的 inject 状态保留,同步不会覆盖。所以 SDK 调 setInject(key, value) 之后即使发生同步也不会被回退。
适用场景:
- AI 追踪:剧情进度、任务阶段、密室锁定状态 等需要 AI 自动维护的变量
- 纯 SDK:UI 主题、音量、调试开关 等纯前端逻辑数据
storvia.state — 完整游戏状态
一次性返回当前会话所有模块(world / player / characters / inventory / relationships)的合并快照。适合渲染整体属性面板、做存档导出、或调试时观察当前游戏状态。
state.get()
const snapshot = await storvia.state.get();返回值
{
current: PublicCurrentState, // 当前活跃模块的合并快照
previous: PublicCurrentState | null, // 上一轮的 current(用于 before/after 对比)
dormant: { // 因角色数量超出上限被移入休眠区的数据
characters: Record<string, CharacterState>,
relationships: Array<Record<string, unknown>>,
}
}会话尚未初始化时返回 null。
完整 JSON 示例
{
"current": {
"world": {
"time": "晚上 22:00",
"date": "2024年3月15日",
"location": "秦照野的直播间",
"region": "江南水乡",
"weather": "小雨"
},
"player": {
"attributes": {
"基础": {
"等级": { "value": 5, "type": "number", "min": 1, "max": 100 },
"经验": { "value": 180, "type": "number", "min": 0, "max": 9999 }
},
"经济": {
"花园币": { "value": 450, "type": "number", "min": 0, "max": 99999 }
}
}
},
"characters": {
"秦照野": {
"isPreset": true,
"priority": 100,
"avatar": "https://cdn.storvia.com/avatars/qzy.png",
"gender": "male",
"attributes": {
"档案": {
"好感度": { "value": 50, "type": "number", "min": 0, "max": 100 },
"心情": { "value": "开心", "type": "text" }
}
}
},
"路人甲": {
"isPreset": false,
"priority": 60,
"gender": "other",
"attributes": {
"档案": {
"好感度": { "value": 0, "type": "number", "min": 0, "max": 100 }
}
}
}
},
"inventory": {
"礼物": {
"玫瑰花": { "count": 3, "desc": "送给主播的礼物", "icon_url": "https://cdn.storvia.com/items/rose.png" }
},
"装备": {}
},
"relationships": [
{ "from": "玩家", "to": "秦照野", "好感度": 50, "关系阶段": "朋友" }
]
},
"previous": {
"world": { "time": "晚上 21:00", "...": "..." },
"player": { "...": "..." },
"characters": { "...": "..." },
"inventory": { "...": "..." },
"relationships": []
},
"dormant": {
"characters": {
"过客": {
"isPreset": false,
"priority": 10,
"attributes": { "档案": { "好感度": { "value": 5, "type": "number" } } }
}
},
"relationships": []
}
}字段说明
current.world— 扁平 KV,对应storvia.world.get()current.player.attributes— 按分组的玩家属性,对应storvia.player.state.get()current.characters[name].attributes— 按分组的角色属性,对应storvia.characters.state.get(name);isPreset/priority/avatar/gender为顶层 meta 字段current.inventory— 分组 → 物品名 →{ count, desc?, icon_url? }current.relationships— 关系数组,每条形如{ from, to, ...自定义字段 }previous— 与current同结构,但是 AI 上一轮回复前的快照;首次对话时为nulldormant.characters— 休眠的角色(priority较低、被挤出活跃区的);不会注入 AI,AI 也不会更新这些角色
和 chat 响应里 full 的关系:storvia.chat.send() / generate() / retry() 在 AI 本轮有属性变更时,会在响应里携带同样结构的 full 字段。两者数据结构完全一致,区别在于:
chat.*的full是对话响应顺带返回——AI 改了什么自动给你storvia.state.get()是主动拉取——任意时刻取当前最新值
该方法在预览模式下无法正常使用,需要发布后在 Storvia 平台测试。 预览会话不持久化到数据库;调用会抛出 StorviaError(code: NOT_SUPPORTED_IN_PREVIEW)。预览阶段需要查看状态时,用各分模块的 get() 单独读取(如 storvia.world.get() / storvia.player.state.get())。
storvia.save — 游戏存档
save 是作者自由读写的任意 JSON,专供游戏运行状态持久化。和 custom 的关键区别:
| 特性 | custom | save |
|---|---|---|
| 数据类型 | 仅文本/数值(可分组) | 任意合法 JSON(嵌套对象、数组、布尔均可) |
| AI 注入 | 可选(创作台开启 AI 追踪时) | 永不注入 |
| 配额 | 共享 100 字段上限 | 独立配额,不计入 custom |
| 大小上限 | 单字符串无上限 | 整个存档序列化后 ≤ 256KB |
| 写入语义 | 深合并(分组合并子字段) | 整体覆写 |
save 已经按会话(存档)自动隔离,用固定 key 即可,不要自己加 sessionId / 时间戳后缀。 每一局游戏都是独立的会话容器(见 存档隔离),save 只属于当前这一局:新开一局时 save.get() 返回 {},旧存档的数据永远不会漏进来。所以「重开游戏会读到旧存档」是不会发生的——你无需用 sessionId 给 key 分桶来"隔离不同存档"。
反例:用 sessionId 拆分存档 key 反而会让同一局游戏前后读写对不上,造成真正的数据丢失。正确做法就是固定结构 save.set({ ... }) / save.get()。
save.get()
读取整个存档对象。第一次调用(未写入过)返回 {}。
const save = await storvia.save.get();
// {} 或上次写入的对象save.set(data)
整体覆写存档。增量更新需要自行 get → 修改 → set。
// 典型存档场景
await storvia.save.set({
chapter: 3,
cursor: { x: 120, y: 340 },
flags: { metAlice: true, doorUnlocked: false },
inventory: [
{ id: "key_red", count: 1 },
{ id: "potion", count: 5 },
],
history: ["scene_intro", "scene_market"],
meta: { savedAt: Date.now(), version: 2 },
});
// 增量更新:先读再改
const cur = await storvia.save.get();
cur.flags.doorUnlocked = true;
await storvia.save.set(cur);存档大小上限为 256KB(JSON.stringify(data).length),超出会返回
SAVE_DATA_TOO_LARGE。如果你的游戏需要存储更多状态(如完整地图数据),考虑只存关键索引,详细内容用
localStorage 或后端服务存储。
禁止把图片本体(base64 / dataURL / 二进制等)直接写入 save!
一张图片就能轻松占用几百 KB 到几 MB,远远超过 256KB 上限,会直接导致 save.set() 失败、玩家整局存档丢失。
正确做法:把图片上传到图床或对象存储,save 里只存图片的 URL / 路径。比如:
// ❌ 错误:把整张图塞进去
await storvia.save.set({ avatar: "data:image/png;base64,iVBORw0KG..." });
// ✅ 正确:只存路径
await storvia.save.set({ avatar: "https://cdn.example.com/avatars/u123.png" });同理适用于音频、视频、PDF 等任意二进制资源——save 的所有字段只用来存轻量结构化数据(数字、布尔、短文本、引用 URL),永远不要存原始资源本身。
storvia.memory — 永久记忆与世界书
读取/编辑当前会话的永久记忆和世界书——也就是聊天界面「AI 记忆」对话框里「永久记忆」「世界书」两档。它们都不会过期、不会被自动总结覆盖。
- 永久记忆:一段文本,每一轮对话都会完整注入;
update(content)用新内容整体替换旧内容,写入时自动trim - 世界书:分三块——「基础」一段文本每轮完整注入;作者写好、玩家按条开启的「预设」(0.10.6 起,0.10.7 起分两种模式);「高级」是玩家自己的条目,每条可开关、分常驻(每轮注入)和关键词(关键词出现在本轮玩家输入或上一轮 AI 回复里才注入)两种模式,还能勾选「小手机生效」让它在小组件里也注入(0.10.7 起)。
worldbook.update是部分更新,只改你传了的那一块 - 玩家共同维护:玩家也能在「AI 记忆」对话框里直接编辑同一份内容,你写入的内容玩家可见、可改
平台的四档记忆,SDK 能读写后两档
聊天界面的「AI 记忆」分四档,后两档对 SDK 开放:
| 档位 | 谁维护 | 生命周期 | 注入位置 | SDK 可读写 |
|---|---|---|---|---|
| 动态记忆 | 平台自动记录游戏里发生过的关键事件,按当前剧情自动挑选相关的几条注入 | 每条自创建起保留 30 天 | 历史记忆 | ❌ |
| 长期记忆 | 平台自动总结的剧情主线,随剧情推进自动更新并完整注入 | 不过期,但会被下次自动总结覆盖 | 历史记忆 | ❌ |
| 永久记忆 | 你(SDK)或玩家手动写入 | 不过期、不被覆盖,始终完整注入 | 历史记忆——AI 当「已经发生的事实」 | ✅ storvia.memory.get / update |
| 世界书 | 你(SDK)或玩家手动写入 | 不过期、不被覆盖;基础与开启的常驻条目始终注入,开启的关键词条目命中才注入 | 自定义规则——AI 当「必须遵循的规则 / 设定」 | ✅ storvia.memory.worldbook.get / update |
怎么选:「发生过什么」写永久记忆,「必须怎么做」写世界书。玩家做过的不可逆选择、揭晓过的身份是事实,进永久记忆;「主角绝不会背叛王国」「叙事必须用第二人称」这类约束是规则,进世界书。
动态记忆 / 长期记忆由平台自动生成与维护(按会话开关),storvia.memory 读不到也写不到它们。所以在你(或玩家)第一次 update 之前,memory.get() 会一直返回 null——即使这局游戏已经积累了大量自动记忆。
如果你的游戏有必须让 AI 永远记住的设定或关键剧情(世界观铁律、玩家做过的不可逆选择等),在剧情关键节点调用 storvia.memory.update(...) 写进永久记忆。
永久记忆每轮都会完整注入提示词,只放最关键的内容。堆太多会明显增加 token 消耗,也会干扰 AI、拉低输出质量——流水账式的剧情回顾交给平台的动态 / 长期记忆即可。
返回类型
interface AISummary {
content: string; // 永久记忆内容
updated_at: string; // ISO 时间戳,最近一次写入时间
}memory.get() → Promise<AISummary | null>
读取当前会话的永久记忆。从未写入过时返回 null。
const m = await storvia.memory.get();
// null 或:
// { content: "主角是龙族最后的后裔,这个身份必须对王国保密。", updated_at: "2026-05-06T10:00:00.000Z" }memory.update(content) → Promise<AISummary>
整体覆写永久记忆内容,返回写入后的最新结构。
const updated = await storvia.memory.update("主角是龙族最后的后裔,这个身份必须对王国保密。");
// { content: "主角是龙族最后的后裔,这个身份必须对王国保密。", updated_at: "..." }update 是覆写而非追加,且会同时覆盖玩家自己写的内容。如需在原内容基础上追加,请先 get 取出旧 content,自行拼接后再 update。
世界书的三块:基础、预设、高级
世界书对应聊天界面「AI 记忆 → 世界书」,按界面顺序分三块:
| 块 | 字段 | 注入方式 | 适合放什么 |
|---|---|---|---|
| 基础 | content(一段文本)+ contentWidget(小手机生效) | 每轮完整注入,没有开关;勾了 contentWidget: true(界面「小手机生效」)才进小组件 | 全局规则、禁忌、写作要求;SDK 想程序化写一段世界书就写这里 |
| 预设 | presets(只读清单)+ enabledPresets(写入用) | 玩家开启的条目按各自模式注入(常驻的与基础同一位置、关键词的命中才注入),默认全部关闭 | 作者在创作台「预设世界书」里写的可选剧情路线、隐藏设定、可选规则、术语表 |
| 高级 | entries(玩家条目数组,每条可开关、分两种模式) | 常驻(constant: true):开启后每轮注入,与基础同一位置;关键词(缺省):开启后任一关键词出现在本轮玩家输入或上一轮 AI 回复里才注入。勾了 widget: true(界面「小手机生效」)的条目在小组件生成里也注入 | 常驻放可随时关掉的成块设定;关键词放名词术语解释、只在提到时才需要的设定 |
关键词按子串匹配、不区分大小写,AI 看不到关键词本身,条目内容请写成完整的句子。同一轮里命中的关键词条目按「开启的关键词预设在前、会话条目在后」注入,内容相同的会去重。关掉的条目(enabled: false)两种模式都不注入、关键词也不扫。
预设由作品在创建存档时快照进会话,SDK 只能读到每条的 id / title / description / constant / enabled,正文与关键词永远不下发,玩家看不到;开启的常驻预设与基础在同一位置注入(预设在前、玩家在后),遵循度一致。
小组件(App)也能写预设世界书,但它的预设不走这个接口:玩家在小组件的悬浮球 → 记忆 → 世界书里按条开关,开启的注入小组件自己的生成。memory.worldbook 读写的始终是主线那份(含主线作者的预设),供作者做记忆编辑。主线世界书里只有勾了「小手机生效」的基础 / 高级条目会进小组件,主线预设不会。玩家在小组件的记忆面板里看到的也是同一份基础 / 高级,在哪边改都一样;详见小组件能做什么。
interface ExtendWorldRuleEntry {
id: string; // 条目 id,新建时可自行生成(如 crypto.randomUUID())
keys: string[]; // 关键词,任一命中即触发;常驻模式忽略
content: string; // 注入的设定
title?: string; // 条目名,只给玩家看(≤50 字)
constant?: boolean; // true = 常驻;缺省 false = 关键词触发(0.10.7 起)
enabled?: boolean; // 缺省 true;false 时不注入也不扫关键词(0.10.7 起)
widget?: boolean; // 小手机生效:true 时在小组件生成里也注入;缺省 false(0.10.7 起)
}
interface PresetWorldbookView {
id: string; // 作者写的预设 id,开关时用它
title: string; // 标题(≤50 字)
description: string; // 简介,告诉玩家这条是干什么的、什么时候开
enabled: boolean; // 当前是否开启
constant: boolean; // true = 常驻,false = 关键词触发(0.10.7 起)
}
interface Worldbook {
content: string; // 基础
contentWidget: boolean; // 小手机生效:true 时基础在小组件生成里也注入(0.10.7 起)
entries: ExtendWorldRuleEntry[]; // 高级条目
presets: PresetWorldbookView[]; // 预设世界书清单(只读,不含正文)
updated_at: string | null; // ISO 时间戳,从未写入过为 null
}memory.worldbook.get() → Promise<Worldbook | null>
读取当前会话的世界书。从未写入过且作品没有预设世界书时返回 null。
const wb = await storvia.memory.worldbook.get();
// null 或:
// {
// content: "主角绝不会背叛王国;所有对话使用第二人称。",
// contentWidget: false,
// entries: [
// { id: "…", keys: ["霜牙", "佩剑"], content: "主角的佩剑名为「霜牙」,祖传之物,遇血泛蓝光。", title: "佩剑", constant: false, enabled: true },
// { id: "…", keys: [], content: "王国正处于持续三年的旱灾之中。", title: "旱灾", constant: true, enabled: false, widget: true },
// ],
// presets: [{ id: "ap-1", title: "雨夜模式", description: "整段剧情改在雨夜发生", enabled: false, constant: true }],
// updated_at: "2026-09-18T10:00:00.000Z"
// }memory.worldbook.update(patch) → Promise<Worldbook>
部分更新:只改你传了的那一块,没传的保留原值。返回写入后的完整结构。
- 传字符串:只覆写基础
content(与旧版调用方式兼容) - 传对象
{ content?, contentWidget?, entries?, enabledPresets? }:分别覆写基础 / 基础的小手机生效 / 高级条目 / 开启的预设 id 列表;entries传空数组即清空全部条目,enabledPresets传空数组即全部关闭
写入时自动清洗:关键词 trim、去空、去重,内容 trim;内容为空的条目、关键词模式但没有关键词的条目会被丢弃;constant / enabled / widget 不传时按缺省值(关键词模式、开、不进小组件)写入。
// 只改基础
await storvia.memory.worldbook.update("主角绝不会背叛王国;所有对话使用第二人称。");
// 只改高级条目(基础不受影响)
await storvia.memory.worldbook.update({
entries: [
{ id: crypto.randomUUID(), keys: ["霜牙", "佩剑"], content: "主角的佩剑名为「霜牙」,祖传之物,遇血泛蓝光。" },
{ id: crypto.randomUUID(), title: "旱灾", constant: true, keys: [], content: "王国正处于持续三年的旱灾之中。", widget: true },
{ id: crypto.randomUUID(), keys: ["芝麻"], content: "店里的黑猫叫芝麻,只让店主摸。", enabled: false },
],
});
// 开启某条预设世界书(id 从 get() 的 presets 里拿;不在清单里的 id 会被忽略)
await storvia.memory.worldbook.update({ enabledPresets: ["ap-1"] });
// 几块一起改
await storvia.memory.worldbook.update({ content: "…", entries: [], enabledPresets: [] });每一块内部仍是覆写:传 entries 会整体替换现有条目列表,传 enabledPresets 会整体替换开启列表。要追加一条,先 get 取出旧值,拼上后再 update——并把读到的每条原样带回(含 title / constant / enabled / widget)。只回传 { id, keys, content } 会让玩家的常驻条目(keys 为空)被当成「关键词模式没关键词」而丢掉,也会把玩家关掉的条目重新打开、勾了小手机生效的条目失效。
storvia.getAssets() — 素材库
平台级方法(非会话模块),用于获取官方素材库中的头像和物品图标。
getAssets(type, category?)
按类型获取素材列表,可选按分类过滤。
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
type | 'avatar' | 'icon' | 是 | 素材类型:头像或物品图标 |
category | string | 否 | 分类名称,不传则返回全部(分类待更新) |
返回值:Promise<AssetItem[]>
interface AssetItem {
id: number;
name: string;
url: string;
assetType: "avatar" | "icon";
category: string;
}示例:
// 获取所有头像素材
const avatars = await storvia.getAssets("avatar");
// [
// { id: 1, name: "骑士", url: "https://...", assetType: "avatar", category: "奇幻" },
// { id: 2, name: "法师", url: "https://...", assetType: "avatar", category: "奇幻" },
// ...
// ]
// 按分类获取物品图标
const weapons = await storvia.getAssets("icon", "武器");
// 用获取到的素材设置角色头像
const avatar = avatars[0];
await storvia.characters.update("骑士", { avatar: avatar.url });素材库数据由平台官方维护。
数据应该放哪里?
| 需求 | 推荐模块 |
|---|---|
| 需要 AI 感知的角色/世界/物品状态 | world / player.state / characters.state / relationships / inventory |
| 需要 AI 感知的简单变量(剧情进度、好感度等) | custom(创作台开启 AI 追踪) |
| 不需要 AI 感知的简单 KV(计数器、开关) | custom(创作台关闭 AI 追踪) |
| 游戏运行状态、UI 缓存、复杂结构 | save(不占 custom 配额,永不注入 AI) |