聊天
SDK 对话模块的完整 API —— send、save、generate、retry、messages、edit、delete、rollback 八种调用方式
SDK 提供八种对话方法,覆盖不同场景:
| 方法 | 用途 | 保存消息 | 调 AI | 扣费 |
|---|---|---|---|---|
storvia.chat.send() | 用户发消息 → AI 回复 | 自动保存用户+AI消息 | 是 | 是 |
storvia.chat.save() | 只保存一条消息 | 保存指定消息 | 否 | 否 |
storvia.chat.generate() | 只请求 AI 生成 | 可选保存AI回复 | 是 | 是 |
storvia.chat.retry() | 重试最后一条 AI 消息 | 覆盖原 AI 消息 | 是 | 是 |
storvia.chat.messages() | 获取历史消息列表 | 不保存 | 否 | 否 |
storvia.chat.edit() | 编辑指定消息文本 | 覆写原消息内容 | 否 | 否 |
storvia.chat.delete() | 删除指定消息 | 删除该条消息 | 否 | 否 |
storvia.chat.rollback() | 回溯到指定消息 | 删除其后所有消息 + 恢复 state | 否 | 否 |
storvia.chat.send()
一体化对话:发送用户消息 → AI 回复 → 自动保存双方消息。适合 1v1 对话场景。
参数
storvia.chat.send({
message: string, // 必填:用户消息内容
topic?: string, // 可选:话题标识,隔离对话历史
sceneKey?: string, // 可选:场景 key,引用创作台预设的场景设定
extract?: { start, end }, // 可选:内容过滤规则,详见「内容过滤」章节
stream?: boolean, // 可选:是否流式输出(默认 false)
onChunk?: (text) => void, // stream=true 时:收到文本片段的回调
onDone?: (id) => void, // stream=true 时:生成完成的回调,id 为消息UUID
onState?: (delta, full) => void, // stream=true 时:AI 本轮有属性变更时触发;非流式通过返回值的 delta/full 字段获取
onThinking?: (status) => void, // stream=true 时:模型 reasoning 状态变更('start' = 开始思考,'end' = 开始输出正文),仅支持思考型模型
})非流式用法
const reply = await storvia.chat.send({
message: '你好',
topic: 'dm_npc1',
sceneKey: 'dm_scene',
});
console.log(reply.id); // 消息 UUID
console.log(reply.content); // AI 回复内容非流式响应格式
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"content": "干嘛,这么晚找我?",
"delta": { ... },
"full": { ... }
}delta 和 full 仅在 AI 本轮产生了属性变更时才出现;无变更的回合这两个字段缺省。
delta — 本轮的增量
只包含 AI 这一轮实际改动过的字段,便于快速读取发生了什么变化:
{
"world": {
"time": "晚上 22:00",
"location": "秦照野的直播间"
},
"player": {
"经验": 180
},
"characters": [
{
"id": "秦照野",
"op": "update",
"attrs": { "好感度": 50, "心情": "开心" }
},
{
"id": "路人甲",
"op": "add",
"attrs": { "好感度": 0 }
}
],
"items": [
{ "name": "玫瑰花", "op": "add", "count": 3 },
{ "name": "旧钥匙", "op": "remove" }
],
"relations": [
{ "from": "玩家", "to": "秦照野", "op": "update", "好感度": 50, "关系阶段": "朋友" }
],
"locations": [
{ "name": "秘密基地", "op": "add", "region": "城东", "description": "隐蔽的据点" }
]
}未发生变更的字段(world、player、对应数组)在 delta 里为 null 或空数组,不会出现。
full — 合并后的完整状态快照
full 是当前会话所有模块合并后的完整快照,结构与 storvia.state.get() 的返回值一致,包含 current / previous / dormant 三个字段。详见状态模块文档。
通常推荐根据业务需要选择使用哪个字段:
- 只需要知道「变了什么」(如触发动画、播放音效)→ 读
delta,结构简洁 - 需要渲染完整 UI(如角色属性面板、物品栏)→ 读
full.current,直接映射即可 - 不需要每轮自动接收完整状态时 → 不读
full,需要时再调用storvia.state.get()主动拉取
流式用法
await storvia.chat.send({
message: '你好',
topic: 'dm_npc1',
sceneKey: 'dm_scene',
stream: true,
onChunk: (text) => {
// 每收到一个文本片段就会调用
// text 是增量内容,不是完整内容
chatBubble.textContent += text;
},
onDone: (id) => {
// 流式输出完成
console.log('消息ID:', id);
},
onState: (delta, full) => {
// AI 本轮有属性变更时触发(仅在确实改动了世界/玩家/角色等字段时)
// delta 是本轮的变更内容(增量),full 是合并后的完整快照
console.log('好感度变了:', delta.characters);
updateUI(full.current);
},
});onState 是可选回调;不需要监听变更时不传即可。回调在整轮生成完成后一次性推送(即 onChunk 全部触发完之后),所以可以在 onState 里直接用最终内容做联动。
流式 SSE 事件格式
流式模式下服务端通过 SSE 推送以下几类事件:
data: {"type":"start","ai_message_uuid":"550e8400-...","is_streaming":true}
data: {"type":"thinking","status":"start"}
data: {"type":"thinking","status":"end"}
data: {"type":"delta","content":"干"}
data: {"type":"delta","content":"嘛"}
data: {"type":"state_updating","updating":true}
data: {"type":"state_updating","updating":false}
data: {"type":"state","delta":{...},"full":{...}}
data: {"type":"done","ai_message_uuid":"550e8400-..."}| 事件类型 | 说明 |
|---|---|
start | 流开始,携带消息 UUID |
thinking | 思考型模型 reasoning 状态变更(status:'start' = 开始思考、status:'end' = 开始输出正文),通过 onThinking 回调传递;非思考模型不会出现此事件 |
delta | 文本片段,通过 onChunk 回调传递给开发者 |
state_updating | AI 正在写 state 标签(updating:true)/ 写完(updating:false) |
state | state 解析完成,携带本轮的增量(delta)和完整快照(full) |
done | 流结束,通过 onDone 回调传递消息 UUID |
流式模式下 send() 不返回 ChatResponse,AI 内容通过 onChunk 回调逐步传递。state 事件在整轮生成完成后一次性推送。
storvia.chat.save()
只保存消息,不调 AI。用于先保存用户消息,再分别请求不同 角色回复的场景(如群聊)。
参数
storvia.chat.save({
role: 'user' | 'assistant', // 必填:消息角色
content: string, // 必填:消息内容
topic?: string, // 可选:话题标识
})返回值
{ id: string } // 保存的消息 UUID用法
// 保存一条用户消息到群聊话题
await storvia.chat.save({
role: 'user',
content: '大家好!',
topic: 'group_chat',
});
// 也可以保存 AI 消息(比如本地生成的内容)
await storvia.chat.save({
role: 'assistant',
content: '欢迎回来~',
topic: 'group_chat',
});save() 不会调用 AI、不会扣费。它只是把消息写入对应的 topic 下,后续 send() 或 generate() 调用同一 topic 时,AI 能看到这条消息。
save() + send() 会导致用户消息被写入两次:save() 写一条,send() 内部还会再写一条。需要用户消息 + AI 回复的场景请直接用 send();需要先保存用户消息再分别请求多个角色回复的场景,使用 save() + generate()。
storvia.chat.generate()
只请求 AI 生成,不需要用户消息。AI 基于该 topic 的历史消息 + 场景上下文生成回复。
参数
storvia.chat.generate({
topic?: string, // 可选:话题标识
sceneKey?: string, // 可选:场景 key
extract?: { start, end }, // 可选:内容过滤规则,详见「内容过滤」章节
stream?: boolean, // 可选:是否流式(默认 false)
onChunk?: (text) => void,
onDone?: (id) => void,
onState?: (delta, full) => void,
onThinking?: (status) => void, // 同 send(),仅思考型模型触发
})返回值
与 send() 相同结构(id / content / state / full)。流式事件格式也完全一致。
用法
// 角色在群聊中回复(AI 能看到 group_chat 的历史,包括之前 save 的消息)
const reply = await storvia.chat.generate({
topic: 'group_chat',
sceneKey: 'group_chat_scene',
});
console.log(reply.content); // "哟,稀客"storvia.chat.retry()
重试最后一条 AI 消息:覆盖原 AI 消息重新生成。适用于回复出错、不满意或中断后恢复的场景。
参数
storvia.chat.retry({
topic?: string, // 可选:重试指定 topic 下的最后一条 AI 消息
sceneKey?: string, // 可选:场景 key
extract?: { start, end }, // 可选:内容过滤规则,详见「内容过滤」章节
stream?: boolean, // 可选:是否流式(默认 false)
onChunk?: (text) => void,
onDone?: (id) => void,
onState?: (delta, full) => void,
onThinking?: (status) => void, // 同 send(),仅思考型模型触发
})返回值
与 send() 相同结构(id / content / state / full)。流式事件格式也完全一致。
非流式用法
const reply = await storvia.chat.retry({
topic: 'dm_npc1',
sceneKey: 'dm_scene',
});
console.log(reply.content); // 新生成的回复流式用法
await storvia.chat.retry({
topic: 'dm_npc1',
sceneKey: 'dm_scene',
stream: true,
onChunk: (text) => {
chatBubble.textContent += text;
},
onDone: (id) => {
console.log('重新生成完成,消息ID:', id);
},
});retry() 会覆盖该 topic 下最后一条 AI 消息(保留原 UUID,替换内容)。用户消息不受影响,AI 会基于原用户消息重新生成。
该方法在预览模式下无法正常使用,需要发布后在 Storvia 平台测试。 预览对话不会写入历史消息,没有"最后一条 AI 消息"可供 retry 操作;调用会抛出 StorviaError(code: NOT_SUPPORTED_IN_PREVIEW)。
storvia.chat.messages()
获取历史消息列表:支持按 topic 过滤、游标分页,两种遍历方向(从新到旧 / 从旧到新)。适合渲染聊天历史消息、做消息回放。
参数
storvia.chat.messages({
order: 'asc' | 'desc', // 必填:asc=从旧到新(历史展示),desc=从新到旧(聊天框)
limit?: number, // 可选:单次最大返回条数,1-50,默认 20
topic?: string, // 可选:按 topic 过滤,不传则返回全部
cursor?: { // 可选:翻页游标,首次不传
uuid: string,
createdAt: string,
},
})返回值
{
data: Array<{
uuid: string,
role: 'user' | 'assistant',
content: string,
topic: string | null,
status: string,
model: string | null,
createdAt: string,
}>,
hasMore: boolean, // 是否还有更多消息
nextCursor: { uuid, createdAt } | null, // 下一页游标;直接传给下次调用的 cursor 字段
}返回的 data 数组始终按时间正序(旧 → 新),不受 order 影响。order 只决定首次加载的起点和翻页方向。
场景 A:游戏内聊天框(从新到旧,向上滚动加载更旧)
// 首次加载:最新 20 条
let result = await storvia.chat.messages({
order: 'desc',
topic: 'dm_npc1',
});
renderMessages(result.data);
// 用户向上滚动,加载更旧的消息
if (result.hasMore) {
result = await storvia.chat.messages({
order: 'desc',
topic: 'dm_npc1',
cursor: result.nextCursor,
});
prependMessages(result.data);
}场景 B:历史消息展示页(从旧到新,向下滚动加载更新)
// 首次加载:最旧 20 条
let result = await storvia.chat.messages({
order: 'asc',
topic: 'dm_npc1',
});
renderMessages(result.data);
// 用户向下滚动,加载更新的消息
if (result.hasMore) {
result = await storvia.chat.messages({
order: 'asc',
topic: 'dm_npc1',
cursor: result.nextCursor,
});
appendMessages(result.data);
}预览模式下 messages() 始终返回 { data: [], hasMore: false, nextCursor: null },因为预览对话不会写入历史消息。
storvia.chat.edit()
编辑指定消息的文本内容:覆写已存在消息(user 或 assistant 都可)的 content 字段。不重新调用 AI、不重算 state、不扣费。适用场景:
- 用户输入了错别字想改正
- 修订 AI 回复中的不当措辞,再继续后续对话
- 在游戏里实现「悔棋」类编辑型 UI
调用签名
storvia.chat.edit({
uuid: string;
content: string;
}): Promise<{
uuid: string;
role: string;
content: string;
updatedAt: string;
}>;用法
// 1. 通过 messages() 拿到目标消息的 uuid
const list = await storvia.chat.messages({ order: 'desc', limit: 10 });
const target = list.data[0];
// 2. 编辑文本
const updated = await storvia.chat.edit({
uuid: target.uuid,
content: '修改后的内容',
});
console.log(updated.content); // "修改后的内容"
console.log(updated.updatedAt); // 新的 ISO 时间戳行为说明
- 范围:只更新
content字段;role/topic/status/ 关联的状态变更(state)均保持原样 - 不重算 state:编辑 AI 消息不会重新解析
<state>标签,状态系统仍以原回复时记录的为准
错误码
| 错误码 | 含义 |
|---|---|
INVALID_REQUEST | uuid 格式无效或 content 为空 |
CONTENT_VIOLATION | user 消息违禁词命中 |
MESSAGE_NOT_FOUND | 消息不存在或无权编辑 |
该方法在预览模式下无法正常使用,需要发布后在 Storvia 平台测试。 预览对话不会写入历史消息,没有 uuid 可供编辑;调用会抛出 StorviaError(code: NOT_SUPPORTED_IN_PREVIEW)。
storvia.chat.delete()
删除指定消息:移除已存在的消息(user 或 assistant 都可)。不重算 state、不回溯后续消息、不调 AI、不扣费。适用场景:
- 在游戏里实现「撤回」按钮
- 清理用户误发的消息
- 配合编辑流程做「弃稿重写」
调用签名
storvia.chat.delete({
uuid: string;
}): Promise<{
uuid: string;
}>;用法
// 1. 通过 messages() 拿到目标消息的 uuid
const list = await storvia.chat.messages({ order: 'desc', limit: 10 });
const target = list.data[0];
// 2. 删除该消息
const { uuid } = await storvia.chat.delete({ uuid: target.uuid });
console.log('已删除:', uuid);行为说明
- 不可恢复:消息会从历史记录中彻底移除,无法撤销
- 不级联:只删除该条消息本身,不会自动删除之后的其他消息(如需级联回溯请在自家业务层多次调用)
- 不重算 state:删除 AI 消息不会回滚关联的状态变更,状态系统保持原值
错误码
| 错误码 | 含义 |
|---|---|
INVALID_REQUEST | uuid 格式无效 |
MESSAGE_NOT_FOUND | 消息不存在或无权删除 |
该方法在预览模式下无法正常使用,需要发布后在 Storvia 平台测试。 预览对话不会写入历史消息,没有 uuid 可供删除;调用会抛出 StorviaError(code: NOT_SUPPORTED_IN_PREVIEW)。
storvia.chat.rollback()
回溯到指定消息:删除目标消息之后的所有消息(目标消息本身保留),并把会话的 game_state 从该消息保存时的快照恢复回来。不调 AI、不扣费。适用场景:
- 让玩家「悔棋」回到某一关键剧情节点重新发展
- 错误回复 / 状态紊乱后整体回滚
- 长会话清场(在某个里程碑消息处一键截断)
该方法仅限「雏菊卡」(sub_tier1)订阅用户调用。非订阅用户会收到 StorviaError(code: DAISY_CARD_REQUIRED,HTTP 403)。
SDK 会强制弹出引导弹窗(与「花园币不足」同款样式),点「去开通」会通过 bus 通知宿主跳转钱包页 —— 与 INSUFFICIENT_CREDITS 体验完全一致。该弹窗是平台统一的钱包跳转 CTA,不受 setToastEnabled(false) 控制、无法被作者抑制。作者可以正常 catch (err) 后继续业务流程,但不要试图覆盖默认引导。
调用签名
storvia.chat.rollback({
uuid: string;
}): Promise<{
deletedCount: number;
deletedUuids: string[];
restoredGameState: { current: unknown; dormant: unknown } | null;
}>;用法
// 1. 通过 messages() 拿到目标消息的 uuid
const list = await storvia.chat.messages({ order: 'asc', limit: 100 });
const checkpoint = list.data[10]; // 想回到第 11 条消息
// 2. 回溯
try {
const r = await storvia.chat.rollback({ uuid: checkpoint.uuid });
console.log(`已删除 ${r.deletedCount} 条后续消息`);
if (r.restoredGameState) {
console.log('状态已从快照恢复:', r.restoredGameState);
}
} catch (err) {
if (err.code === 'DAISY_CARD_REQUIRED') {
showUpgradePrompt('回溯功能仅限雏菊卡会员使用');
} else if (err.code === 'MESSAGE_TOO_OLD') {
showToast('该消息已超过 30 天,无法回溯');
} else {
throw err;
}
}行为说明
- 目标消息保留:仅删除
created_at > 目标消息.created_at的所有消息 - 不可恢复:被删除的消息会从历史记录中彻底移除,无法撤销
- 30 天上限:仅可回溯
created_at在 30 天内的消息 - state 恢复:若目标消息存在
game_state_snapshot,会用其覆写会话当前的current/dormant,并把previous清空;若快照不存在则跳过(restoredGameState返回null)
错误码
| 错误码 | HTTP | 含义 |
|---|---|---|
INVALID_REQUEST | 400 | uuid 格式无效 |
MESSAGE_TOO_OLD | 400 | 目标消息超过 30 天 |
NO_MESSAGES_TO_DELETE | 400 | 目标消息之后没有消息可删 |
DAISY_CARD_REQUIRED | 403 | 当前用户没有有效的雏菊卡订阅 |
MESSAGE_NOT_FOUND | 404 | 消息不存在或无权回溯 |
该方法在预览模式下无法正常使用,需要发布后在 Storvia 平台测试。 预览对话不会写入历史消息,没有 uuid 可供回溯;调用会抛出 StorviaError(code: NOT_SUPPORTED_IN_PREVIEW)。
群聊场景完整示例
群聊是 save() + generate() 配合使用的典型场景:
// 1. 玩家发消息 → 保存到 group_chat topic
await storvia.chat.save({
role: 'user',
content: '大家晚上好!',
topic: 'group_chat',
});
// 2. 请求秦照野回复
// AI 能看到 group_chat 历史中的「大家晚上好!」
const reply1 = await storvia.chat.generate({
topic: 'group_chat',
sceneKey: 'group_chat_scene',
});
showMessage('秦照野', reply1.content);
// 3. 请求宋亦回复
// AI 能看到玩家的消息 + 秦照野的回复
const reply2 = await storvia.chat.generate({
topic: 'group_chat',
sceneKey: 'group_chat_scene',
});
showMessage('宋亦', reply2.content);每个 generate() 调用时,AI 都能看到该 topic 下最新的历史消息(包括前面刚保存的)。所以宋亦回复时,能看到玩家说了什么、秦照野回了什么,从而产生自然的群聊效果。
Topic
Topic 决定了每次 AI 调用时加载哪些历史消息:
- 传入 topic 字符串 — 只加载该 topic 下的历史消息作为上下文
- 不传或传
null— 加载该会话下的全部历史消息
持续对话
需要 AI 记住上下文的场景,使用固定的 topic 字符串。同一 topic 下的消息会持续积累,每次调用时 AI 都能看到完整历史。
// 和秦照野的私聊,始终传同一个 topic
await storvia.chat.send({ message: '你好', topic: 'dm_qin_zhaoye' })
await storvia.chat.send({ message: '昨天的事情…', topic: 'dm_qin_zhaoye' }) // AI 能看到上一条一次性对话
不需要参考任何历史消息的场景(如弹幕生成、系统通知、独立事件),不需要给每次调用都生成随机 topic。
反例:不要用 Date.now() 之类的方式生成随机 topic
// ❌ 错误用法:每次都创建新 topic
const reply = await storvia.chat.generate({
topic: `danmaku_${Date.now()}`,
sceneKey: 'danmaku_scene',
})这样每次调用都会在历史消息里写入一条永远不会被复用的消息,长期累积会产生大量垃圾消息,且无法清理。
推荐做法:一次性生成、且可能会被多次重复触发的场景(例如玩家点"换一条"重新生成弹幕、系统反复刷新事件文案),用固定 topic + chat.retry()。retry() 会覆盖该 topic 下最后一条 AI 消息,既不会产生新数据,也不会让历史无限累积。
// ✅ 推荐:固定 topic,首次用 generate,后续重复生成走 retry
const first = await storvia.chat.generate({
topic: 'danmaku',
sceneKey: 'danmaku_scene',
})
// 用户点击"换一条" —— 覆盖上一条,不产生新消息
const next = await storvia.chat.retry({
topic: 'danmaku',
sceneKey: 'danmaku_scene',
})常用 Topic 命名参考
| 场景 | topic 示例 |
|---|---|
| 角色私聊 | dm_qin_zhaoye |
| 群聊 | group_chat |
| 直播互动 | live_qin_zhaoye |
| PK 对战 | pk_qin_zhaoye |
| 角色主动发言 | proactive_npc1 |
| 一次性生成(可重复触发) | danmaku / system_notify(配合 retry()) |
场景上下文(Scene Key)
Scene Key 是在创作台中预设的场景设定,用于告诉 AI 当前的场景信息。SDK 调用时只需传入对应的 key。
在创作台中配置
在创作台的「场景上下文」区域,添加 key-value 对:
| Key | 值(场景设定) |
|---|---|
dm_scene | 你正在和玩家私聊,语气亲密自然 |
live_scene | 你正在直播,需要回应观众的互动和弹幕 |
group_chat_scene | 你在主播群聊天,保持角色性格,注意其他人的发言 |
单个场景设定没有字数硬上限,但所有场景设定会按最长那个的字数计入「世界观/剧情设定」的 20000 字总上限。
在 SDK 中使用
const reply = await storvia.chat.send({
message: '你好',
topic: 'dm_npc1',
sceneKey: 'dm_scene', // 引用创作台预设的场景设定
});定义输出形式
沉浸创作下,每一轮 AI 输出的形式和长度由当前场景设定决定 —— 可能是一句台词、一段叙述、一封信、一份资料。
| Key | 值(场景设定) |
|---|---|
直播间 | 你正在直播,只输出一句直播话术,回应弹幕或抛出话题 |
私聊 | 这是私聊场景,只输出一句当前角色的发言,不要叙述旁白 |
世界观 | 玩家在查阅资料,输出一段背景介绍,包含历史、势力、风俗 |
信件 | 输出一封信的正文,开头称呼、落款署名都要有 |
作者可以在场景设定里自由指定每轮输出的形式与长度,AI 会优先遵循场景中的要求。
指定输出格式
如果你需要 AI 按结构化格式返回内容(在 SDK 里解析后做后续处理),强烈推荐使用 XML 标签 + 正则提取,不要让 AI 输出 JSON。
| 对比项 | XML 标签 | JSON |
|---|---|---|
| 解析稳定性 | ✅ 高,少量字符错误也能容忍 | ❌ 低,少一个引号或逗号就整体解析失败 |
| 模型能力要求 | 低,几乎所有模型都能稳定输出 | 高,弱模型经常输出非法 JSON |
| 流式渲染 | ✅ 可以边生成边显示 | ❌ 必须等完整结果到达再解析 |
推荐写法(写在场景设定里):
请严格按以下格式输出,不要有任何额外文字:
<title>标题文本</title>
<summary>一段摘要</summary>
<tags>标签1,标签2,标签3</tags>SDK 端用正则提取即可:
const reply = await storvia.chat.send({ message, sceneKey: 'card_scene' });
const title = reply.content.match(/<title>([\s\S]*?)<\/title>/)?.[1];
const summary = reply.content.match(/<summary>([\s\S]*?)<\/summary>/)?.[1];
const tags = reply.content.match(/<tags>([\s\S]*?)<\/tags>/)?.[1]?.split(',');自定义标签名不要和系统保留标签冲突(<state> / <world> / <player> / <character> / <item> / <relation> / <custom>),否则会被状态解析器吞掉。
不推荐使用 JSON:让 AI 输出 { "title": "...", "summary": "..." } 这种结构,弱模型频繁出现漏引号、多逗号、转义错误等问题,导致 JSON.parse 失败;即使是强模型,遇到 token 截断、内容里含中文引号或换行时也容易解析出错。XML 标签的容错性远高于 JSON。
与扩展属性联动
在创作台的「扩展属性」中,开启 AI 追踪的字段可以关联一个或多个场景 key。关联后,该字段只在对应场景被激活时才注入 AI:
- 未关联任何场景(默认)→ 任何对话都注入
- 关联了特定场景 → 只有
sceneKey包含对应 key 时才注入
例如:字段 viewer_count 关联了 live_scene,那么只有传入 sceneKey: 'live_scene' 时 AI 才能看到这个字段,私聊场景不会注入,减少无关信息干扰。
Scene Key 和状态模块是互补的。状态模块中的数据(角色属性、好感度、心情等)会自动注入给 AI,sceneKey 用于补充当前场景的指令信息,并控制哪些扩展属性字段参与注入。不传 sceneKey 时,AI 仍然能看到所有状态数据和全局扩展属性。
内容过滤(extract)
chat.send / chat.generate / chat.retry 都支持传入 extract 选项:服务端只保留 AI 回复中 start..end 起止标记之间的内容,流式推送 + 入库都按过滤后版本进行。其余部分(思考、旁白、元数据)会被丢弃。
适用场景
- AI 用
<thinking>...</thinking>写思考、<reply>...</reply>写正式回复,作者只想给玩家看 reply 部分 - AI 输出结构化内容(卡片、信件、UI 数据),作者只想把指定区段写入历史
调用示例
const reply = await storvia.chat.send({
message: '你好',
topic: 'main',
extract: { start: '<reply>', end: '</reply>' },
});
// reply.content 包含首尾标记 + 中间内容
// 例如 AI 输出 "让我想想...<reply>干嘛,这么晚找我?</reply>"
// reply.content → "<reply>干嘛,这么晚找我?</reply>"
console.log(reply.content);在指令 / 场景设定中告诉 AI:
请按以下格式输出:先用
<thinking>...</thinking>写你的思考过程,然后用<reply>...</reply>包裹给玩家看的回复。
之后调用 send / generate / retry 时传 extract: { start: '<reply>', end: '</reply>' }。
行为说明
返回内容保留首尾标记,方便开发者用正则精确解析:
| 情况 | 推送 / 入库结果 |
|---|---|
AI 输出 <reply>X</reply> | 推送 / 入库 = <reply>X</reply> |
AI 输出多段 <reply>A</reply>B<reply>C</reply> | 推送 / 入库 = <reply>A</reply><reply>C</reply>(自动拼接,中间的 B 被丢弃) |
AI 输出 <reply>未闭合的内容 | 推送 / 入库 = <reply>未闭合的内容(流末尾兜底吐出) |
AI 完全没输出 <reply> | fallback:推送 / 入库原始干净文本(避免空消息),便于排查 prompt 问题 |
流式行为
- SSE 推送的内容也只是
start..end之间的部分;AI 在写"开头思考"时前端不会收到任何 chunk,等<reply>标签出现后才开始流式推送 - 跨 chunk 拆分的标签(如
<re+ply>)会正确处理,不会把不完整标签当作正文 emit - 没有
<reply>命中时,整段原始内容会在流末尾作为一次性 chunk 推送给前端
约束
| 项目 | 限制 |
|---|---|
start / end | 字面量字符串(非正则),支持中文 / emoji 等任意 UTF-8 |
| 长度 | 每个标记 1-64 字符 |
start 与 end | 不能相同 |
| 数量 | 单次调用只支持一条规则(一对 start/end) |
错误码
| 错误码 | 含义 |
|---|---|
INVALID_EXTRACT_RULE | extract 入参格式不合法(缺字段 / 类型错 / 长度超限 / start 与 end 相同) |
如何选择 extract 还是 sceneKey 限定输出格式:sceneKey 用来告诉 AI 「当前场景的输出形式」(一句话 / 一封信 / 一段叙述等),是 prompt 注入;extract 是后端兜底过滤,确保入库的是干净的、解析后的内容。两者通常配合使用:sceneKey 引导 AI 输出固定格式,extract 把范围内的内容捞出来。
extract 不会也不应当替代系统保留标签的解析。<state> 等系统标签在 extract 之前就已经被剥离,不会出现在最终入库内容里。详见 系统保留标签。
超时保护
SDK 内置三级超时机制,覆盖对话请求的完整生命周期。超时后会自动抛出 StorviaError 并弹出 Toast 提示,开发者无需额外配置。
| 阶段 | 超时时间 | 触发条件 | 错误码 |
|---|---|---|---|
| 请求超时 | 60 秒 | 发出请求后,服务器未在 60 秒内响应 | REQUEST_TIMEOUT |
| 模型加载超时 | 180 秒 | 服务器已响应,但 180 秒内未收到第一个 AI 文本片段 | MODEL_LOADING_TIMEOUT |
| 流式无活动超时 | 60 秒 | 流式输出过程中,60 秒内未收到任何数据 | STREAM_INACTIVITY |
超时保护对 send() / generate() / retry() 的流式和非流式调用均生效。save() / messages() / edit() 等普通请求仅受请求超时(60 秒)保护。
错误处理
try {
await storvia.chat.send({
message: '你好',
stream: true,
onChunk: (text) => { chatBubble.textContent += text; },
onDone: (id) => { console.log('完成:', id); },
});
} catch (err) {
switch (err.code) {
case 'REQUEST_TIMEOUT':
console.log('请求超时,请检查网络');
break;
case 'MODEL_LOADING_TIMEOUT':
console.log('模型响应超时,请稍后重试');
break;
case 'STREAM_INACTIVITY':
console.log('连接中断,请重试');
break;
}
}