【西游劫:第三篇】 API 路由设计详解
API 路由设计详解
一、设计总览
在构建一个沉浸式的文字冒险游戏(《西游记》题材)时,后端 API 需要同时满足多种需求:
- 支持存档管理,让玩家可以拥有多个游戏进度;
- 提供游戏初始化与状态快照,保证每次进入游戏都能准确恢复现场;
- 处理核心交互,既要能快速返回完整结果(JSON),也要能流式输出(SSE)以提升 LLM 生成的体验;
- 记录消息历史便于复盘,并提供LLM 连通性测试帮助调试外部模型配置。
基于以上考量,设计了一套 RESTful 风格的路由,围绕/api/game资源展开。下面将按章节逐一拆解每个接口的作用、内部逻辑以及设计背后的思考。
路由总览(Mermaid 思维导图)
二、存档管理:/api/game/saves
作用:列出所有游戏存档的基本信息,以及删除不再需要的存档。
GET 请求 —— 获取存档列表
返回内容:每个存档的摘要,包括:
id、updatedAt等基础字段- 从
playerState和worldStateJSON 中解析出的关键数值(如等级、修为、当前位置、当前章节) - 聚合统计信息:该存档下的消息数量、物品数量、任务数量(通过 Prisma 的
_count实现)
设计意图:玩家可能在多个存档间切换,列表视图需要快速呈现“一眼看懂”的进度信息,而不需要加载完整的游戏状态。通过预解析 JSON 字段和关联计数,可以避免额外的数据库查询。
DELETE 请求 —— 删除存档
- 参数:查询参数
?id=<存档ID> - 实现:利用数据库的级联删除(Prisma 的
cascade配置),一次删除GameSave及其关联的PlayerState、WorldState、Inventory、NPC 关系、消息、任务进度等所有子表数据,保证无残留。
三、新游戏启动:POST /api/game/init
当玩家点击“新游戏”时,后端需要快速生成一份完整的初始世界。
请求参数
{playerName:string// 非空,最长20字符statOverrides?:{// 可选,允许覆盖部分初始属性strength?:number// 1-20agility?:numberwisdom?:numberluck?:number}}核心流程(Mermaid 流程图)
典型初始值(以“五行山脚下”剧本为例)
- 玩家状态:生命 100 / 法力 20 / 位置 五行山脚下 / 铜钱 10
- 世界状态:贞观十三年春,辰时黎明,附近有明显 NPC(孙悟空)
- 初始道具:粗布衣 ×1(已装备)、馒头 ×3
- 默认属性:力量 8 / 敏捷 10 / 慧根 12 / 运气 6 —— 可通过
statOverrides自定义调整
返回示例
{"gameSaveId":"uuid","playerState":{...},"worldState":{...},"narrative":"你从一块青石上醒来...","quests":[...],"worldEvents":[...]}为什么单独返回
narrative?
初始化本身就是一次“叙事开头”,直接给出一段文字描述可以让前端立即显示,避免发起一次额外的/action请求。
四、主交互:POST /api/game/action
这是游戏最核心的接口 —— 玩家输入一个动作(如“与孙悟空对话”、“跳到悬崖下”),后端调用大语言模型(LLM)生成剧情响应。
请求参数
{gameSaveId:stringaction:string// 非空,最长500字符llmConfig?:{// 可选,用于临时覆盖全局LLM配置model?:stringtemperature?:number// 0-2,默认0.8maxTokens?:number// 256-4096,默认2048topP?:number// 0.1-1.0,默认0.9customSystemPrompt?:stringcustomEndpoint?:stringcustomApiKey?:stringcustomModelId?:string}}流式(SSE)与 非流式(JSON)的协商策略
前端通过设置Accept请求头来决定使用哪种响应模式:
Accept: text/event-stream→SSE 流式- 其他(或不传) →JSON 非流式
非流式处理步骤
- 调用
processAction(gameSaveId, action, llmConfig)—— 这是封装了 LLM 调用、状态更新、剧情生成的纯函数。 - 等待返回完整的
GameResponse(包含narrative、stateChanges等)。 - 额外调用
loadGameState(gameSaveId)重新加载数据库中的最新状态,确保返回给前端的数据与数据库完全一致。 - 返回一个包含以下字段的 JSON 对象:
narrative:生成的剧情文本stateChanges:本次动作导致的状态变更摘要playerState、worldState、inventory、npcRelations等完整快照
流式处理步骤
- 调用
processActionStream(...)获得一个AsyncIterable(异步可迭代对象),它会随着 LLM 生成逐块产出文本片段。 - 将 AsyncIterable 包装为标准的Server-Sent Events (SSE)响应:
- 设置
Content-Type: text/event-stream - 保持连接打开,不断发送事件
- 设置
- 定义的事件类型:
heartbeat:表示 LLM 已开始处理,用于避免前端超时误判。chunk:携带一部分增量文本(例如每次输出几个 token)。done:完整响应结束,携带最终的结构化结果(与 JSON 模式的返回体结构相同)。error:出现异常时发送错误信息,并关闭流。
为什么需要两种模式?
- JSON 模式简单可靠,适合网络状况较好或希望一次拿到所有数据的场景(如批量测试)。
- SSE 模式极大提升了用户体验 —— 玩家能在 LLM 生成过程中“实时阅读”逐渐浮出的文字,感觉更像真人在叙述。同时 SSE 天然支持心跳,防止代理服务器超时断开。
五、状态快照:GET /api/game/state
玩家切换面板(背包、属性、任务)或需要刷新界面时,客户端可请求此接口获取当前所有状态的“合影”。
查询参数
gameSaveId:必填,作为查询字符串参数(例如?gameSaveId=xxx)
返回内容
gameSave:存档摘要(id、更新时间等)playerState:生命、法力、位置、属性等(JSON 解析后的对象)worldState:世界时间、场景描述、当前主线章节等inventory:背包物品列表(含装备标记)npcRelations:与每个 NPC 的好感度、已知信息activeQuests:进行中的任务列表activeWorldEvents:当前触发的时间限定事件recentMessages:最近 5 条消息记录(截断长文本,并补充时间戳)
为什么只返回最近 5 条消息?
状态快照主要用于 UI 面板的刷新,不需要完整历史。如果需要查看全部对话,应该使用专门的/message分页接口。
六、消息历史:GET /api/game/message
专用于翻阅玩家与游戏之间的所有对话记录。
分页参数
| 参数 | 默认值 | 最大值 | 说明 |
|---|---|---|---|
limit | 50 | 200 | 每页条目数 |
offset | 0 | 无 | 跳过的条目数(非负整数) |
实现细节
- 参数校验:
limit必须为正整数,offset为非负整数,否则返回 HTTP 400。 - 排序:数据库查询按
createdAt DESC获取最新记录,然后在前端 / 中间层调用reverse()转换成时间正序(从旧到新),便于按页阅读。 - 元数据处理:每条消息的
metadata字段(存储为 JSON 字符串)会被解析成对象返回。
返回结构
{"messages":[{"id":"...","role":"user","content":"我要跳到悬崖下","createdAt":"2025-...","metadata":{"actionType":"dangerous"}},...],"pagination":{"total":487,"limit":50,"offset":0,"hasMore":true}}七、LLM 连通性测试:POST /api/game/test-endpoint
当管理员或玩家在配置自定义 LLM 端点时,需要一个安全的探测接口来验证端点是否可用、认证是否正确。
请求参数
{endpoint:string// 必填,会进行 URL 格式校验modelId:string// 必填,模型名称(如 "gpt-3.5-turbo")apiKey?:string// 选填,如果提供则作为 Bearer Token}测试逻辑
- 构造最小化请求体:
{"model":modelId,"messages":[{"role":"user","content":"你好"}],"max_tokens":10,"temperature":0.1} - 设置请求头:
Authorization: Bearer <apiKey>(如果提供了 apiKey) - 超时限制:15 秒,防止阻塞。
- 成功条件:收到符合 OpenAI 风格的响应格式(至少包含
choices数组)。 - 返回信息:请求耗时(毫秒)、响应状态、是否通过了格式校验。
设计考量:该接口不会保存任何配置,仅做探测。真正的 LLM 配置会由用户在自己的客户端或服务器全局配置中管理,不会通过此接口写入数据库。
八、统一的错误处理模式
为了给前端一致的错误反馈,所有 API 端点遵循相同的错误处理约定:
示例错误响应
{"success":false,"error":"gameSaveId 不能为空"}为什么统一返回
success: false?
前端可以通过该字段快速判断调用是否成功,而不用依赖 HTTP 状态码的解析(尤其某些代理环境可能篡改状态码)。同时保留状态码用于网关层监控。
九、总结
本章详细介绍了游戏 API 的每一个端点:从存档管理、新游戏初始化,到核心的动作交互(支持流式与 JSON 两种模式),再到状态快照、消息历史以及 LLM 连通性测试。整套设计遵循以下原则:
- 职责单一:每个接口只做一件事,便于维护。
- 性能友好:通过分页、滑动窗口、并发查询等手段减少数据库负担。
- 体验优先:SSE 流式响应让 LLM 输出更自然。
- 灵活可配:玩家或管理员可以临时覆盖 LLM 参数,便于调试和个性化。
通过这样的 API 结构,前端可以轻松构建出沉浸、流畅、具有丰富剧情分支的文字冒险游戏。
