当前位置: 首页 > news >正文

【西游劫:第三篇】 API 路由设计详解

API 路由设计详解

一、设计总览

在构建一个沉浸式的文字冒险游戏(《西游记》题材)时,后端 API 需要同时满足多种需求:

  • 支持存档管理,让玩家可以拥有多个游戏进度;
  • 提供游戏初始化状态快照,保证每次进入游戏都能准确恢复现场;
  • 处理核心交互,既要能快速返回完整结果(JSON),也要能流式输出(SSE)以提升 LLM 生成的体验;
  • 记录消息历史便于复盘,并提供LLM 连通性测试帮助调试外部模型配置。

基于以上考量,设计了一套 RESTful 风格的路由,围绕/api/game资源展开。下面将按章节逐一拆解每个接口的作用、内部逻辑以及设计背后的思考。

路由总览(Mermaid 思维导图)

游戏API路由

/saves

GET 列出所有存档

DELETE 删除指定存档

/init

POST 创建新游戏

/action

POST 主交互

Accept: application/json -> 非流式全量返回

Accept: text/event-stream -> SSE流式响应

/state

GET 获取当前完整状态

/message

GET 分页拉取消息历史

/test-endpoint

POST 测试外部LLM连通性

二、存档管理:/api/game/saves

作用:列出所有游戏存档的基本信息,以及删除不再需要的存档。

GET 请求 —— 获取存档列表

  • 返回内容:每个存档的摘要,包括:

    • idupdatedAt等基础字段
    • playerStateworldStateJSON 中解析出的关键数值(如等级、修为、当前位置、当前章节)
    • 聚合统计信息:该存档下的消息数量、物品数量、任务数量(通过 Prisma 的_count实现)
  • 设计意图:玩家可能在多个存档间切换,列表视图需要快速呈现“一眼看懂”的进度信息,而不需要加载完整的游戏状态。通过预解析 JSON 字段和关联计数,可以避免额外的数据库查询。

DELETE 请求 —— 删除存档

  • 参数:查询参数?id=<存档ID>
  • 实现:利用数据库的级联删除(Prisma 的cascade配置),一次删除GameSave及其关联的PlayerStateWorldStateInventoryNPC 关系消息任务进度等所有子表数据,保证无残留。

三、新游戏启动:POST /api/game/init

当玩家点击“新游戏”时,后端需要快速生成一份完整的初始世界。

请求参数

