Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

arklog · 方舟寻访录

《明日方舟》抽卡记录查询工具,按卡池算出每个 6 星实际花了多少抽。 纯 Go,单文件可执行,Windows 与 Android 双端。

页面分成四栏横排(普通池 / 限定池 / 中坚池 / 全部抽卡记录), 每一栏内每期卡池各成一块、可折叠;每个 6 星是一行,点开能看到这一段里抽到的 5★ / 4★ / 3★。首次获得的干员标 new。支持按卡池、星级、名称筛选与排序。

前端内嵌进二进制,编译产物是单个可执行文件,无外部运行时依赖。

姊妹项目:endlog · 终末寻访录(《明日方舟:终末地》版)。 两者架构相同,但抽卡接口是完全不同的两套,数据模型也有实质差异,详见「设计要点」。

下载

Releases 取:

文件 平台
arklog.exe Windows 64 位,双击即用
arklog.apk Android 7.0+,仅 arm64-v8a

运行

Windows

拿到 arklog.exe双击即可。首次启动会在 exe 同目录生成 config.yaml, 并自动打开浏览器到 http://127.0.0.1:8788

配置与抽卡记录都存在 exe 旁边(config.yamldata/), 所以整个目录可以随便挪、也可以放 U 盘带走 —— 但要连 data/ 一起搬, 只搬 exe 的话历史存档不会跟过去。

命令行参数:

arklog.exe -listen 127.0.0.1:9000   # 端口被占用时换一个
arklog.exe -no-open                 # 不自动打开浏览器
arklog.exe -config D:\my.yaml       # 指定配置文件
arklog.exe -version                 # 打印版本号

Android

安装 arklog.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.yamldns_servers 手填。

从源码构建

go run ./cmd/arklog                    # 直接运行

bash scripts/build-windows.sh 1.0.0  # → dist/windows/arklog.exe
bash scripts/build-android.sh 1.0.0  # → dist/android/arklog.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 无法覆盖安装,只能卸载重装(那会丢掉手机上的存档)。

登录方式

手机号 + 短信验证码,仅此一种。全程不接触也不保存你的密码。

登录要连续走六个接口才能换到抽卡凭据,因此会比一般应用慢几秒。

只支持国服。国际服用 Yostar 账号体系(邮箱 + 图形验证码),与鹰角通行证完全无关, 而且官方并没有提供国际服的抽卡记录接口 —— 现有的国际服分析工具都是 手工导入/粘贴式的,不存在本程序所依赖的那个端点。

关于 B服:抽卡接口本身没有渠道参数,渠道信息来自 binding_list 返回的 每个绑定角色。因此只要 B服 角色确实绑在同一个鹰角账号下,就能通过 arknights.binding_index 选到它。但 B服 支持未经验证 —— B服 账号通常无法通过鹰角通行证短信登录。

接口说明

以下端点经交叉核对五个开源实现确认(AceDroidX/arknights-gacha-export 的 Rust 实现与其 API.mdRoLingG/Arknights_Gacha_App 的 Go 实现、bhaoo/ArknightsGachaExportbricks0411/astrbot_plugin_for_Arknightsqihang518887/astrbot_plugin_sklandv2 的 Python 实现), 并已用真实国服账号跑通全链路(登录 → 逐分类翻页 → 落盘),实测拉到数百条记录。

旧端点已废弃

ak.hypergryph.com/user/api/inquiry/gacha(那个一次返回整个十连、内含 chars 数组的接口) 已经不再是 API —— 它现在落到前端 SPA 的 catch-all 路由上,返回 HTTP 200 + <!doctype html>。 网上大量教程还在用它。程序识别到返回网页时会直接把这件事说出来,而不是只报「不是合法 JSON」。

现行接口是下面两个。

认证链

步骤 端点 请求 得到
1 POST as.<host>/general/v1/send_phone_code {phone, type} 下发短信
2 POST as.<host>/user/auth/v2/token_by_phone_code {phone, code} 账号 token
3 POST as.<host>/user/oauth2/v2/grant {token, appCode, type:1} OAuth token
4 GET binding-api-account-prod.<host>/account/binding/v1/binding_list ?token=&appCode=arknights uid(含渠道)
5 POST binding-api-account-prod.<host>/account/binding/v1/u8_token_by_uid {token, uid} u8_token
6 POST ak.<host>/user/api/role/login {token:<u8_token>, …} ak-user-center cookie

第 1~5 步与 endlog 几乎一致(只有 binding_listappCode 换成 arknights)—— 这是鹰角通用账号体系,与具体游戏无关。第 6 步是明日方舟多出来的一环: 终末地凭 u8_token 直接访问 ef-webview 即可,这里少了那个 cookie,抽卡接口会回 MissingCookie

