CLI 参考:从别的程序驱动 Mercy SF
mercy-cli 有一个非交互模式:参数进去,一个 JSON 对象出来。它是给面板、守护进程和 脚本用的。
这一页就是承诺。 下面这些命令和字段名不会在不打招呼的情况下变。CLI 的其他一切, 菜单、它的措辞、输出的排版,随时都可以变,所以请不要在那些东西上搭建。
不带参数运行,交互菜单会和以前完全一样地启动。
每个响应都带 "api"。今天它是 1。加字段不会改变它,只读自己认识的字段的调用方不 受影响。它会在字段被删除或含义改变时变化,所以你可以拒绝在一个自己没有为之构建过的 形状上运行。
登录
通过 S&F 单点登录创建的账号不需要额外的东西。一个属于某个服务器、从未纳入单点登录的 账号,在它看来并不存在,需要 --server:
mercy-cli --status --user <account> --server https://f1.sfgame.net/--server 对下面每一条命令都适用。
输出
除 --start 之外,每条命令都只打印 JSON,所以 --json 本来就是默认,传不传都一 样。它存在是为了那些宁愿说明白也不愿依赖默认值的调用方,以及为了 --start,因为它 否则会写一份给人看的日志。
读取
列出一个账号上的角色
mercy-cli --characters --user <account>{
"ok": true,
"characters": [
{ "character": "Hero", "server": "https://s1.sfgame.net/", "level": 345 },
{ "character": "Alt", "server": "https://s42.sfgame.net/", "level": 88 }
]
}一次登录就覆盖这个账号上的每个角色,跨服务器也一样。如果某个角色的状态取不到, level 会是 null,但这一条依然列出来,因为知道它存在正是这次调用的意义。
读取一个角色的状态
mercy-cli --status --user <account> --character <name>当账号恰好只有一个角色时,--character 可以省略。
{
"ok": true,
"character": "Hero",
"server": "https://s1.sfgame.net/",
"level": 345,
"class": "DemonHunter",
"race": "DarkElf",
"experience": { "current": 740300000, "next_level": 769800000 },
"silver": 725000000,
"mushrooms": 8,
"honor": 12043,
"rank": 1800,
"action": "CityGuard",
"attributes": {
"strength": { "base": 4077, "bonus": 1200 },
"dexterity": { "base": 1000, "bonus": 300 },
"intelligence": { "base": 900, "bonus": 250 },
"constitution": { "base": 3000, "bonus": 800 },
"luck": { "base": 700, "bonus": 150 }
},
"equipment": [
{
"slot": "Weapon",
"model_id": 55,
"color": 1,
"class": "DemonHunter",
"epic": true,
"legendary": false,
"armor": 0,
"min_damage": 900,
"max_damage": 1400,
"upgrades": 3,
"attributes": { "strength": 420, "constitution": 210 },
"rune": { "type": "FireDamage", "value": 60 },
"enchantment": "SwordOfVengeance",
"gem": { "type": "Strength", "value": 300 }
}
],
"arena": {
"fights_for_xp": 7,
"next_free_fight": "2026-08-06T12:04:11+02:00",
"opponents": [50099, 34316, 13668]
},
"tavern": {
"thirst_for_adventure_sec": 3600,
"beer_drunk": 2,
"mushroom_skip_allowed": true,
"quests": [
{ "length_sec": 600, "silver": 120000, "experience": 45000,
"location": "Wolves", "item": true }
]
},
"guild": {
"name": "Blvck Clover",
"rank": 12,
"honor": 90000,
"members": [
{ "name": "Hero", "level": 345, "last_online": "2026-08-06T09:12:00+02:00" }
]
},
"scrapbook_items": 1102
}所有内容都来自同一次状态拉取,所以没有分区可以点名索取,也没法靠少要一点来省下什 么。
角色不在任何公会时,guild 是 null。在剪贴簿被读过至少一次之前, scrapbook_items 是 null。空的装备槽会被省略,而不是列成 null。
关于物品名称: 协议里带的是模型 id 和槽位,而不是显示名称,所以你拿到的就是这 些。它同时也是美术资源的寻址方式,因此面板可以画出和桌面应用一模一样的图。在这里编 一个名字,等于给你一个无从核对的猜测。
按间隔重复读取
mercy-cli --status --user <account> --character <name> --watch 60登录一次,然后每 60 秒吐出一个对象,直到进程被停掉。请用这个,而不是在循环里反复跑 命令:每跑一次就是一次全新登录,而那是盯一个数字最贵的做法,也是在服务器那一侧最扎 眼的做法。下限是 30 秒。失败的一轮会被打印出来,循环继续;一个碰到第一次打嗝就放弃 的观察者,看起来和一个「什么事都没有」的观察者一模一样。
它接下来会打谁,以及为什么
mercy-cli --plan --user <account> --character <name>{
"ok": true,
"outcome": "planned",
"reason": "Next: Someone — 3 scrapbook items, 72% odds",
"candidates": [
{
"name": "Someone",
"level": 340,
"source": "pool",
"missing_items": 3,
"rank_gain": 12,
"favourite": false,
"win_chance": 0.72,
"item_score": 300.0,
"xp_score": 60.0,
"rank_score": 25.0,
"base": 385.0,
"score": 277.2
}
]
}和应用画出来的是同一个判断:候选人按物品、每日经验和名次打分,再乘以胜率。outcome 是 planned、waiting、blocked 或 empty 之一,而 reason 用一句话说明是哪 个。
这不会花掉任何一场战斗,也不会花蘑菇。但它确实会为了找候选人而爬取,所以在请求上并 不免费,别把它放进一个紧凑的循环里。
预演一场战斗
mercy-cli --simulate --user <account> --character <name> --against <player>{ "ok": true, "against": "Someone", "against_level": 340, "win_chance": 0.63, "iterations": 1000 }纯算术。会有一次查询发到服务器去确认对方是谁;战斗本身从不发送。win_chance 是胜场 除以分出胜负的场数,所以一次无法收敛的模拟,报告的基数会少于它实际跑的次数。
身上带着什么
mercy-cli --inventory --user <account> --character <name>{ "ok": true, "carried": 12, "backpack": [ { "slot_index": 0, "model_id": 55 } ] }只有一份列表,因为游戏也只保留一份。物品的结构和 --status 里相同,另加 slot_index。
剪贴簿
mercy-cli --scrapbook --user <account> --character <name>{ "ok": true, "unlocked": true, "owned": 1102, "monsters": 214 }在赋予剪贴簿的那个等级以下,unlocked 是 false,而 owned 在那时是 null。这 不是错误。
记录下来的战斗
mercy-cli --history --user <account> --character <name> --limit 50{ "ok": true, "total": 4210, "returned": 50, "battles": [] }最新的排在前面。这些是由跑过机器人的那台安装记录下来的,不是从服务器取回的,所 以一个刚启动的进程没有东西可展示,那并不是故障。total 是存在的总数,returned 是 --limit 之后剩下的数量。
执行
运行机器人
mercy-cli --start --user <account> --character <name>一直跑到进程被停掉,其间持续记录日志。这里 --character 是必需的:列清单和读状 态都无害,但启动机器人就是在玩这个游戏,而选错角色不是一件可以撤销的事。
--all 则会在这一个进程里跑这个账号的每一个角色。那是每账号一次登录,而不是每角色 一次,既更省,也能避免角色之间互相把会话作废。
按下 Ctrl-C(或 SIGTERM)时,当前这一步会被允许走完,然后进程才退出,而不是在请求中 途被切断。
用事件代替文字
mercy-cli --start --user <account> --character <name> --events每行一个 JSON 对象,守护进程可以一行一行地读:
{"at":"2026-08-06T17:22:04+02:00","api":1,"event":"arena_win","character":"Hero"}
{"at":"2026-08-06T17:22:31+02:00","api":1,"event":"quest_done","character":"Hero"}event 是 started、stopping、stopped、arena_win、arena_loss、 dungeon_win、dungeon_loss、quest_done、scrapbook_win、scrapbook_loss、 scrapbook_items(它会带上 count)之一。
这份清单会变长。 请把不认识的事件当成可以忽略的一个,而不是当成错误;这些名字之 所以被明确写出来、而不是随 Rust 打印什么算什么,理由全在这里。
请用这个,而不是去读那份给人看的日志。那份日志是写给人的,随时可以被改写措辞;解析 它意味着别人把一句话写得更好时,你的工具就坏了。
把等着的东西收掉
mercy-cli --claim --user <account> --character <name>日历、每日任务、待领的解锁。--character 是必需的,理由和 --start 一样。
读取或修改配置
mercy-cli --config --user <account> --character <name>
mercy-cli --config --user <account> --character <name> --set auto_arena=false{ "ok": true, "settable": ["auto_arena"], "changed": [ { "key": "auto_arena", "from": true, "to": false } ] }读取会把整份配置放在 config 下返回。那一块里面的字段名不属于这份承诺,只有 settable 中列出的键属于。配置有远超一百个字段,而且每个版本都在长;把它们全部承诺 下来,就等于以后再也不能重新整理设置了。
--set 接受 true/false、on/off、yes/no、1/0,始终要求 --character(一个 开关属于某一个角色,而「反正只有这一个」是一种读的时候没问题、写的时候就是错的猜 测),并且可以重复使用。值随后会从磁盘上回读一次,如果没有落盘,这次调用就会失败, 因为报告一个没有发生的改动,是唯一一种你无法补救的结果。
运维
这是哪个构建
mercy-cli --version{ "ok": true, "version": "2.12.0", "api": 1 }唯一一条既不需要密码也不需要网络的命令。守护进程必须能在拿到用于对话的凭据之前,先 问清楚自己在跟什么对话。
它通不通
mercy-cli --health --user <account> --character <name>{ "ok": true, "character": "Hero", "level": 345, "round_trip_ms": 412 }登录、拉取、报告、退出。刻意做得很小:一个返回完整角色面板的健康检查,会诱使别人去 解析它,而那之后,这个检查本身就变成了又一个接口。
密码
没有 --password 这个参数,是有意为之。命令行参数在这台机器上被其他每一个进程都读 得到,ps、/proc、任务管理器都行,所以一个替好几个人跑这条命令的面板,等于把他们 全部的游戏密码交给任何一个在这台机器上有 shell 的人。
请改用这两条路:
MERCY_SF_PASSWORD='…' mercy-cli --status --user someoneprintf '%s' "$password" | mercy-cli --status --user someone --password-stdin--password-stdin 读标准输入的第一行,这样秘密连环境变量里也不会留下。
退出码
| 码 | 含义 |
|---|---|
0 | 成功,--help 也是这个 |
1 | 跑了,但失败了,原因在 error 里,ok 是 false |
2 | 调用本身写错了:未知选项、缺少取值、没给动作。输出在 stderr |
1 和 2 的区别对守护进程有意义:2 每次都会以同样的方式失败,需要人来处理;1 则很可能 下一次就成了。
错误
失败同样是 JSON,所以一个解析器就能同时处理两种情况:
{ "ok": false, "error": "no character called 'Typo' on this account" }每个响应里都有 ok。在做别的事之前先检查它。
常见原因:
| 消息 | 该怎么办 |
|---|---|
--user is required | 少了 --user(除 --version 外的每个动作) |
--start needs --character (or --all) | 点名目标;它不会去猜 |
this account has N characters — name one with --character | 有好几个,挑一个 |
no character called '…' on this account | 拼错了;--characters 会列出有效名字 |
no password: set MERCY_SF_PASSWORD or pass --password-stdin | 从两条密码路径里选一条 |
login failed: … | 检查凭据或服务器;非单点登录的账号需要 --server |
'…' is not settable from here | 不在允许清单上;不带 --set 的 --config 会打印 settable |
'…' did not save — it still reads back unchanged | 改动没落盘;检查写入权限 |
常用配方
每天在每个账号上收一次
for account in alpha beta gamma; do
MERCY_SF_PASSWORD="$(pass mercy/$account)" \
mercy-cli --claim --user "$account" --character Hero || echo "failed: $account"
done让面板上的一块保持最新
MERCY_SF_PASSWORD='…' mercy-cli --status --user someone --character Hero --watch 60一次登录,一分钟一个对象。不要改成从 cron 每分钟重跑一次,那是每次都重新登录一遍。
把事件记进文件
MERCY_SF_PASSWORD='…' mercy-cli --start --user someone --character Hero --events \
>> events.ndjson登录之前先确认你在跟什么说话
api=$(mercy-cli --version | jq -r .api)
[ "$api" = "1" ] || { echo "unknown contract version: $api"; exit 1; }作为 systemd 服务
[Service]
Environment=MERCY_SF_PASSWORD=…
ExecStart=/usr/local/bin/mercy-cli --start --user someone --all --events
Restart=on-failure
KillSignal=SIGTERMSIGTERM 会让当前这一步走完。请不要对它用 SIGKILL。
目前还做不到的事
启动多个角色并逐个停止(--all 是全部启动、全部一起停止)、往运行中的进程里开一条 控制通道,以及修改模块开关之外的任何东西。
如果你需要其中某一项,说一声是哪一项;把对的东西造出来,比猜着造完又没人用要容易。