Rabi搞定让Agent CLI使用规范驱动开发的事情之后,就想开发一套东西来解决自己在云上开饥荒联机版服务器的几大痛点。归纳起来大概有这么几项:
- 自动更新服务端并重启服务器
饥荒联机版的服务端更新频率还挺高的。如果服务端过期,玩家就无法搜索和加入服务器,需要经过相对繁琐的人工停服-升级-再开服流程才能恢复。Rabi也不可能一天24小时盯着服务器,如果群友想玩的时候服务器搜不到,那肯定就玩别的去了,又或者本来约好了要玩的群友,结果人到齐了还要等更新,等手忙脚乱的一通搞好,玩游戏的心情也没了大半。 - 自动处理MOD订阅
服务器上多多少少要开一些MOD来提高游玩体验,但是对于服主来说就是个麻烦的活。不像本地服那样在界面上勾选几下就可以用,独立服务器要把需订阅的MOD的创意工坊ID写进dedicated_server_mods_setup.lua里,导致想增减个MOD很费时间。而服务端数据里的MOD配置文件modoverrides.lua里也包含MOD的创意工坊ID,因此理论上可以在启动服务器的时候根据其内容自动生成MOD订阅文件,就不需要手工修改了。 - 方便执行一些常用命令
按Rabi的习惯,如果开了新档,第一件事就是确认一下主世界的海象小屋是1个还是4个。毕竟步行手杖大家都想要,1个海象小屋加上25%的爆率根本供不起多人游玩。要统计海象小屋数量,需要在控制台执行c_countprefabs("walrus_camp"),但不管是从游戏里还是游戏外,这事儿都不好干。游戏内控制台的命令回显是不可见的,还要加上c_announce()才能在聊天区域看到返回的数值,但是搓控制台期间联机游戏可是不会暂停的,被路过的野狗咬死了也没地方说理去。
就算是先不进游戏,在控制台敲命令吧,由于饥荒服务端并不是一个很规范的控制台程序,命令打到一半会被新出现的日志直接冲走就算了,甚至退格键都不能正常工作,敲错一个字符就只能先回车报错然后从头写起,手感也是烂得不行。再说谁家好人干点什么事都要用控制台啊,要是能在聊天区域发点命令就解决了不是更好么。 - 给新人游戏指导
饥荒作为一个开放生存游戏,是真的一点教程没有。硬要说也就单机版饥荒在开始游戏的时候,老麦会在你旁边蛐蛐道“朋友,你看起来不太好。你最好在天黑之前找点东西吃。”要是有新人刚进来玩,作为老资历肯定要悉心指导——但停下来打字竟是最大死亡因素!Rabi经常在QQ群里看到各式各样的聊天机器人,如果服务器里常驻一位具有相应知识的聊天助手,不就大大的解决了问题吗?
开发历程
没错,项目名用的就是老麦的这一句Say pal。Rabi最开始的主意是做三个组件,API Server负责将服务端那个不太好用的终端转换成稳定的HTTP API端点,作为后续所有功能的基础。MCP Adapter则把服务端的控制能力进一步转换成MCP协议,供Agent调用。不过在做的过程中,Rabi意识到相较于一个需要24小时烧Token来管理饥荒服务端的Agent,用传统的固定算法来实现自动更新这样的需求也并不差。
经过一番考虑,Rabi将原计划中的独立Agent改成了Agent Host。具体来说这实际上是一个挂有若干不同插件的消息总线,插件的架构是统一的,无论是专门从Websocket API拉取实时日志并作为事件发送到总线的消息源插件,还是订阅聊天事件并作出响应的聊天命令插件,都同属一种插件类。而原计划中的聊天和游戏指导功能,也作为一个单独的插件来实现。
API Server
饥荒服务端的本体是一个dontstarve_dedicated_server_nullrenderer_x64二进制程序,大概是代码里硬编了一些../data之类的路径,必须从所在目录启动它才能正常工作。古时候Rabi试过把服务端放在tmux里面,然后用tmux send-keys往里面塞指令。这样倒是确实能行,但要是有人动过控制台窗口,留了几个字符,那不是很爆炸?
于是Rabi寻找其他的途径,最后选定了通过pty来控制服务端。Codex做出了一个不错的命令注入实现,不仅仅是注入命令本身(如果服务端同时在吐出别的日志的话,可能会干扰判断),而是带着专用的标识符(例如__API_BEGIN__)以及唯一事务ID注入,使得注入命令的返回值能够被精确区分出来。
迭代到当前版本,API Server具备了独立管理饥荒服务端生命周期的能力,例如检测到更新时可以直接调用POST /admin/update-server接口,服务端会关闭、更新并重启,而API Server自身不需要重启,从而保持API的可用性。
API Server还内置了任务队列的功能,即使接口调用频率很高,也能通过返回202 Accepted并将任务放入队列来避免出现竞态。
MCP Adapter
虽然最新的架构实际上不再需要MCP Adapter这一层,而是由Agent Host内建并暴露给插件的能力来和API Server交互,不过万一什么时候真的要把这套能力接入自己的Agent呢?所以还是把MCP组件保留了下来。想法也很简单,就把API的功能1:1地做出来,然后看情况再做成点Skill,就可以接到 OpenClaw之类的上面玩了。暂定如此,还没有继续完善。
Agent Host
设计之初这是一个持续接收日志信息→触发动作的流水线,后来实现的功能多了,意识到得做成消息驱动的架构比较好。于是便有了一条消息总线+若干插件,一些插件生产消息、一些插件消费消息这样的设计。一开始迁移的时候还有流水线的逻辑残留,比如消费了消息需要在一定事件内返回动作,但是到了要增加chat_llm插件的时候立刻发现了问题,因为LLM的返回时间通常都比较长,于是立刻开始追加修改,一方面让消费消息本身只做简单的确认,后续可能触发长时间处理就异步了;另一方面由于插件要做的动作很多是共通的,在每个插件里实现一遍很没必要,于是又加上了由Agent Host统一暴露一组常用能力的设计,也算是和API Server的接口对应上了。
先是做自动升级插件,从https://api.steamcmd.net/v1/info/343050拉取在线版本的buildid,和本地的appmanifest对比,如果不一致就触发更新流程,预设了从只在服务器里发消息提醒到自动倒计时重启服务器更新后再上线的几种策略。搞来搞去竟然是这个公共API不靠谱,查询到的最新版本是23119522,真的更新完了本地的版本却是23206748,仍然和在线版本不一致,导致更新陷入死循环,只好把判断逻辑改成本地版本<在线版本才正常。
chat_llm是实现了最初设想的聊天+教程功能的插件,为了让回答能够有好的参考价值,Rabi很早就考虑要让它基于检索增强生成(RAG)作答。到了实际实现的时候,才发现并不是什么很复杂的事情,简单的解析Markdown根据段落切块,调个embedding模型来嵌入,量也不大就直接用sqlite存,然后再简单设计一下系统提示词,让它有得参考就作答,没得参考就老实承认,这么几板斧下来,效果却是有模有样的。
考虑到把所有聊天都回复一遍也不合适,这里是用了激活词+激活时间的方法,按默认配置,玩家聊天只要提到“Pal”或者“帕鲁”就会触发LLM的回复,并且在之后的2分钟内继续发送的聊天内容也会被视为对话的延续。每个玩家的激活状态都是独立的,如此一来聊天助手就能相对像人一点了。之后准备在这方面再继续优化,比如让LLM从上下文综合判断是对话的延续,还是转而跟别人说话了,然后找个机制让LLM可以决定“不作回复”。如果API那边再加几个能主动感知游戏内状态的接口,就连让Pal主动给新手提供指导都可以实现。
部署方式
项目公开在GitHub了,主要讲一下常见的地上+地下双服务端的部署方法。
首先用uv同步依赖:
uv sync
API Server配置
复制api_config.json.example,推荐命名为api_master.json和api_caves.json。
{
"install_dir": "~/.local/share/Steam/steamapps/common/Don't Starve Together Dedicated Server",
"steamcmd_dir": "~/.local/share/Steam/steamcmd",
"klei_dir": "~/.klei/DoNotStarveTogether",
"cluster": "Cluster_1",
"shard": "Master",
"host": "127.0.0.1",
"port": 8765,
"auto_start_server": true,
"fix_server_workshop": true,
"setup_mods": true
}
如果安装饥荒联机版服务器的时候没有修改安装路径(直接steamcmd +login anonymous +app_update 343050 +quit)的话,前面三处路径都不需要修改。
cluster和shard根据实际路径修改名称就可以。API和Agent一般在同一个服务器上,所以host保持127.0.0.1。port则要把Master和Caves设成不一样的,例如8765和8766。
auto_start_server控制是否同时启动服务端;fix_server_workshop修复Linux服务端可能无法正常下载某些比较老的服务端Mod的问题;setup_mods会自动根据modoverrides.lua来生成dedicated_server_mods_setup.lua,以便自动订阅开启了的MOD。这三项都保持true即可。
使用下列命令启动API和饥荒服务端,启动完成之后就可以正常进入服务器游玩。如果想要自动更新等更高级的功能,就需要启动Agent Host。
uv run dst-say-pal-api serve --config api_master.json
uv run dst-say-pal-api serve --config api_caves.json
Agent Host配置
复制agent_config.json.example,推荐命名为agent_master.json和agent_caves.json。将api_base_url修改为和api对应的地址,按照前面的示例就是地表http://127.0.0.1:8765,地下http://127.0.0.1:8766。
server_update_check默认启用,每5分钟检查在线版本,如果本地版本过期,默认策略when_stopped会在服务器内发送提醒,并在服务端关闭时自动更新。如果想无人值守更新服务器,可以将update_policy设为autorestart,这样在检测到本地服务端过期时会往服务器内发送倒数消息,并在倒计时结束后关闭服务器、执行更新再自动启动。
{
{
"type": "server_update_check",
"enabled": true,
"settings": {
"online_version_url": "https://api.steamcmd.net/v1/info/343050",
"check_interval_seconds": 300,
"update_policy": "autorestart",
"announce_interval_seconds": 300,
"lock_retry": {
"fallback_base_seconds": 60,
"jitter_seconds": 15
}
}
},
chat_llm默认不启用。如果想启用,需要将chat_llm的enabled改为true,并填写chat模型和embedding模型的URL、模型名称、API Key。
{
"type": "chat_llm",
"enabled": true,
"settings": {
"activation_words": ["帕鲁", "Pal"],
...
"system_prompt": "你的名字是帕鲁(Pal),一个饥荒联机版服务器聊天助手,请用简洁中文回答玩家问题。回答问题时优先依据知识库内容。",
"chat": {
"base_url": "https://api.example.test/v1",
"model": "chat-model",
"api_key": "replace-with-chat-api-key",
"timeout_seconds": 20.0,
"temperature": 0.2
},
"embeddings": {
"base_url": "https://api.example.test/v1",
"model": "embedding-model",
"api_key": "replace-with-embeddings-api-key",
"timeout_seconds": 20.0
},
"rag": {
"enabled": true,
"fallback_text": "当前 RAG 不可用或知识库为空,请明确告知玩家无法基于知识库回答,并仅提供通用建议。",
"knowledge_dir": "./knowledge",
...
}
}
将Markdown格式的游戏攻略放在knowledge文件夹里,然后把rag的enabled设为true就能够让聊天助手基于本地知识库作答。这一部分反而是最难搞定的,Rabi打算之后找一些饥荒百科的站点沟通看看能不能做一版公共的出来,方便开箱使用。还有system_prompt可以用来自定义聊天助手的个性,在这里自由发挥一下说不定就能让喜欢的角色教你玩饥荒了呢。
一些闲话
DST-Say-Pal这个项目还称不上完善,但是Rabi觉得效果已经不错,所以现在就把它发布出来。之后会考虑开一个公共服务器,方便大家直接品鉴。那么永恒领域见,冒险家们。



文章评论