appCode 最容易搞错:明日方舟用户中心是 be36d44aa36bfb5b,配 type:1,返回 data.token。 森空岛的 4ca99fa6b56cc2batype:0 返回的是 data.code(一次性授权码, 只能换森空岛的 cred),拿不到抽卡记录。程序检测到返回 code 而非 token 时会直接点明这一点。

寻访记录

x-role-token 头 + ak-user-center cookie 访问,不需要 cred/sign/salt 签名 (签名只有森空岛 zonai.skland.com 的接口才要):

GET ak.<host>/user/api/inquiry/gacha/cate     ?uid                              → data 是数组
GET ak.<host>/user/api/inquiry/gacha/history  ?uid&category&size[&pos&gachaTs]  → data.list + data.hasMore
  • 必须按 category 逐个查询/cate 给出分类列表(实测有 normalclassicanniver_festsummer_festMH_02 等),没有「查全部卡池」的用法。
  • 翻页是游标:首次不带 pos/gachaTs,随后取本页最后一条的 posgachaTs, 循环直到 hasMore 为 false。
  • 凭据走请求头,不走 query 参数,因此无需 URL 转义。

单条记录:

{"poolId":"","poolName":"","charId":"","charName":"",
 "rarity":2,"isNew":false,"gachaTs":"1770697079082","pos":0}

鉴权失败完全绕过 code 外壳,只有 {"message":"未登录","reason":"UN_LOGIN"} (缺 cookie 时是 reason:"MissingCookie")。这两种响应既没有 status 也没有 code, 朴素反序列化会得到 code=0 从而被误判成成功 —— 程序单独检查 reason

设计要点

以下几条是这个项目真正需要动脑的地方,不是把 endlog 改个名就能得到的。

一条记录 = 一抽,十连靠 pos 区分

现行接口一条 list 项就是一抽(旧接口才是整个十连含 chars 数组)。 一次十连表现为 10 条 gachaTs 完全相同、pos 为 0..9 的兄弟记录

接口不提供唯一记录 ID,去重键必须自己合成,而且 pos 不能省: 同一次十连里出现两个同名同星级的干员非常常见(3★ 尤其如此), 它们的时间戳、卡池、名称三者全部相同。若沿用 endlog 那种 「时间 + 卡池 + 名称」的键,这些记录会被判成同一条而丢掉 —— 悄悄少算抽数,并让保底统计算错。

键取 poolId:charId:gachaTs:pos(见 gacha.RecordID),配了专门的回归测试。

pos 还有第二个用途:给同一时刻的记录定出确定的先后顺序。 保底计数要数「上次出金之后又抽了几次」,批次内顺序若错了,结果最多偏差 9 抽。

增量水位按卡池分别记

这一点与 endlog 相同,而且原因也相同 —— 因为 history 要按 category 逐个查询,那个 bug 的结构条件在这里同样成立: 历史卡池早已结束,其记录整体都早于「全局最新时间」。若给所有卡池共用一个全局水位, 每个历史卡池都会在第一条记录上判定「已追上」而停止翻页, 结果是历史卡池的数据永远补不回来。水位键为 category, 由 store.Watermarks() 从已存记录算出。

停止翻页用严格「早于」Record.IsOlderThan):一次十连的多条记录时间戳相同, 把相等也判成旧记录会整批漏掉,由 Merge 按键去重兜底。

星级是 0 基

接口的 rarity 是 0 基:5 表示 6★、2 表示 3★。 转换(gacha.StarsFromRarity)只在数据源的映射边界做一次,用命名常量而非裸 +1 —— 这是这类项目最经典的 off-by-one 来源。

时间戳是毫秒的字符串

gachaTs 形如 "1770697079082":字符串、毫秒。用字符串接收再解析, 但仍走 gacha.NormalizeUnixTime 按数值量级判断单位,这是刻意的双重防御 —— 写死任一单位一旦接口改动就会算出公元五万年,而 time.Time 只能序列化 [0,9999] 年, 一条越界记录就能让整份快照连带 HTTP 响应一起失败。入库前另有 PlausibleTime 校验。

本地快照是唯一的长期存档

官方接口能回溯的历史有限(各实现都只靠 hasMore 收敛,未发现明确的页数上限, 但是否静默截断未经验证)。因此:

  • data/ 目录是你的长期存档。scripts/build-windows.sh 里那段 「只删自己产出的文件、绝不 rm -rfdata/」的处理就是为此。
  • store.Merge 只增不删:接口不再返回的旧记录会继续留在本地, 绝不会被「本次拉到的结果」覆盖掉(有专门的测试锁住这条)。
  • 「完整重新同步」只能补回接口还愿意给的部分,补不回超出窗口的数据

