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,全部通过。
这个提交包含在:
+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);
|
||||
|
||||
在新工单中引用
屏蔽一个用户