{playerName:string// 非空,最长20字符statOverrides?:{// 可选,允许覆盖部分初始属性strength?:number// 1-20agility?:numberwisdom?:numberluck?:number}}

核心流程(Mermaid 流程图)

接收 playerName 与可选的 statOverrides

调用 initGame 创建核心记录

GameSave, PlayerState, WorldState, Inventory, NPC关系

并发查询 QuestLog 和 WorldEvent 表

组装初始任务列表与世界事件

返回完整初始化数据

结束

典型初始值(以“五行山脚下”剧本为例)

  • 玩家状态:生命 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-streamSSE 流式
  • 其他(或不传) →JSON 非流式

包含 text/event-stream

不包含或为其他

客户端发起 POST /action

检查 Accept 头

调用 processActionStream

返回 SSE 响应流

客户端逐块接收增量文本

调用 processAction

等待完整 GameResponse

加载最新玩家/世界状态

返回完整 JSON 快照

非流式处理步骤
  1. 调用processAction(gameSaveId, action, llmConfig)—— 这是封装了 LLM 调用、状态更新、剧情生成的纯函数。
  2. 等待返回完整的GameResponse(包含narrativestateChanges等)。
  3. 额外调用loadGameState(gameSaveId)重新加载数据库中的最新状态,确保返回给前端的数据与数据库完全一致。
  4. 返回一个包含以下字段的 JSON 对象:
    • narrative:生成的剧情文本
    • stateChanges:本次动作导致的状态变更摘要
    • playerStateworldStateinventorynpcRelations等完整快照
流式处理步骤
  1. 调用processActionStream(...)获得一个AsyncIterable(异步可迭代对象),它会随着 LLM 生成逐块产出文本片段。
  2. 将 AsyncIterable 包装为标准的Server-Sent Events (SSE)响应:
    • 设置Content-Type: text/event-stream
    • 保持连接打开,不断发送事件
  3. 定义的事件类型:
    • 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

专用于翻阅玩家与游戏之间的所有对话记录。

分页参数

参数默认值最大值说明
limit50200每页条目数
offset0跳过的条目数(非负整数)

实现细节

  1. 参数校验limit必须为正整数,offset为非负整数,否则返回 HTTP 400。
  2. 排序:数据库查询按createdAt DESC获取最新记录,然后在前端 / 中间层调用reverse()转换成时间正序(从旧到新),便于按页阅读。
  3. 元数据处理:每条消息的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 结构,前端可以轻松构建出沉浸、流畅、具有丰富剧情分支的文字冒险游戏。

http://www.zskr.cn/news/1453134.html

相关文章:

  • 从Pwn到实战:用IDA Pro和Ghidra手把手分析CTF二进制逆向题(附解题脚本)
  • 深入vsomeip:从Unix Domain Socket看高性能IPC如何实现(附Wireshark抓包分析)
  • 网盘下载困境的破解方案:LinkSwift直链下载助手深度解析
  • 医用超声图像后处理中的帧率算法:原理、优化与实践
  • 网盘直链下载助手:一键获取真实下载地址的终极解决方案
  • 深入内核:拆解WCH CH32V303的SDI Printf机制,对比它与SEGGER RTT和传统串口的异同
  • 别再手动找驱动了!手把手教你用Lenovo XClarity Provisioning Manager搞定ThinkSystem服务器Windows Server 2019安装
  • 量子加速DDPG在电力系统频率调节中的应用与优化
  • 如何用3步将QQ空间回忆永久保存到本地?GetQzonehistory开源工具全解析
  • 期末周救命神器 Paperxie!3 步搞定课程论文,再也不用熬夜肝初稿了
  • EverCrypt:形式化验证加密库,为开发者提供可证明的安全保证
  • 泗洪县26年最新专业手表包包回收权威店铺推荐,TOP排行榜 - 莘州文化
  • 钢材产生腐蚀的原因及防护方法有哪些?
  • 告别SpeechRecognition!用阿里FunASR搞定会议录音转文字(附离线模型部署避坑指南)
  • UE5 SpatialLabs插件实战:如何解决摄像机外物体不显示这个“反常识”的立体成像问题?
  • 全网最细java零基础学习就业课程教学之java基础篇3
  • Python函数:局部变量与全局变量的作用域
  • 别再堆技术了!高并发高可用下单系统,真正的架构精髓在这里
  • 耐火浇注料供应商怎么选?2026年行业深度解析与优质厂家推荐 - 深度智识库
  • YOLOv8安装踩坑记:手动创建setup.py和requirements.txt的保姆级教程
  • 5个突破性技巧彻底改变你的OneNote笔记管理效率
  • 当AI学会了“理解“医院:医疗企业本体语义模型落地记
  • 揭秘Chromatic:5分钟掌握Chromium/V8应用的终极修改神器
  • STM32F103C8T6直接驱动SG90舵机的PWM控制工程(标准库版,含接线图与示例)
  • 一张图搞懂 HarmonyOS SnapshotUtil:什么场景用哪个截图方法?
  • 保姆级教程:用CrewAI+Ollama在本地电脑搭建你的第一个多Agent协作项目(附避坑指南)
  • 3分钟掌握B站视频转文字:你的个人知识管理助手
  • 盐城核心商圈黄金回收套路多,正规渠道这样选才安心 - 黄金上门回收
  • 一种颠覆传统RAG的检索范式,把 RAG 从“向量搜索”变成“推理式检索”
  • Esxi 7.0装好后必做的5件事:从激活许可证到上传ISO镜像的完整配置流程