部分失败不全废

一次完整同步要访问若干卡池分类。单个分类失败时,已拿到的记录照常入库, 失败信息作为警告显示,不会让整次同步白跑(gacha.PartialError)。 同步结果按卡池列出条数,据此可以确认哪些卡池实际拉到了。

抽数结转:三条互不相通的链

「这个 6 星花了多少抽」不能只数当前卡池内的抽数 —— 抽数会跨期结转。 三类卡池的规则各不相同,这是出金分析里唯一需要小心的地方:

分类 判定(按接口的 category 结转
中坚池 classic 各期之间结转,但只和中坚池互通
普通池 normal 各期之间结转,共享一条链
限定池 其余一切(含认不出的分类) 每期归零,不结转

所以三条链彼此独立:在限定池垫的抽数不会带到普通池,反之也不会。 在一期卡池的第一个 6 星上,若实际消耗包含上一期垫的抽数,界面会标出 「上期结转 N 抽」。

认不出的 category 归限定池,这是刻意选的保守方向。 归限定只是不给它算跨期 结转(各卡池仍照常统计、照常显示);而归普通池会让它和普通池共用一条结转链, 凭空给普通池的出金添上不属于它的垫抽数 —— 宁可少算结转,不可无中生有。

这条规则是被真实数据纠正过的:实测账号里出现了一个 MH_02(联动池), 早先的判定规则(只认 fest/limit 为限定)没认出它,把它当成普通池, 于是联动池垫的抽数被错误地结转进了普通池。

实测出现过的 categorynormalclassicanniver_festsummer_festMH_02

分类与结转都在前端(web/app.jspoolClass / chainKeyOf / analyzePools), 与 endlog 的做法一致。代价是这部分没有单元测试,靠样例数据与真实数据实跑验证; 样例数据刻意给每个分类都安排了多期卡池,否则结转这条路径根本走不到。

总览统计放服务端算

顶部统计条的数值(总抽数、6★/5★ 个数与出现率、平均/最惨/最快出金间隔) 由 Go 侧算好下发。这些的边界条件不少:批次内顺序要按 pos、 最后一个 6★ 之后的抽数还没出金、不能算成一个「已完成间隔」、 样本量不足时不能给出误导性的百分比(10 抽出 1 金算成 10%,而实际综合概率约 2%)。 放在 internal/gacha/stats.go(纯函数)才能用单元测试锁住。

数据源可换

上层只依赖 gacha.Provider 接口。官方接口若有变动,可切到 provider: remote, 用 config.yaml 里的声明式配置描述新接口,无需改代码重新编译。 但要注意 remote 是通用实现、不认识 pos,同一十连里的重复干员会被去重掉, 所以它只适合应急。

目录结构

cmd/arklog/                入口
internal/config/         配置加载 + 区域定义(域名、appCode)
internal/gacha/          Record 类型、去重合并、Provider 接口
  ├── stats.go           抽数统计 / 保底计数(纯函数)
  ├── arknights/         官方接口实现(按 category 逐个查,pos+gachaTs 游标翻页)
  ├── mockprov/          样例数据源(确定性生成,可反复同步而不产生重复)
  └── remote/            配置驱动的应急数据源 + JSON 路径取值
internal/hypergryph/     账号体系:短信登录与六步认证链
internal/store/          内存记录集 + JSON 快照(原子写入)
internal/server/         HTTP 路由、会话、接口处理
internal/netdns/         安卓上的 DNS 兜底
web/                     前端单页,go:embed 内嵌

配置

config.yaml 每项都有注释。常改的几处:

provider: "arknights"   # arknights 官方接口 / mock 样例数据 / remote 自定义

arknights:
  binding_index: 0      # 一个账号绑了多个角色时选第几个(官服/B服)
  page_size: 50         # history 接口的 size
  delay_ms: 1000        # 翻页间隔,别调太小

不想登录先看效果的话,把 provider 改成 mock,任意手机号与验证码都能进, 会载入 300 条样例记录(含真实干员名、按十连成批、含同批重复干员), 用于验证筛选、排序、统计、持久化等功能。

排错

程序出错时会把接口的原始返回一起报出来。常见情况:

  • 接口返回的是网页而不是 JSON —— 打到了废弃的旧端点。检查 arknights.history_url 是否被改成了 /user/api/inquiry/gacha
  • 访问凭据已失效(UN_LOGIN) —— 退出登录后重新用短信验证码登录。 凭据有有效期,长时间不用会失效。
  • 缺少 ak-user-center cookie(MissingCookie) —— 登录链路第 6 步没走完,重新登录。
  • OAuth 授权返回的是一次性 code 而不是 token —— auth.app_code 填成了森空岛的 4ca99fa6b56cc2ba。留空即用正确的默认值。
  • 收不到短信验证码 —— 核对 auth.send_code_type。这个值只有单一来源佐证、 含义未确认,抓「获取验证码」那条请求看 body 里 type 的数字,改到配置里。
  • 该账号下没有查到明日方舟角色 —— 确认游戏里已创建干员,且登录的是同一个鹰角账号。 B服 账号通常无法通过鹰角通行证登录。
  • 部分卡池未能拉取 —— 通常是限流。等一会儿再点同步,已同步的部分不会重复拉。
  • 某些卡池一条记录都没有 —— 点「完整重新同步」。但超出官方保留窗口的数据补不回来。

关于账号安全

  • 只用短信验证码,程序不接触也不保存密码。
  • u8_tokenak-user-center cookie 只存在服务端进程内存里, 不写磁盘、不下发给浏览器(有测试专门断言响应里不含凭据)。 浏览器只持有一个本地随机 session cookie。进程退出即失效。
  • 磁盘上只存抽卡记录快照,文件名用 区域-uid,不含手机号。
  • 服务默认只监听 127.0.0.1,不要改成 0.0.0.0
  • 这个工具只用于查询你自己账号的记录。请保持 delay_ms 不要调得太小。

测试

go test ./...

覆盖:

  • 同一十连内重复干员不被去重(去重键含 pos,最关键的一条)
  • 星级 0 基归一(接口 rarity=5 必须是 6★)
  • 字符串毫秒时间戳解析,以及结果必须能 JSON 序列化
  • category 逐个查询、pos+gachaTs 游标翻页、游标不推进时及早停止
  • 历史卡池不被新卡池的水位挡住(按卡池水位的回归测试)
  • 增量同步时时间戳相等的记录不被丢弃
  • 部分失败降级为 PartialError,已拿到的记录照常入库
  • 鉴权失败(UN_LOGIN / MissingCookie)不被静默当成成功,且报错可指导操作
  • 打到废弃端点返回 HTML 时给出明确提示
  • 统计:保底计数(含十连内按 pos 定序)、出金间隔、样本量不足不输出百分比
  • 快照只增不删(接口不再返回的旧记录不丢)、重启恢复、重复同步 0 新增
  • 基于样例数据源的完整 HTTP 流程(登录 → 同步 → 记录 → 退出 401)、凭据不外泄

官方接口相关的测试通过改写出站 host 指向本地测试服务器, 既验证真实的 URL 拼装,又不会真的访问官方接口。

许可证

本项目采用 GNU General Public License v3.0,全文见 LICENSE

Copyright (C) 2026 Gordenghost

本程序是自由软件:你可以依据自由软件基金会发布的 GNU 通用公共许可证
(第 3 版,或你选择的任何更新版本)的条款,重新发布和/或修改它。

本程序的发布是希望它能有用,但不作任何担保;甚至不含对
适销性或特定用途适用性的默示担保。详见 GNU 通用公共许可证。

你应当已随本程序收到一份 GNU 通用公共许可证的副本。
若没有,请查阅 <https://www.gnu.org/licenses/>。

需要注意:仓库里的 pic.jpg(应用图标源图)不是本人作品,仅作个人使用, 不在上述 GPL 授权范围内。若你要二次分发,请自行替换成你有权使用的图片, 再跑一次 bash scripts/gen-icons.sh 重新生成图标。

免责声明

本项目是第三方工具,与上海鹰角网络科技有限公司(Hypergryph)没有任何隶属、 合作或背书关系。《明日方舟》及相关名称、素材的著作权归鹰角所有。

本工具只做一件事:用你自己的账号,读取你自己的抽卡记录。 它不修改任何游戏数据,不提供任何游戏内优势,也不代替你操作游戏。

  • 凭据只留在你本机的进程内存里,不上传到任何第三方服务器(本程序没有服务端)。
  • 请不要把 delay_ms 调得过小 —— 高频请求可能触发风控,后果自负。
  • 请勿用于查询他人账号。

使用本工具的风险由你自行承担。

About

方舟寻访录 — 《明日方舟》抽卡记录查询工具,按卡池算出每个 6 星实际花了多少抽。纯 Go,单文件可执行,Windows 与 Android 双端。

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages