Storvia

聊天

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": { ... }
}

deltafull 仅在 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": "隐蔽的据点" }
  ]
}

未发生变更的字段(worldplayer、对应数组)在 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_updatingAI 正在写 state 标签(updating:true)/ 写完(updating:false
statestate 解析完成,携带本轮的增量(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_REQUESTuuid 格式无效或 content 为空
CONTENT_VIOLATIONuser 消息违禁词命中
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_REQUESTuuid 格式无效
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_REQUEST400uuid 格式无效
MESSAGE_TOO_OLD400目标消息超过 30 天
NO_MESSAGES_TO_DELETE400目标消息之后没有消息可删
DAISY_CARD_REQUIRED403当前用户没有有效的雏菊卡订阅
MESSAGE_NOT_FOUND404消息不存在或无权回溯

该方法在预览模式下无法正常使用,需要发布后在 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 字符
startend不能相同
数量单次调用只支持一条规则(一对 start/end)

错误码

错误码含义
INVALID_EXTRACT_RULEextract 入参格式不合法(缺字段 / 类型错 / 长度超限 / 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;
  }
}

On this page