1. 从“看商店”这个日常痛点说起
如果你刷到过 valorant-skin-cli 这个项目,大概率和我一样,已经受够了每次想确认今日商店皮肤都要先把游戏客户端打开的那种别扭流程。这个工具不复杂,却精准解决了一个高频需求:不启动游戏,不输入账号密码,直接在终端里查看瓦洛兰特当日皮肤商店、收藏库和点券余额,还能把数据接给脚本做自动化监控。
我最开始也不觉得命令行看皮肤有什么必要,直到我想在某个指定皮肤出现的瞬间收到提醒,才发现网页版社区皮肤展示虽然有,但没法给程序用。CLI 的价值正在这里:它把“看商店”从一次性的手动操作变成了一个稳定的数据入口,脚本可以每隔几分钟请求一次,拿到结构化数据后自己决定怎么处理。输出到终端只是最简单的一种消费方式。
什么人会需要这个工具?我总结是三类:一是平时不常登录游戏但会关注商店更新的玩家,二是在做 Valorant 周边工具或数据统计的开发者,三是喜欢把一切事情都塞进终端、不愿意被 GUI 绑架的人。如果你只是偶尔打开游戏顺手看一眼,那网页社区完全够用;但如果你想“掌控”皮肤信息,而不是被动等待刷新,CLI 几乎是现阶段最合适的形态。
1.1 这个工具真正解决的三个问题
第一个问题是客户端启动成本太高。很多人都有过这种经历:晚上突然想起来商店刷新了,打开客户端,结果赶上版本更新,下载几百兆,等进度条走完已经没有购买的冲动了。CLI 可以把整个过程压缩到几秒钟,而且不占用显卡和内存。
第二个问题是官方没有提供直接的“商店订阅”能力。拳头游戏虽然有一个比较完整的数据后台,但普通玩家没有公开接口可以查询自己的商店,只能登录游戏或者依赖第三方社区网站。第三方网站要手动打开、手动搜索,还没法做持续监控,用户体验始终隔了一层。
第三个问题是自动化入口缺失。如果你想做心愿单监控,想在某个皮肤重新上架时立刻收到通知,或者想统计过去一年商店到底出现多少次某个皮肤,靠人工刷新根本做不到。CLI 把这个缺口补上了,一条命令就能返回机器可读的数据,剩下的事情全部可以交给脚本。
最近 AI 编程工具持续火起来之后,CLI 这种交互形式重新回到了大众视野。市面上的 codex cli、trae cli 本质上都在做同一件事:把复杂的操作收敛成一条带参数的指令,让机器和人都能理解。valorant-skin-cli 也是这个思路的产物,它把查商店、查余额、看收藏这些动作全部收进终端里,非常适合继续往上层搭各种自动化。
1.2 为什么不做 GUI,非要坚持命令行
做图形界面需要处理窗口布局、跨平台打包、版本更新提醒,维护成本很高。做一个网页服务也行,但意味着要部署在线服务,还要接触更多玩家的账号数据,安全责任一下子变大。CLI 的投入产出比是最高的,它只需要处理三件事:参数解析、HTTP 请求、终端输出,就能覆盖绝大多数真实使用场景。
命令行工具还有一个天然优势:支持管道和组合。我可以直接写valorant-skin-cli store --json | jq '.entries[].name',提取出所有皮肤名称,再交给下一个脚本做过滤或通知。这种能力传统客户端给不了,因为 GUI 的交互是用鼠标点的,没法轻松形成可编程链条。
在 Linux 服务器或者无图形界面的场景下,CLI 更是唯一选择。我见过有人把自己账号的本地数据接口接到群晖 NAS 的定时任务里,每天凌晨自动抓取商店并保存到数据库。这种玩法离了 CLI 基本没法实现。
2. 拆解核心原理:数据从哪来,怎样保证安全
很多人第一次看到这个工具时都会疑惑:游戏皮肤数据不是只能在客户端里看吗?Riot 客户端为什么会在本地开放接口?其实官方客户端为了实现登录态、商品展示、余额查询这些功能,内部本来就要跟服务器通信,而承载这些通信的本地服务一直存在,只是普通玩家没有注意到。
2.1 本地 lockfile 决定了整个工具的运行方式
Riot 客户端启动后,会在本机的 Riot Client 配置目录下生成一个名为 lockfile 的文件。这个文件有一行文本,用冒号分隔了客户端名称、进程 PID、端口号、密码和协议。CLI 工具读取这个文件,就能拿到本地服务地址以及一套临时认证信息。
lockfile 的路径在不同系统上不完全一样。Windows 通常在%LOCALAPPDATA%\Riot Games\Riot Client\Config\lockfile,macOS 通常在~/Library/Application Support/Riot Games/Riot Client/Config/lockfile,Linux 下位置不固定,更需要程序做多路径探测或手动配置。
使用 lockfile 的最大好处是省掉了账号密码管理。工具不需要保存玩家的账号密码,也不需要处理两步验证,只要当前系统里已经有一个处于登录状态的官方客户端,CLI 就可以借用这个身份来请求用户自己的数据。对使用者来说,这比存一份明文密码在本地安全得多。
2.2 一次完整的数据请求流程
整个流程可以拆成五个步骤:
- 读取 lockfile,解析出端口、密码和协议。
- 构造基础认证请求头,用户名固定为 riot,密码就是 lockfile 里那串随机字符串。
- 请求本地接口的商店皮肤端点,拿到当天的皮肤 ID、价格和剩余时间。
- 对 JSON 返回做字段清洗和规范化。
- 根据用户参数渲染成表格、纯文本或 JSON 输出。
这里有一个容易误解的点:本地接口监听在 127.0.0.1,只有本机进程能访问,不会暴露到局域网。数据最终虽然是远端服务返回的,但请求链路经过的是官方客户端建立的安全通道。我在代码里不写任何上传逻辑,也不允许工具把请求转发到第三方服务器,安全边界非常清晰。
2.3 为什么不直接调云端的玩家数据服务
社区里有人会走完整 OAuth 流程获取 Access Token,再直接请求 Riot 官方的区域数据服务,这样能拿到更完整的收藏库信息。但这套流程有一个很现实的问题:需要处理登录跳转、回调地址、令牌过期和刷新令牌,复杂度一下子提升不少。
更重要的是,频繁触发令牌申请容易引来官方风控,轻则请求失败,重则可能影响账号安全。我在实际测试中比较过,本地接口的路径短、响应稳定,更适合作第一版功能的数据源。如果你之后想做非本机部署,比如跑在一台没有登录客户端的服务器上,再考虑完整的令牌流程不迟。
2.4 功能模块怎么拆分
我起初想把所有功能一次性做完,后来发现还是要克制。第一版我建议只做四个模块:商店查询、收藏库查询、余额查询、轮询提醒。
商店查询负责展示今日商店的皮肤与价格;收藏库查询读取账号已经拥有的皮肤列表;余额查询展示当前 VP 点和 Radianite 点数量;轮询提醒则周期性地请求商店接口,当发现新皮肤出现时在终端输出提醒。
这四个模块互相独立,底层共用一套 lockfile 读取和 HTTP 请求代码,上层各自负责不同的业务逻辑。这样拆的好处是每块功能都能单独测试,不会因为一个模块出错把整个工具拖垮。
3. 手把手把 CLI 搭起来
下面这一段是我实际搭建项目时走过的完整路径。我选用 Node.js 来实现,原因是跨平台支持好、异步编程顺手、终端生态成熟。你用 Python 或者 Go 也能达到差不多的效果,但 Node 在命令行工具这个细分领域里确实很舒服。
3.1 初始化项目与依赖选择
先建一个新目录并初始化 npm 项目:
mkdir valorant-skin-cli cd valorant-skin-cli npm init -y npm install commander node-fetch chalk cli-table3commander 用来解析命令行参数,node-fetch 发送 HTTP 请求,chalk 给终端输出配色,cli-table3 把数据渲染成表格。如果你本机 Node 版本在 18 以上,内置了 fetch,node-fetch 可以不装,但保留它能让老版本 Node 也能运行,兼容性更好。
在 package.json 里加上 bin 配置,把命令注册到全局:
"bin": { "valorant-skin-cli": "./bin/index.js" }然后创建 bin/index.js 作为入口文件,文件顶部必须有#!/usr/bin/env node这一行,否则系统不知道怎么解释这个文件。做完这些之后执行npm link,终端里就能直接使用valorant-skin-cli命令了。
3.2 读取 lockfile 的完整代码
lockfile 读取是整个工具的地基,代码写得好不好直接影响后续功能稳定性。下面是我的实现:
const fs = require('fs'); const path = require('path'); function getLockfile() { const candidates = [ path.join(process.env.LOCALAPPDATA || '', 'Riot Games', 'Riot Client', 'Config', 'lockfile'), path.join(process.env.HOME || '', 'Library', 'Application Support', 'Riot Games', 'Riot Client', 'Config', 'lockfile'), path.join(process.env.HOME || '', '.local', 'share', 'riotclient', 'Config', 'lockfile') ]; for (const p of candidates) { if (fs.existsSync(p)) { const parts = fs.readFileSync(p, 'utf8').trim().split(':'); return { name: parts[0], pid: Number(parts[1]), port: Number(parts[2]), password: parts[3], protocol: parts[4], file: p }; } } throw new Error('找不到 lockfile,请确认 Riot 客户端已启动并已登录'); } module.exports = { getLockfile };有几个细节很容易踩坑。第一,文件读取后必须做 trim,因为末尾可能有换行符,不处理的话 split 出来的最后一个字段会带\n。第二,pid 和 port 建议转成数字类型,后续比较和拼接 URL 时不容易出类型问题。第三,候选路径数组是顺序尝试的,如果你用了绿色版或者自定义安装目录,可能需要手动添加路径。
3.3 请求商店接口并处理证书问题
拿到 lockfile 之后,下一步就是请求本地接口。商店数据的请求端点和参数会随客户端版本变化,下面的路径来自我长期使用的稳定版本:
const https = require('https'); const { getLockfile } = require('./lockfile'); async function fetchStore() { const lockfile = getLockfile(); const baseUrl = `${lockfile.protocol}://127.0.0.1:${lockfile.port}`; const auth = 'Basic ' + Buffer.from('riot:' + lockfile.password).toString('base64'); const res = await fetch(`${baseUrl}/valorant-store/v1/skin-offers`, { headers: { Authorization: auth }, agent: new https.Agent({ rejectUnauthorized: false }) }); if (!res.ok) { throw new Error(`商店接口请求失败: ${res.status} ${res.statusText}`); } return res.json(); }这里有一个所有做同类工具的人都会遇到的问题:本地接口的 HTTPS 证书是自签名的,Node 默认会拒绝。解决办法是给当前请求单独设置一个不校验证书的 HTTPS agent。注意不要直接修改全局的NODE_TLS_REJECT_UNAUTHORIZED环境变量,否则会影响程序中其他所有 HTTPS 请求。只对 127.0.0.1 这个地址关闭校验,风险是可控的。
接口返回的 JSON 通常包含商店皮肤 ID、价格、剩余时间等字段,但不同版本结构可能不同。我的做法是先做一层适配,把原始 JSON 统一清洗成entries数组,后续渲染层只认这个结构,接口字段变了也只改适配层一个地方。
3.4 命令入口与表格渲染
CLI 的入口用 commander 注册子命令。第一版我只需要一个 store 命令:
#!/usr/bin/env node const { Command } = require('commander'); const { fetchStore } = require('../src/api'); const { renderStore } = require('../src/render'); const program = new Command(); program .name('valorant-skin-cli') .description('在终端里查看 Valorant 皮肤商店') .version('0.1.0'); program .command('store') .description('显示今日商店') .option('--json', '输出 JSON 格式') .action(async (options) => { try { const data = await fetchStore(); if (options.json) { console.log(JSON.stringify(data, null, 2)); } else { renderStore(data); } } catch (err) { console.error('获取商店失败:', err.message); process.exit(1); } }); program.parse(process.argv);渲染函数用 cli-table3 输出表格,用 chalk 给价格上色:
const Table = require('cli-table3'); const chalk = require('chalk'); function renderStore(data) { const table = new Table({ head: ['皮肤名称', '价格(VP)', '剩余时间'], colWidths: [45, 12, 20] }); for (const item of data.entries) { table.push([ item.name, chalk.green(String(item.price)), item.remaining ]); } console.log(table.toString()); }这里的设计逻辑是:默认输出给人看,--json输出给脚本用。很多命令行工具只关注终端展示,忽略了机器可读输出,结果用户想配合 jq 处理都没有入口。我建议从第一版就把 JSON 输出当作一等公民对待。
3.5 轮询提醒功能怎么实现
商店每天刷新时间是固定的,但每个人的作息不一样。我加了 watch 子命令,每隔几分钟自动请求商店接口,如果发现皮肤 ID 列表里出现新的项,就在终端输出提醒:
async function watch(intervalMinutes) { let previous = new Set(); const intervalMs = intervalMinutes * 60 * 1000; console.log(`开始监控商店,每 ${intervalMinutes} 分钟检查一次`); setInterval(async () => { try { const data = await fetchStore(); const current = new Set(data.entryIds || []); const added = [...current].filter((id) => !previous.has(id)); if (added.length > 0) { console.log(`发现新皮肤,时间:${new Date().toLocaleTimeString()}`); for (const id of added) { console.log(`- ${id}`); } } previous = current; } catch (err) { console.error('检查失败:', err.message); } }, intervalMs); }这个版本的 previous 只存在内存里,进程一旦重启就会丢失对比基准。我后来改成把历史 ID 缓存到本地文件,重启后还能继续对比,避免错过一次完整刷新周期。如果你要长期挂机,建议早点做持久化,不然后续统计会少掉前面几天的数据。
4. 常见问题与排查技巧实录
工具写出来不难,真正用起来之后踩到的坑比想象中多。我把这些经验整理成一份速查表,方便你遇到问题时快速定位。
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 找不到 lockfile | Riot 客户端未启动或未登录 | 先手动打开客户端完成登录 |
| 找不到 lockfile | 安装路径不是默认路径 | 用 find 命令搜索 lockfile 并手动配置路径 |
| 请求失败:self signed certificate | 本地接口自签名证书被拒绝 | 只对当前请求设置 rejectUnauthorized false |
| 接口返回 401 | 认证头构造错误 | 检查密码字段是否被错误分割 |
| 接口返回 404 | 本地 API 路径随版本变化 | 抓包确认新路径,或提供 --endpoint 参数覆盖 |
| 请求频繁被限制 | 轮询间隔太短 | 把默认间隔调到 5 分钟以上 |
4.1 本地接口连不上,先别急着找网络原因
运行时报找不到 lockfile,最常见的三个原因:客户端没有启动、启动了但还没完成登录、当前终端用户和客户端用户不是同一个系统账户。我踩过最隐蔽的坑是:终端用管理员权限启动,游戏客户端却是在普通用户桌面下启动的,两个进程读到的 LOCALAPPDATA 环境变量完全不同,自然找不到同一份配置文件。
排查顺序建议这样:先打开配置目录,看 lockfile 文件是否存在;存在的话 cat 一下确认不是空文件;再检查是不是跨用户访问问题。很多情况下把终端权限降到普通用户就能解决。
4.2 证书校验失败是这类本地接口工具的常见问题
报错文本通常是self signed certificate或者unable to verify the first certificate。原因就是本地服务的 HTTPS 证书没有经过系统信任链认证。解决办法是在请求时构造一个关闭校验的 https.Agent,代码示例已经在上面给出。
务必注意作用域。不要为了省事在环境变量里全局关闭校验,否则后续请求外部服务时会失去基本的安全保障。我见过有人在同一个脚本里同时请求本地接口和外部 API,因为全局关闭证书校验,结果外部请求也被中间人工具截获,这是完全可以避免的风险。
4.3 接口返回 401 或 404 的时候怎么定位
401 说明认证信息不对。先检查 lockfile 内容里的密码字段是否被正确解析,尤其是用 split(':') 分割时,如果密码段出现冒号就会错位。稳妥做法是先按前四个字段切开,再把后面的内容全部拼接为密码。
404 则基本是接口路径更新了。客户端升级后部分本地 API 端点会调整,社区工具最怕这种情况。我给命令行加了一个--endpoint参数,允许用户手动覆盖默认路径,这样即使官方改了路径,也不用改代码、不用重新发布版本,直接用参数传新的目标地址即可。
4.4 如何避免请求过于频繁触发限流
本地接口监听在 127.0.0.1,但数据最终来自官方服务器。如果轮询间隔设置成 1 秒,短时间内一定会触发对端限制。正常使用场景下,商店一天只刷新一次,完全没有高频请求的必要。我的默认间隔是 5 分钟一次,这个频率既能保证及时看到刷新结果,也不会给服务端造成压力。
如果你需要监控特定皮肤出现,可以在商店刷新时间点前后半小时提高频率,其他时间切回低频率甚至暂停轮询。商店的刷新时间是统一日历日切换,北京时间大概在早上 8 点前后,设置定时任务时注意时区换算。
4.5 跨平台路径不统一,怎么兼容
Windows 的路径依赖 LOCALAPPDATA,macOS 用的是 Home 目录下的 Application Support,Linux 则完全没有固定标准。代码里我预置了三组候选路径顺序尝试,但仍然覆盖不了所有发行版。对于这种场景,最稳妥的做法是提供一个配置项让用户手动指定 lockfile 路径。
我建议在valorant-skin-cli中增加配置系统,把 lockfile 路径、接口地址、输出格式都存到本地配置文件中。用户装好工具后如果默认路径找不到,可以执行一条命令手动指定,比每次改源码好得多。
5. 还可以继续往哪个方向扩展
如果你已经把基础的商店查询跑通了,后续扩展空间其实非常大。我个人接下来想做的事有三个方向。
第一个是历史价格记录。每天把商店数据存入本地数据库,慢慢积累之后就知道了每个皮肤的返场频率、价格变化趋势,甚至能根据历史数据猜测下一次刷新出现某个皮肤的概率。这比单纯盯着当天商店要有意思得多。
第二个是心愿单监控。与其每次刷出一整片列表再人工找,不如在配置文件中写几个想等的皮肤 ID,CLI 检测到目标出现时直接打出一条醒目的提醒,甚至可以触发系统通知。有了这个能力,就不用天天手动打开客户端看刷新结果了。
第三个是跟其他工具联动。CLI 输出 JSON 结构化数据后,理论上可以接进任何自动化系统,比如存入数据库、发送到通知服务、生成每日商店海报。工具本身只是入口,真正有价值的是在你自己的自动化体系里,它扮演了一个稳定的数据供给角色。
在动手之前有一个建议:第一版只做读取 lockfile 和显示商店两件事,不要一开始就想着把收藏库、价格统计、心愿单全部做完。命令行工具最好的成长路径是先用起来,再在真实使用中慢慢发现缺什么,然后一个一个补。等到你发现自己已经连续用了一周,这个工具基本就是成功的了。