去年年底我把一台 PS5 挪到书房第二块屏幕旁边本来只是想让客厅和书房都能玩结果发现真正烦人的不是游戏运行而是围绕主机的那些零散操作看折扣要开一个网页查奖杯要翻另一个 App远程开机每次都要经过官方软件层层跳转截图想整理到 NAS 还得手动导来导去。AnyPS5 这个项目就是在这种“被碎片化工具烦够”的状态下开工的。简单来说AnyPS5 是一个跑在局域网里的自托管聚合面板把一台 PS5 的状态、游戏库、折扣信息、截图素材和奖杯数据收拢到一个统一入口浏览器打开就能用。它不是改机工具不涉及系统破解也不提供任何绕过机制纯粹是做一个面向玩家的信息聚合与服务面板。名字里的 Any 有两层意思任意一台 PS5 都能接入任意设备的浏览器都能访问。如果你平时喜欢折腾自托管服务或者手头正好有一台主机想玩得更顺手这篇文章里的架构思路、模块划分和排坑经验可以直接拿去参考。1. 为什么要做 AnyPS5碎片化信息与重复操作的痛点1.1 玩家日常的“多端切换”现场我先描述一下自己每天的真实状态你会发现这个需求不是凭空想象出来的。早上起来想看一眼常玩的游戏有没有打折需要打开购物网站或官方商店页面先登录、再搜索偶尔还要切换区服才能看到价格。午休时间想在手机上扫一眼奖杯进度又得掏出手机那个 App等它同步半天列表加载出来之后还要手动刷新。晚上打开主机准备玩结果发现系统有更新等了五分钟等进入游戏之后想录一段操作又发现截图管理功能藏得特别深导出的路径还不好找。这种状态持续了挺久我一直觉得缺少一个“只负责汇总”的中间层。游戏本体我已经在主机上玩了数据也都存在官方生态里我只是不希望每一次查点信息都要在浏览器标签页、手机 App、U盘和共享文件夹之间来回折腾。AnyPS5 最开始只有一张简单的状态卡片后来才慢慢长成现在的样子进门一台旧笔记本做服务端接上局域网打开浏览器就能看到主机当前状态、最近折扣、截图归档和奖杯概览。1.2 现成工具不是不好而是“接不住”肯定有人会问官方 App 不是已经能看状态和截图了吗第三方奖杯网站也很多为什么还要自己写一个我整理过一张很朴素的对比表做完之后就明白了问题不在功能而在“聚合能力”。方案数据维度时效性跨端体验可扩展性官方 App较全但入口分散依赖推送手机为主桌面弱受限第三方奖杯站奖杯/游戏库有延迟Web 较好数据只读无法本地化手动维护表格全靠人肉更新差一般极好但太累AnyPS5 自聚合按需组合定时任务即时轮询任意浏览器自己控制这里的关键不是谁替代谁而是你愿不愿意接受“多个数据源各自为政”。我当时的判断是如果能把所有信息来源集中到一个本地服务里把外部接口的差异都消化在采集层那么前端只需面对一个统一的数据模型。以后不管官方接口怎么变我只改采集模块前端几乎不用动。这个思路后来被证明是 AnyPS5 最值得坚持的一点。1.3 定位只做聚合层不做另一个生态入口项目做到一半时我也犹豫过要不要顺手加入远程启动游戏、修改存档、跨区购买之类的“进阶功能”。后来我把它们全部砍掉了原因很简单维护成本和风险都不值当。AnyPS5 的定位永远是一层聚合器不扮演生态入口。它从官方公开的能力和本地网络协议里读取状态把数据存进本地数据库然后以 Web 页面的形式展示。任何涉及写操作、账户交易、系统层修改的内容都不属于这个项目的边界。这样做有几个直接好处一是安全边界清晰不会因为存了不该存的东西而提心吊胆二是项目迭代时不用每改一行代码都担心兼容性三是别人用起来也放心都知道这个工具只是“帮忙看”不是“替你做”。一个聚合工具最怕的就是野心太大。我只想解决“信息查询太散”的问题至于购买和决策留给玩家自己做就好。2. 整体设计思路把数据源收口成一个本地服务2.1 边界划分先解决“读”的需求功能范围一开始就要划清楚。我给自己定的原则是只读为主可控写作为辅。只读的部分包括主机在线状态、当前运行的游戏、已安装游戏库、官方商店折扣信息、截图与视频素材列表、奖杯进度。可控写入的部分只有两个一是发送局域网唤醒包让处于待机状态的主机开机二是触发一次截图归档任务把新文件拉取到本地。除此之外不做任何需要账号密码敏感操作的“写”。这个边界划分在后期帮了大忙。因为所有采集模块都依赖外部接口接口一变就容易出错。如果把写操作也押上去接口一旦调整用户可能直接卡死在流程里而且错误很难排查。只读特性让每个模块都是独立的“拉取-解析-落库”流程出问题顶多某一块数据不更新不会让整套服务瘫痪。2.2 技术选型让人愿意维护的轻量栈AnyPS5 的技术栈特别朴素朴素到很多朋友看到后都觉得不够“极客”。服务端用的是 Node.js配一个轻量的 Web 框架定时任务用系统自带能力或简单的 node-cron 就能跑。数据库选择了 SQLite不单独部署服务数据就是本地一个文件备份时把这个文件拷走即可。前端没有引入重框架只用了比较轻的渲染方案加少量原生 JavaScript页面卡片化布局尽量做到手机和桌面都能直接浏览。整体只要求能跑在一台旧笔记本或入门级 NAS 上内存占用控制在一两百兆以内。为什么选这么轻的组合我自己的体会是自托管项目最大的敌人不是性能而是懒得维护。如果一上来就上微服务、消息队列、容器编排机器配置要求高了部署步骤复杂了项目大概率在一两个月后被弃养。轻量栈最大的优势是想改就改今天加一个模块明天调一个样式重启服务一两秒完成。对于一个面向个人或小圈子的工具来说这种快感比抽象架构重要得多。2.3 模块布局与数据流向整个系统可以分为四层外部数据源、采集层、存储层、展示层。外部数据源包括主机本身、官方商城页面、官方奖杯接口、本地局域网共享目录。采集层由不同的定时任务组成每个模块负责一类数据。存储层使用 SQLite 保存采集结果外加一个文件目录用来整理截图素材。展示层就是一个 Web 服务提供状态页、折扣列表、素材库和奖杯页四大板块。数据流向是单向的这点我想强调一下。采集层从外部源拿到数据后先做清洗和格式转换再落库。前端不直接访问外部接口只读取本地 API。这么做的好处是即使在断电断网的情况下历史数据依然可以查看不至于打开面板一片空白。模块外部数据源采集方式落库内容状态模块主机本地网络接口定时轮询在线状态、当前应用库存模块官方商店公开页面定时任务游戏名称、价格、折扣区间素材模块局域网共享目录手动/定时同步文件名、大小、所属游戏奖杯模块官方公开接口定时任务奖杯列表、获得时间、稀有度用文字描述这个架构可能显得有些干但实际跑起来之后你会发现它最大的优点是每个模块都可以独立拆出来测试。新加一个数据源时只需照葫芦画瓢写一个采集任务前端对应加一个页面组件完全不影响其他功能。3. 核心模块实现状态、库存、素材和奖杯3.1 设备状态感知与远程唤醒AnyPS5 第一个做的模块是状态感知它解决的是“主机现在到底开没开、在跑什么”的问题。实现方式并不神秘本质上就是局域网内的地址发现加定时状态探测。主机启动后会暴露一些本地网络服务我在服务端通过 mDNS 或 IP 扫描找到它的地址然后定时调用状态接口。关键点是超时控制休眠状态和网络拥塞都会导致请求超时如果不加区分很容易把在线误判成离线。这里贴一段简化的状态轮询逻辑真实的实现会多一重异常分支// 状态轮询简化版真实代码会更强调超时与重试 async function checkConsoleStatus(host, maxTimeout 5000) { const started Date.now(); try { const controller new AbortController(); const timer setTimeout(() controller.abort(), maxTimeout); const response await fetch(http://${host}/status, { signal: controller.signal }); clearTimeout(timer); if (!response.ok) return error; const data await response.json(); return data.running ? online : standby; } catch (err) { // 超时不一定等于离线可能是休眠握手慢 if (Date.now() - started maxTimeout) return pending; return offline; } }远程唤醒模块用的是局域网唤醒包也就是魔法包。主机的网络设置里允许局域网唤醒后发一个 UDP 广播就能让它从待机状态开机。这里有一个非常容易踩的坑很多无线路由器会过滤广播包导致服务端发出去之后主机毫无反应。后来我把发送方式从广播改成了单播并确保服务端和主机在同一网段成功率一下子上来了。3.2 游戏库与折扣信息聚合折扣数据是最依赖外部接口的模块。官方商店页面有公开的网页数据我并没有做任何绕过机制只是按照正常频率去读取公开页面然后解析出游戏名称、原价、折后价和折扣截止时间。采集任务固定在每天凌晨执行一次因为折扣信息不需要秒级实时。数据清洗时最需要注意的是价格字段有的地区显示含税价有的地区不含税解析时如果直接把数字存成整数后面很容易出现偏差。我统一把价格换算成以“分”为单位的整数存储展示层再按其格式化。这样就不会出现 9.99 和 10.00 混在一起的尴尬情况。聚合完成之后我加了两个很实用的规则一是价格低于设定阈值的游戏自动标记为“重点关注”二是在折扣截止前 24 小时做一次提醒。阈值这个功能特别适合等某个游戏进折扣的玩家比如原价三百多的游戏心里预期是低于两百再入手那么阈值就设成两百命中后直接在面板首页出现提示。3.3 截图与视频素材的本地归档素材模块在最开始只是想把主机里的截图自动整理到 NAS后来发现真正的难点在于分类和去重。主机截图默认会按游戏标题形成目录但不同语言环境下同一款游戏可能显示不同的目录名比如中文环境和英文环境的目录名不一样。我让采集任务先去读取共享目录再按三种规则做归类优先匹配已入库的游戏 ID其次匹配游戏标题关键词最后落到“未识别”目录。未识别目录需要人工处理一次处理完后我会把规则补充到配置里下次就不会再分错了。去重逻辑也很重要。截图文件会频繁出现同名文件或重复导出的情况我用“文件大小 修改时间 文件名”三个维度做联合判断只有三个条件同时变化时才视为新素材。这个方案比单纯比较哈希节省很多计算时间因为局域网共享目录里的文件动辄几百个逐个算哈希会导致归档任务非常慢。归档完成后素材模块会生成一张调度表记录哪些游戏最近新增了什么素材前端素材库页面按游戏分组展示缩略图并支持下载到手机或电脑。3.4 奖杯数据展示只读透明的处理方式奖杯模块是我写起来最谨慎的一块因为涉及账号数据稍不注意就容易踩到授权红线。我的处理方式是只走官方公开接口申请只读权限不碰任何敏感操作。采集任务拿到奖杯列表后以“奖杯 ID 游戏 ID 获得时间”作为唯一索引入库以后定期更新稀有度数据。稀有度是玩家比较在意的信息它会随着全球玩家获得情况动态变化所以每次采集时都要刷新一次。这里有一个让我印象深刻的坑奖杯 ID 的大小写在不同接口版本里可能不一致。第一次接入时我直接用原始字符串做索引结果同一个奖杯因为大小写不同被存成了两条记录页面看起来像是同一个游戏里有几百个重复奖杯。后来统一做了一次规则化处理所有 ID 在入库前转为统一格式问题立刻消失。展示层上奖杯模块按游戏列表、合集进度和最近获得三个维度呈现。合集进度用简单的进度条表示最近获得则按时间倒序方便快速翻看自己这段时间在玩什么。整个模块只做陈列和汇总不做任何代操作这也是它运行半年多以来一直很稳定的原因。4. 运行半年踩过的坑跨端、假在线、凭证和缓存4.1 手机浏览器把字体放大布局全乱了AnyPS5 早期版本在桌面浏览器上很正常但手机上打开后页面会突然变得特别糟糕卡片错位、按钮溢出、表格横滚。最开始我以为是框架问题直到在调试台里看到用户代理是手机浏览器才意识到是移动端渲染没有专门处理。根因有两个一是字号基准值太小手机浏览器会自动尝试放大文本二是卡片宽度用了固定像素导致小屏设备放不下。解决办法也很朴素把字号基准提高到适合移动阅读的值卡片宽度改为自适应布局重要按钮加上触控热区。改完之后再做了一次“手机缩放测试”用不同尺寸的虚拟窗口分别打开面板确认没有横向滚动条这才算通过。4.2 主机明明在跑面板却显示离线“假离线”是状态模块上线后遇到的第一个大问题。具体表现为主机明明在运行游戏面板上却长时间显示离线偶尔刷新一下又恢复在线。排查链路是这样的先看采集日志发现状态接口偶尔会超时再看网络拓扑确认主机在路由器重启后拿到了新的 IP 地址接着看配置原来 IP 地址是写死在配置文件里的没有做动态发现。三者叠加在一起就形成了“IP 变了端口探不通超时被判为离线”的结果。解决方案分三层第一层定时做局域网设备发现自动刷新主机地址第二层状态判定时引入“最近一次成功时间”做加权不因为一次失败就立刻标记离线第三层缓存状态设置有效时间避免前端频繁刷新导致误判。这三层都落地之后假离线的出现频率几乎降为零。4.3 账号凭证怎么存才安全AnyPS5 会涉及到一些外部接口授权所以凭证管理是绝对不能忽视的一环。我的原则是能不用账号密码就不用即使用也只存令牌绝不保存明文。具体来说授权令牌会单独放在一个本地配置文件中启动时用一个随机密钥解密后再写入内存。这个随机密钥存放在服务目录之外只有部署者自己知道。所有采集任务都只申请只读权限不申请任何资金操作相关权限。如果令牌需要更新面板里会给出提醒但不会自动重登。有一次我为了图省事把令牌直接写进环境变量结果排查问题时截图发到聊天工具里虽然很快撤回但还是让我紧张了很久。从那之后我养成一个习惯任何包含令牌的日志、截图、调试输出一律先做脱敏再处理。自托管项目没有专人盯着安全这种细节只能靠自己把关。4.4 缓存过期和时区陷阱所有采集模块都涉及缓存但折扣模块的缓存最容易出错因为它的数据天然具备时效性。第一次实现时我直接把折扣截止时间存成 Unix 时间戳并在前端用本地时区格式化。看起来没问题但跨时区部署后发现同样的截止时间在北京时间下显示的和在伦敦时间下显示的相差八小时。后来我统一把时间存储改成 ISO 8601 字符串展示层再按访问者自己的本地时区格式化问题才彻底解决。缓存策略也做了分级折扣数据因为精度要求高两个小时过期奖杯稀有度因为变化较慢六小时过期游戏库列表则保留二十四小时后重新拉取。分级缓存的本质是“用不同的新鲜度换取不同的请求负载”在自托管场景下特别实用既不浪费外网请求又保证数据不会太旧。5. 现在的使用习惯与下一步方向5.1 我每天怎么用 AnyPS5现在这台 AnyPS5 已经成了我书房的固定成员。早上起来我会先看一眼面板首页的折扣汇总如果某个常玩的游戏进入了“重点关注”区间就顺手点进去看详情。午休时间用手机打开面板翻一下素材库确认昨晚自动归档的截图有没有分错游戏。晚上准备玩之前我会看一眼状态卡片如果主机还在待机就点一下远程唤醒等走进书房时它已经启动好了。整个使用过程中我基本不需要再打开官方 App 去查价格、导截图或者翻奖杯。很多步骤从原来的“打开软件-等待加载-定位功能-操作”变成了“打开面板-一眼可见-完事”。这种体验带来的幸福感不是功能数量堆出来的而是“少了两三次让人烦躁的跳转”。5.2 下一步插件化数据源与家庭自动化联动AnyPS5 目前的数据源还是写死的那几类下一步我打算把采集层彻底插件化。每个数据源对应一个插件目录目录里包含采集逻辑和字段描述前端可以自动识别插件并生成对应的展示卡片。这样一来即使将来有新的公开数据源出现也不需要改动核心代码只需要新增一个插件。另一个想法是和家庭自动化联动。比如面板检测到主机进入待机状态后可以自动触发一个过场动画展示在墙上的小屏幕上或者当截图归档完成时在电视端弹出一个通知。这些都属于“让数据自己流动”的范畴做起来并不复杂但需要想清楚触发条件和自动化规则避免出现误触。最后再分享一点个人经验如果你也想搭一个类似的自托管工具我最大的建议是先定边界再写代码先做最小的端到端流程再谈功能扩充。不要一上来就幻想做一个全能控制中心从“能看状态、能看折扣、能看截图”这三个最基础的只读功能开始跑顺之后你自然会知道下一个模块该加什么。工具是为习惯服务的不是为自我感动服务的。AnyPS5 真正值钱的地方不在于它有多炫而在于它把那些本来要反复切换的碎片操作变成了一次统一的、不用思考的访问路径。如果你也有类似的主机使用痛点给它一个周末写一个简化版出来我相信你会爱上这种“自己掌控信息流”的感觉。