基于 ElysiaJS + Bun 的技术研究项目,仅供学习交流使用。
- 本项目仅为技术研究用途,旨在探索 HTTP 签名认证、OAuth2 授权流程、Base64 数据解码等计算机技术原理。
- 本项目为学习研究用途,不用于任何商业目的。禁止将本项目用于盈利或商业服务。
- 本项目的所有代码均为独立编写,不包含任何第三方平台的源代码、密钥、证书或机密信息。
- 使用者须自行承担全部责任。下载、运行或修改本项目代码即表示您同意:
- 遵守相关法律法规及第三方平台的服务条款
- 自行获取必要的授权和许可
- 不会将本项目用于任何非法目的
- 不会利用本项目侵犯他人知识产权或隐私权
- 本项目作者不对使用者的行为承担任何责任,包括但不限于因使用本项目导致的账号封禁、数据丢失、法律纠纷等后果。
- 如任何第三方平台认为本项目侵犯了其权益,请联系项目作者,我们将积极配合处理。
- 文档中出现的数字 ID 均为随机生成的示例占位符,不指向任何真实用户或数据。
bun install
bun run dev服务默认监听 http://localhost:3000。
游戏数据来自 GameData/ArknightsGameData 目录中的 Kengxxiao/ArknightsGameData Git 子模块。首次克隆后请初始化子模块:
git submodule update --init --recursive更新游戏数据后,请在主仓库提交新的子模块版本:
git -C GameData/ArknightsGameData pull --ff-only
git add GameData/ArknightsGameData
git commit -m "chore: update ArknightsGameData"flowchart TB
subgraph Client[客户端]
Browser["浏览器 / curl"]
end
subgraph Server[skland-server :3000]
Elysia["Elysia HTTP Server"]
Login["/auth/login<br>/auth/qr<br>/auth/qr-sse"]
Player["/games/:game/player|card<br>/profile/:profileId"]
Status["/auth/status<br>/auth/logout"]
subgraph Core["核心模块"]
Crypto["lib/crypto<br>签名算法"]
Skland["lib/skland<br>登录链路"]
Auth["lib/auth<br>凭证/玩家数据"]
end
DB[("bun:sqlite<br>凭证缓存")]
end
subgraph Upstream[上游 API]
HG["OAuth2 授权服务器"]
Zonai["数据 API 服务器"]
end
Browser -->|HTTP| Elysia
Elysia --> Login
Elysia --> Player
Elysia --> Status
Login --> Skland
Player --> Auth
Auth --> Crypto
Auth --> DB
Skland --> HG
Auth --> Zonai
sequenceDiagram
participant User as 用户
participant Server as skland-server
participant HG as OAuth2 服务器
participant API as 数据 API 服务器
participant SQLite as bun:sqlite
User->>Server: GET /auth/qr-sse
Note over Server,HG: Step 1 — 生成扫码会话
Server->>HG: POST /gen_scan/login
HG-->>Server: scanId
Server-->>User: 显示二维码 (ASCII / PNG)
Note over Server,HG: Step 2 — 轮询扫码状态 (每 2s,最长 120s)
loop 直到扫码或超时
Server->>HG: GET /scan_status?scanId=xxx
HG-->>Server: 未扫码 → 继续等待
end
HG-->>Server: scanCode (扫码成功)
Note over Server,HG: Step 3 — scanCode → access_token
Server->>HG: POST /token_by_scan_code
HG-->>Server: access_token (24位)
Note over Server,HG: Step 4 — access_token → grant code
Server->>HG: POST /oauth2/v2/grant
HG-->>Server: grant code
Note over Server,API: Step 5 — grant code → CRED
Server->>API: POST /generate_cred_by_code
API-->>Server: cred + cred_token
Note over Server,API: Step 6 — 获取完整用户标识 (签名请求)
Server->>API: GET /user/teenager
API-->>Server: userId
Server->>SQLite: 保存凭证
Server-->>User: ✓ 登录成功
flowchart LR
subgraph 扫码阶段
Scan["scanId"] -->|轮询| ScanCode["scanCode"]
ScanCode -->|POST| Token["access_token<br>24位"]
end
subgraph 凭证交换
Token -->|grant_type: 0| Grant0["森空岛 grant code"]
Token -->|grant_type: 1| Grant1["官网通行证 token<br>+ hgId 信息"]
Grant0 --> Cred["cred + cred_token"]
end
subgraph 会话管理
Cred --> Sign["HMAC-SHA256 签名"]
Cred -->|cred_token 过期| Refresh["刷新 cred_token"]
Cred -->|cred 过期| ReAuth["通过 access_token 重新获取"]
end
flowchart TD
Start["signedGet() 发起签名请求"] --> Call["调用 API"]
Call --> Code{响应 code?}
Code -->|"code = 0"| OK["返回 data"]
Code -->|"code = 10000"| Expired["cred_token 失效"]
Code -->|"code = 10002"| CredFail["cred 失效"]
Code -->|其他| Throw["抛出异常"]
Expired --> Refresh["refreshToken(cred) → 新 cred_token"]
Refresh --> Save1["更新 SQLite"]
Save1 --> Retry1["重试请求"]
Retry1 --> OK
CredFail --> HasToken{"有 access_token?"}
HasToken -->|是| NewGrant["getGrantCode → getCred → 新 cred"]
HasToken -->|否| Rebind["提示用户重新扫码登录"]
NewGrant --> Save2["更新 SQLite"]
Save2 --> Retry2["重试请求"]
Retry2 --> OK
所有需要鉴权的 API 请求均需在 Header 中携带签名,算法如下:
flowchart LR
subgraph Input[输入参数]
C["cred<br>32位凭证"]
CT["credToken<br>HMAC 密钥"]
U["请求 URL"]
M["请求方法"]
end
subgraph Step[计算步骤]
direction TB
S1["1. 构造 header_ca<br>{ platform, timestamp, dId, vName }"]
S2["2. 提取 query_params<br>GET → URL query<br>POST → JSON body"]
S3["3. 拼接签名原文<br>path + query_params<br>+ timestamp<br>+ JSON(header_ca)"]
S4["4. 计算签名<br>HMAC-SHA256(credToken, secret)<br>→ MD5(hex)"]
end
subgraph Output[输出请求头]
O1["{ cred, sign, platform,<br>timestamp, dId, vName }"]
end
C --> S1
CT --> S4
U --> S2
M --> S2
S1 --> S3
S2 --> S3
S3 --> S4
S4 --> O1
C --> O1
注:
credToken仅用于本地计算 HMAC,不会出现在任何网络请求中。
以下 ID 均为示例占位符,不代表真实用户。
flowchart TB
subgraph Source[用户可见来源]
Profile["个人主页 URL<br>profile?id=xxx"]
App["App 账号设置页"]
end
subgraph IDs[身份标识]
HGID["hgId<br>鹰角通行证 ID<br>✗ 不可用于查询"]
ProfileID["Profile ID<br>个人主页展示 ID<br>✓ 可解析"]
UserID["userId<br>森空岛内部 ID<br>仅内部解析使用"]
GameUID["game UID<br>游戏角色账户 ID"]
ServerID["serverId<br>游戏区服 ID"]
end
subgraph API[解析接口]
Center["/user/center<br>ID 解析服务"]
Binding["/player/binding<br>角色绑定查询"]
Info["/player/info<br>玩家数据查询"]
end
App --> HGID
Profile --> ProfileID
ProfileID -->|✓| Center
Center -->|返回 userId| UserID
HGID -->|✗ 不支持| Fail["解析失败"]
UserID --> Binding
Binding --> GameUID
GameUID --> Info
Info -->|响应数据| Result["结构化玩家数据"]
| ID 类型 | 示例(纯虚构) | 位数 | 说明 |
|---|---|---|---|
| hgId | 1111111111111 |
13 | 鹰角通行证 ID |
| Profile ID | 2222222222222 |
变长 | 个人主页展示 ID |
| userId | 42 |
变长 | 森空岛内部 ID,不作为公开查询参数 |
| game UID | 333333333 |
变长 | 服务端根据绑定信息解析的游戏角色账户 ID |
| serverId | 1 |
变长 | 服务端根据绑定信息解析的游戏区服 ID |
flowchart TD
Profile["Profile 路由<br>只传 profileId"] --> Center["user/center<br>解析内部 userId"]
Center --> Binding["player/binding<br>解析 Game UID 与 serverId"]
Binding --> Result["获取玩家数据"]
Profile 路由用于查询公开个人主页,不要求调用方提供区服;服务端会自行解析绑定的游戏角色与区服。
| 方法 | 路径 | 说明 |
|---|---|---|
GET |
/ |
列出所有接口 |
GET |
/auth/login |
扫码登录入口(返回 JSON 含 base64 二维码) |
GET |
/auth/qr |
直接返回二维码 PNG 图片 |
GET |
/auth/qr-sse |
纯文本流:终端 ASCII 二维码 + 实时状态 |
GET |
/auth/status |
查看登录状态 |
POST |
/auth/logout |
退出登录 |
GET |
/games/arknights/player/profile/:profileId |
通过 Profile ID 查询明日方舟玩家数据 |
GET |
/games/arknights/player/profile/:profileId/simple |
通过 Profile ID 查询明日方舟玩家摘要 |
GET |
/games/arknights/player/profile/:profileId/raw |
通过 Profile ID 查询明日方舟原始数据 |
GET |
/games/endfield/card/profile/:profileId |
通过 Profile ID 查询终末地角色卡片 |
以上明日方舟路由使用 GameData/ArknightsGameData/zh_CN/gamedata/excel 中的客户端数据,将角色、技能、物品、皮肤、关卡、区域、模组、基建技能和蚀刻章 ID 转换为包含 id、name、type 等字段的可读对象。无法可靠识别的值保持原样,原始响应可通过带 /raw 后缀的对应路由获取。
带 /simple 后缀的明日方舟路由仅返回玩家名称与等级、加入时间、主线进度、干员/时装/家具/蚀刻章数量,以及最多三位助战干员的等级、精英阶段、潜能、技能、模组和皮肤名称。
终末地角色卡片接口会保留上游 detail 的完整结构,可通过以下方式请求:
curl 'http://localhost:3000/games/endfield/card/profile/2222222222222'
上游字段使用内部命名,不能直接按英文键名翻译。项目中的字段语义按游戏内正式中文术语标注:
| 上游字段 | 游戏内含义 |
|---|---|
base |
角色基础信息 |
chars |
个人名片展示干员及养成信息 |
achieve |
「光荣之路」蚀刻章 |
spaceShip |
「O.M.V.帝江号」舱室与派驻干员 |
domain |
地区建设与地区探索 |
dungeon |
理智、理智上限及恢复时间,并非“地牢”玩法 |
bpSystem |
「协议通行证」等级 |
dailyMission / weeklyMission |
每日任务活跃度 / 每周任务进度 |
indieHard |
「影拓丰碑」挑战进度 |
seekSuspicion |
「蚀像寻遗」行动刻度 |
crisisContract |
「危机合约」活动进度 |
curl -N http://localhost:3000/auth/qr-sse| 字段 | 类型 | 说明 |
|---|---|---|
id |
INTEGER PK | 自增主键 |
access_token |
TEXT | 扫码获得的临时令牌 |
cred |
TEXT | 长期登录凭证 |
cred_token |
TEXT | 签名密钥(不对外暴露) |
user_id |
TEXT | 平台用户标识 |
created_at |
INTEGER | 创建时间 |
updated_at |
INTEGER | 更新时间 |
- 单行设计:始终只存一条管理员凭证
- SQLite:
skland.db,journal_mode = MEMORY - ORM:Drizzle ORM +
bun:sqlite
skland-server/
├── GameData/
│ └── ArknightsGameData/ # Kengxxiao/ArknightsGameData 子模块,提供 ID 字典
├── index.ts # Elysia 入口
├── drizzle.config.ts # Drizzle Kit 配置
├── package.json
├── skland.db # SQLite 凭证缓存
└── src/
├── db/
│ ├── schema.ts # 表定义
│ └── index.ts # 数据库连接
├── hg/ # Hypergryph 平台与游戏业务域
│ ├── auth.ts # 森空岛凭证管理
│ ├── crypto.ts # HMAC-SHA256 + MD5 签名算法
│ ├── skland.ts # 登录链路与平台 API
│ ├── skland-client.ts # 签名请求与凭证刷新
│ ├── routes/ # 平台路由与游戏路由装配
│ └── games/arknights/ # 明日方舟玩家服务、数据字典与路由
└── lib/
└── storage.ts # 通用凭证文件存储
| 接口 | 方法 | 说明 |
|---|---|---|
/gen_scan/login |
POST | 生成扫码会话 |
/scan_status |
GET | 轮询扫码状态 |
/token_by_scan_code |
POST | scanCode → access_token |
/oauth2/v2/grant |
POST | access_token → grant code / hgId |
| 接口 | 鉴权 | 说明 |
|---|---|---|
/generate_cred_by_code |
无需签名 | grant code → CRED |
/auth/refresh |
无需签名 | 刷新 cred_token |
/user/teenager |
签名 | 获取当前用户标识 |
/user/center |
签名 | Profile ID → 森空岛内部 userId |
/player/binding |
签名 | userId → 游戏角色绑定列表 |
/player/info |
签名 | 已绑定游戏角色 → 玩家完整数据(Base64) |
/endfield/card/detail |
签名 | 终末地角色完整卡片数据 |
- hgId 不可用于查询:通行证账户 ID 与数据平台的用户标识体系之间无公开的转换接口
- 仅支持 Profile ID 查询:服务端会自动解析内部 userId 和游戏角色绑定信息
本项目代码仅供学习交流使用。使用者须自行评估法律风险并承担全部责任。