《明日方舟:终末地》抽卡记录查询工具 —— 按卡池分组,算出每个 6 星实际花了多少抽。
纯 Go 实现,前端经 go:embed 内嵌,产物是单个可执行文件、无外部运行时依赖;
同一份代码同时打包 Windows exe 与 Android APK。
安装: 下载 endlog.exe 双击运行,或安装 endlog.apk
登录: 手机号 + 短信验证码(不接触密码)
拿到 endlog.exe 后双击即可。首次启动会在 exe 同目录生成 config.yaml,
并自动打开浏览器到 http://127.0.0.1:8787 。
配置与抽卡记录都存在 exe 旁边(config.yaml 和 data/),
所以整个目录可以随便挪、也可以放 U 盘带走 —— 但要连 data/ 一起搬,
只搬 exe 的话历史存档不会跟过去。
命令行参数:
endlog.exe -listen 127.0.0.1:9000 # 端口被占用时换一个
endlog.exe -no-open # 不自动打开浏览器
endlog.exe -config D:\my.yaml # 指定配置文件
endlog.exe -version # 打印版本号安装 endlog.apk,打开即用。界面与功能和 Windows 版完全一致 ——
应用内部就是把同一个 Go 服务跑起来,再用 WebView 显示它的页面。
- 仅支持 arm64-v8a(目前绝大多数安卓手机都是),Android 7.0 以上
- 数据存在应用私有目录,卸载即清空;换手机不会自动带走存档
- 两端的存档目前无法互相同步(想要的话得加导入/导出功能)
安卓上有一个必须处理的坑:Android 没有 /etc/resolv.conf,
Go 的纯 Go DNS 解析器找不到 DNS 服务器就会退回本机 53 端口,
于是每个请求都失败在 lookup xxx on [::1]:53: connection refused。
官方解法是开 cgo 用 Bionic 的 getaddrinfo,但那需要 NDK;
这里改由 Java 外壳从 ConnectivityManager 查出系统 DNS,用 -dns 传给 Go,
Go 再据此替换 net.DefaultResolver(见 internal/netdns)。
查不到系统 DNS 时会退回内置公共 DNS,也可以在 config.yaml 的 dns_servers 手填。
go run ./cmd/endlog # 直接运行
bash scripts/build-windows.sh 1.0.0 # → dist/windows/endlog.exe
bash scripts/build-android.sh 1.0.0 # → dist/android/endlog.apk两个脚本都会先跑 gofmt / go vet / go test,任一不过就中止。
Android 构建不走 Gradle,直接调 aapt2 / javac / d8 / zipalign / apksigner。
原因与两个踩过的坑记在 scripts/build-android.sh 的注释里:
build-tools 29 的 d8.bat / apksigner.bat 用了 JDK 9 就移除的 -Djava.ext.dirs,
必须绕过 .bat 直接调 jar;而那一版 d8 处理非静态内部类会 NPE,
所以 MainActivity.java 里的嵌套类必须全部是 static。
首次构建会在 build/ 下生成签名密钥。这个 keystore 要自己备份 ——
换密钥签名的 APK 无法覆盖安装,只能卸载重装(那会丢掉手机上的存档)。
手机号 + 短信验证码,仅此一种。全程不接触也不保存你的密码。
只有国服支持。国际服官方用邮箱密码 + 图形验证码,本程序未实现,
因此 region: global 目前无法登录(抽卡接口本身是按区域适配好的,缺的只是登录)。
登录要连续走四个接口才能换到抽卡凭据,因此会比一般应用慢几秒。
以下端点经两个开源实现交叉核对(bhaoo/endfield-gacha 的 TypeScript 实现与 BoxCatTeam/endfield-cat 的 Rust 实现),两者在域名、appCode 与字段名上完全一致。参数细节另参考 AixLnyt/skport-api-docs。
国服域名根为 hypergryph.com,国际服为 gryphline.com,结构相同。
| 步骤 | 端点 | 请求 | 得到 |
|---|---|---|---|
| 1 | POST as.<host>/user/auth/v2/token_by_phone_code |
{phone, code} |
账号 token |
| 2 | POST as.<host>/user/oauth2/v2/grant |
{type:1, appCode, token} |
OAuth token |
| 3 | GET binding-api-account-prod.<host>/account/binding/v1/binding_list |
?token=&appCode=endfield |
角色 uid |
| 4 | POST binding-api-account-prod.<host>/account/binding/v1/u8_token_by_uid |
{uid, token} |
u8_token |
appCode:国服 be36d44aa36bfb5b,国际服 3dacefa138426cfe。
凭 u8_token 访问 ef-webview.<host>,不需要 cred/salt 签名流程:
GET /api/record/char ?lang&token&server_id&pool_type[&seq_id]
GET /api/record/weapon/pool ?lang&token&server_id
GET /api/record/weapon ?lang&token&server_id&pool_id[&seq_id]
返回统一为 {"code":0,"msg":"","data":{"list":[...],"hasMore":bool}},
但武器池列表接口的 data 直接是数组。
翻页游标 seq_id 取上一页最后一条记录的 seqId。
角色卡池必须按 pool_type 逐个查询,共四种:
| pool_type | 卡池 |
|---|---|
E_CharacterGachaPoolType_Special |
特许寻访 |
E_CharacterGachaPoolType_Joint |
辉光庆典 |
E_CharacterGachaPoolType_Standard |
基础寻访 |
E_CharacterGachaPoolType_Beginner |
启程寻访 |
单条记录字段:seqId、gachaTs、rarity、isNew、isFree、poolId、poolName,
名称字段角色为 charName/charId、武器为 weaponName/weaponId。
gachaTs 与 rarity 在不同端点上会以数字或数字字符串两种形式出现,程序两种都接受。
server_id:国服 1,国际服 2。
上述端点已对国服实测确认可达:请求
GET ef-webview.hypergryph.com/api/record/char?lang=zh-cn&token=…&server_id=1&pool_type=E_CharacterGachaPoolType_Special
返回 HTTP 200 与 {"code":…,"msg":…,"data":…} 外壳,URL 拼装、参数名与响应结构均与实现一致。
cmd/endlog/ 入口
internal/config/ 配置加载 + 区域定义(域名、appCode、server_id)
internal/gacha/ Record 类型、去重合并、Provider 接口
├── endfield/ 官方接口实现(角色 4 池 + 武器池,seq_id 游标翻页)
├── mockprov/ 样例数据源(确定性生成,可反复同步而不产生重复)
└── remote/ 配置驱动的应急数据源 + JSON 路径取值
internal/hypergryph/ 账号体系:短信登录与 u8_token 认证链
internal/store/ 内存记录集 + JSON 快照(原子写入)
internal/server/ HTTP 路由、会话、接口处理
web/ 前端单页,go:embed 内嵌
- 增量水位按卡池分别记(
gacha.Watermarks)。这是最容易踩错的地方: 历史卡池早已结束,其记录整体都早于「全局最新时间」。若给所有卡池共用一个全局水位, 每个历史卡池都会在第一条记录上判定「已追上」而停止翻页, 结果是历史卡池的数据永远补不回来。水位键为类型:查询单元(角色用pool_type,武器用pool_id),由store.Watermarks()从已存记录算出。 - 完整重新同步:
POST /api/sync带{"full":true}会忽略全部水位, 把所有卡池从头拉一遍,用于补回因同步中断而缺失的数据。页面上对应「完整重新同步」按钮。 - 时间戳单位自适应:官方
gachaTs实测是毫秒,而文档与部分开源实现按秒。 写死任一单位都会算出公元五万年,而time.Time只能序列化[0,9999]年 —— 一条越界记录就能让整份快照连带 HTTP 响应一起失败。 故用gacha.NormalizeUnixTime按数值量级判断,并在入库前用gacha.PlausibleTime校验。 - 去重:用
seqId加类型前缀(char:123/weapon:456)作为主键, 避免角色与武器序号空间撞号;接口没给seqId时按时间+卡池+名称生成稳定键。 - 部分失败不全废:一次完整同步要访问 4 个角色池加若干武器池。单个池失败时, 已拿到的记录照常入库,失败信息作为警告显示,不会让整次同步白跑。
- 限速:翻页之间按
endfield.delay_ms(默认 700ms)间隔,并有max_pages上限。 - 数据源可换:上层只依赖
gacha.Provider接口。官方接口若有变动, 可切到provider: remote,用config.yaml里的声明式配置描述新接口,无需改代码重新编译。
config.yaml 每项都有注释。常改的几处:
region: "cn" # cn 国服 / global 国际服
provider: "endfield" # endfield 官方接口 / mock 样例数据 / remote 自定义
endfield:
include_weapon: true # 关掉可少翻很多页,同步更快
binding_index: 0 # 一个账号绑了多个角色时选第几个
delay_ms: 700 # 翻页间隔,别调太小不想登录先看效果的话,把 provider 改成 mock,任意手机号与验证码都能进,
会载入 320 条样例记录(含角色与武器),用于验证筛选、排序、持久化等功能。
程序出错时会把接口的原始返回一起报出来。常见情况:
- 访问凭据已失效(40100: Token is invalid) —— 退出登录后重新用短信验证码登录。 凭据有有效期,长时间不用会失效。
- 收不到短信验证码 —— 核对
auth.send_code_type。这是整套流程里唯一没有交叉验证过的 参数,抓「获取验证码」那条请求看 body 里type的数字,改到配置里。 - 该账号下没有查到终末地角色 —— 确认游戏里已创建角色,且登录的是同一个鹰角账号。
- 部分卡池未能拉取 —— 通常是限流。等一会儿再点同步,已同步的部分不会重复拉。
- 某些武器卡池一条记录都没有 —— 点「完整重新同步」。历史卡池的数据若因早先的 同步中断而缺失,普通增量同步不会主动回头补,完整重新同步会把所有卡池从头拉一遍。 同步结果下方会按卡池列出条数,可据此确认哪些池已经补齐。
- 只用短信验证码,程序不接触也不保存密码。
u8_token只存在服务端进程内存里,不写磁盘、不下发给浏览器。 浏览器只持有一个本地随机 session cookie。进程退出即失效。- 磁盘上只存抽卡记录快照,文件名用
区域-uid,不含手机号。 - 服务默认只监听
127.0.0.1,不要改成0.0.0.0。 - 这个工具只用于查询你自己账号的记录。请保持
delay_ms不要调得太小。
go test ./...覆盖:官方接口的字段映射与游标翻页(含数字/字符串两种时间戳形式)、部分失败降级、 增量同步的按卡池水位(含历史卡池不被新卡池挡住的回归测试)、去重合并的幂等性、 快照持久化与重启恢复、以及基于样例数据源的完整 HTTP 流程。
官方接口相关的测试通过改写出站 host 指向本地测试服务器, 既验证真实的 URL 拼装,又不会真的访问官方接口。
Copyright (C) 2026 Gordenghost
本程序是自由软件:你可以依据自由软件基金会发布的 GNU 通用公共许可证
(第 3 版或任何更新版本)的条款重新发布和/或修改它。
发布本程序的目的是希望它有用,但不附带任何担保;甚至不包含对
适销性或特定用途适用性的默示担保。详见 GNU 通用公共许可证。
- 本项目是第三方工具,与上海鹰角网络科技有限公司无任何关联,也未获其授权或认可。
- 它调用的是游戏内「寻访记录」页面所用的同一套接口,这些接口非官方公开文档, 端点与字段随时可能变动或失效。
- 只用于查询你自己账号的抽卡记录。请勿用于批量抓取他人数据,
并保持
endfield.delay_ms不要调得过小。 - 使用本工具的风险由你自行承担。