fix: 按实测响应修正 SimpCore API-V2 的字段名

接入 V2 密钥后实跑发现字段名与文档描述对不上,全部按真实响应修正:

- /players 返回的是对象 {online,max,players:[...]} 而不是裸数组,
  原解析会拿到空列表;且里面没有坐标与延迟,只有所在世界
- /dragon 的字段是 dragonAlive,不是 alive,也没有 health / phase
- /time 直接给算好的时刻 {world,tick,phase,time12,time24},
  原实现还在用 world_time/day_time/is_day 自己换算,全部取不到
- /players/{name}/equipment 的物品是 {type,key,amount,damage,maxDurability,unbreakable}
  且挂在 equipment 子对象下;damage 是已损耗值,原来会把「损耗 157」当成
  「剩余 157」显示,现在换算成 remainingDurability 并显示「剩余/上限」+ 百分比
- 429 限流时服务端带 retryAfterSeconds,现在提示里直接写出还要等多少秒
  (末影龙 1 次/分钟,实测提示「还需等待 16 秒」)

另外给 SimpApiV2Client 加了可注入的 fetchImpl,便于测试替换网络层。

实测(DiLiDaLaDaLa):在线 8 人、游戏时间 19:45 NIGHT、
下界合金剑 剩余 1992/2031、头盔 剩余 198/407。

测试 336 -> 343,全部通过。
这个提交包含在:
lyizai
2026-09-28 13:56:41 +08:00
父节点 b9cae1aac2
当前提交 71e78f06dc
共修改 6 个文件,包含 304 行新增和 97 行删除
+57 -54
查看文件
@@ -1123,6 +1123,41 @@ function onEnter(input, handler) {
});
}
/**
* 渲染装备表(API-V2 的 `equipment` 结构)。
*
* 服务端给的 `damage` 是**已损耗值**,所以这里显示「剩余 / 上限」,
* 并把耐久百分比做成颜色提示 —— 挂机时最关心的就是装备还剩多少。
*
* @param {HTMLElement} box
* @param {object} data `/api/simp/v2/equipment/:name` 的 data
*/
function renderEquipment(box, data) {
const slots = [
['主手', data.mainHand], ['副手', data.offHand], ['头盔', data.helmet],
['胸甲', data.chestplate], ['护腿', data.leggings], ['靴子', data.boots]
];
box.appendChild(simpTable([
['部位', (r) => r.slot],
['物品', (r) => r.name],
['数量', (r) => r.amount],
['耐久', (r) => r.durability],
['剩余', (r) => r.percent]
], slots.map(([slot, item]) => {
if (!item) return { slot, name: '空', amount: '-', durability: '-', percent: '-' };
const hasDurability = item.maxDurability > 0 && !item.unbreakable;
const remaining = hasDurability ? item.remainingDurability : -1;
const percent = hasDurability ? Math.round((remaining / item.maxDurability) * 100) : null;
return {
slot,
name: item.key || item.type,
amount: item.amount,
durability: hasDurability ? `${remaining} / ${item.maxDurability}` : (item.unbreakable ? '不可破坏' : '-'),
percent: percent === null ? '-' : `${percent}%`
};
}), null, [2, 3]));
}
// ---------- 概览 ----------
async function renderSimpOverview(body) {
@@ -1392,24 +1427,7 @@ async function renderSimpPlayers(body, force) {
wrap.appendChild(el('h3', null, '装备与手持(API-V2)'));
if (v2.ok && v2.data) {
const slots = [
['主手', v2.data.mainHand], ['副手', v2.data.offHand],
['头盔', v2.data.helmet], ['胸甲', v2.data.chestplate],
['护腿', v2.data.leggings], ['靴子', v2.data.boots]
];
wrap.appendChild(simpTable([
['部位', (r) => r.slot],
['物品', (r) => r.name],
['数量', (r) => r.count],
['耐久', (r) => r.durability]
], slots.map(([slot, item]) => ({
slot,
name: item ? (item.displayName || item.name) : '空',
count: item ? item.count : '-',
durability: item && item.durability >= 0
? `${item.durability}${item.maxDurability > 0 ? ' / ' + item.maxDurability : ''}`
: '-'
})), null, [2, 3]));
renderEquipment(wrap, v2.data);
} else {
wrap.appendChild(simpEmpty(v2.ok ? '没有装备数据' : v2.message));
}
@@ -1481,33 +1499,35 @@ async function renderSimpV2(body, force) {
return btn;
};
actions.appendChild(make('在线玩家', 'v2online', '/api/simp/v2/online', (box, rows) => {
box.appendChild(simpTable([
['玩家', (r) => r.name],
['世界', (r) => r.world],
['坐标', (r) => (Number.isFinite(r.x) ? `${r.x.toFixed(0)}, ${r.y.toFixed(0)}, ${r.z.toFixed(0)}` : '-')],
['延迟', (r) => (r.ping >= 0 ? r.ping + ' ms' : '-')],
['模式', (r) => r.gameMode]
], rows, null, [3]));
actions.appendChild(make('在线玩家', 'v2online', '/api/simp/v2/online', (box, data) => {
box.appendChild(simpStats([
['在线人数', `${data.online} / ${data.max || '?'}`],
['世界', data.players.length > 0 ? [...new Set(data.players.map((p) => p.world))].join(', ') : '-']
]));
if (data.players.length > 0) {
box.appendChild(simpTable([
['玩家', (r) => r.name],
['显示名', (r) => r.displayName],
['世界', (r) => r.world],
['UUID', (r) => r.uuid]
], data.players));
}
}));
actions.appendChild(make('游戏时间', 'v2time', '/api/simp/v2/time', (box, t) => {
const ticks = t.dayTime % 24000;
const hours = Math.floor(((ticks / 1000) + 6) % 24);
const minutes = Math.floor(((ticks % 1000) / 1000) * 60);
box.appendChild(simpStats([
['游戏内时刻', `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`],
['天数', t.day >= 0 ? t.day : '-'],
['白天', t.isDay === null ? '-' : (t.isDay ? '是' : '否')],
['world_time', t.worldTime]
['游戏内时刻', t.time24 || '-'],
['12 小时制', t.time12 || '-'],
['时段', t.phase || '-'],
['世界', t.world || '-'],
['tick', t.tick]
]));
}));
actions.appendChild(make('末影龙', 'v2dragon', '/api/simp/v2/dragon', (box, d) => {
box.appendChild(simpStats([
['存活', d.alive ? '是' : '否'],
['生命', Number.isFinite(d.health) ? d.health : '-'],
['阶段', d.phase || '-']
['所在世界', d.world || '-']
]));
box.appendChild(el('p', 'hint', '该接口限速 1 次/分钟,请勿频繁点击。'));
box.appendChild(el('p', 'hint', '该接口限速 1 次/分钟,超限会返回还需等待多少秒。'));
}));
body.appendChild(actions);
body.appendChild(el('p', 'hint', 'API-V2 是独立于 API 的另一套密钥体系(需在密钥中心单独申请),接口级配额很小,因此不做自动轮询。'));
@@ -1529,24 +1549,7 @@ async function renderSimpV2(body, force) {
const result = await simpFetch('v2equip:' + name, '/api/simp/v2/equipment/' + encodeURIComponent(name), force);
box.innerHTML = '';
if (!simpResult(box, result, '没有装备数据')) return;
const d = result.data;
const slots = [
['主手', d.mainHand], ['副手', d.offHand], ['头盔', d.helmet],
['胸甲', d.chestplate], ['护腿', d.leggings], ['靴子', d.boots]
];
box.appendChild(simpTable([
['部位', (r) => r.slot],
['物品', (r) => r.name],
['数量', (r) => r.count],
['耐久', (r) => r.durability]
], slots.map(([slot, item]) => ({
slot,
name: item ? (item.displayName || item.name) : '空',
count: item ? item.count : '-',
durability: item && item.durability >= 0
? `${item.durability}${item.maxDurability > 0 ? ' / ' + item.maxDurability : ''}`
: '-'
})), null, [2, 3]));
renderEquipment(box, result.data);
});
onEnter(equipInput, () => equipBtn.click());
equipRow.appendChild(equipInput);