cito-mcp 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -4
- package/dist/client.js +50 -0
- package/dist/index.js +45 -6
- package/dist/instructions.js +1 -0
- package/dist/tools/insight.js +60 -8
- package/dist/tools/live.js +176 -25
- package/dist/tools/match.js +91 -33
- package/dist/tools/meta.js +1 -1
- package/dist/tools/normalize.js +70 -16
- package/dist/tools/player.js +54 -10
- package/dist/tools/resolve.js +149 -62
- package/dist/tools/standings.js +34 -1
- package/dist/tools/team.js +140 -54
- package/dist/version.js +29 -0
- package/package.json +1 -1
package/dist/tools/match.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* match_summary + match_details
|
|
3
3
|
*/
|
|
4
|
-
import { extractRows, fetchJson, gameNotIncludedHint, asRecord, pickString, } from '../client.js';
|
|
4
|
+
import { extractRows, fetchJson, gameNotIncludedHint, asRecord, pickString, unwrapPayload, } from '../client.js';
|
|
5
5
|
import { errorEnvelope, mapHttpToCode, newRequestId, partialFromRejection, successEnvelope, } from '../envelope.js';
|
|
6
6
|
import { normalizeMatch } from './normalize.js';
|
|
7
7
|
import { boolSchema, gameSchema, isPrimaryGame, parseGame, stringSchema, } from './types.js';
|
|
@@ -9,8 +9,14 @@ async function getSection(ctx, path, query) {
|
|
|
9
9
|
return fetchJson(ctx, path, { query });
|
|
10
10
|
}
|
|
11
11
|
function matchCore(game, matchId, raw) {
|
|
12
|
-
const
|
|
13
|
-
const
|
|
12
|
+
const entity = unwrapPayload(raw);
|
|
13
|
+
const m = normalizeMatch(game, entity);
|
|
14
|
+
const r = asRecord(entity) ?? {};
|
|
15
|
+
const s1 = m.team1?.score ?? null;
|
|
16
|
+
const s2 = m.team2?.score ?? null;
|
|
17
|
+
const scoreline = m.team1 || m.team2
|
|
18
|
+
? `${m.team1?.name ?? '?'} ${s1 ?? '-'} : ${s2 ?? '-'} ${m.team2?.name ?? '?'}`
|
|
19
|
+
: null;
|
|
14
20
|
return {
|
|
15
21
|
matchId: m.matchId !== 'unknown' ? m.matchId : matchId,
|
|
16
22
|
game,
|
|
@@ -18,14 +24,66 @@ function matchCore(game, matchId, raw) {
|
|
|
18
24
|
startTime: m.startTime,
|
|
19
25
|
endTime: pickString(r.endTime, r.endedAt, r.finishedAt) ?? null,
|
|
20
26
|
bestOf: r.bestOf ?? r.bo ?? null,
|
|
27
|
+
label: m.label,
|
|
28
|
+
scoreline,
|
|
21
29
|
score: {
|
|
22
|
-
team1:
|
|
23
|
-
team2:
|
|
30
|
+
team1: s1,
|
|
31
|
+
team2: s2,
|
|
24
32
|
},
|
|
25
33
|
team1: m.team1,
|
|
26
34
|
team2: m.team2,
|
|
27
35
|
event: m.event,
|
|
28
36
|
league: m.league,
|
|
37
|
+
method: pickString(r.method, r.resultMethod) ?? null,
|
|
38
|
+
winner: (() => {
|
|
39
|
+
// UFC rows carry an explicit winner slug/fighter ref.
|
|
40
|
+
const explicit = pickString(r.winnerFighterSlug, r.winnerSlug, r.winner);
|
|
41
|
+
if (explicit)
|
|
42
|
+
return explicit;
|
|
43
|
+
if (game === 'ufc')
|
|
44
|
+
return null;
|
|
45
|
+
// CS2 rows expose winnerTeamId instead; map it to a side, else fall back to scores.
|
|
46
|
+
const sideLabel = (side) => side ? pickString(side.slug, side.id, side.name) ?? null : null;
|
|
47
|
+
const winnerTeamId = pickString(r.winnerTeamId, r.winner_team_id);
|
|
48
|
+
if (winnerTeamId) {
|
|
49
|
+
const t1Ids = [m.team1?.id, m.team1?.slug, pickString(r.team1Id, r.team1_id)].filter(Boolean).map(String);
|
|
50
|
+
const t2Ids = [m.team2?.id, m.team2?.slug, pickString(r.team2Id, r.team2_id)].filter(Boolean).map(String);
|
|
51
|
+
if (t1Ids.includes(winnerTeamId))
|
|
52
|
+
return sideLabel(m.team1);
|
|
53
|
+
if (t2Ids.includes(winnerTeamId))
|
|
54
|
+
return sideLabel(m.team2);
|
|
55
|
+
}
|
|
56
|
+
if (s1 != null && s2 != null && s1 !== s2)
|
|
57
|
+
return sideLabel(s1 > s2 ? m.team1 : m.team2);
|
|
58
|
+
return null;
|
|
59
|
+
})(),
|
|
60
|
+
weightClass: pickString(r.weightClass, r.division) ?? null,
|
|
61
|
+
referee: (() => {
|
|
62
|
+
const nested = asRecord(r.referee);
|
|
63
|
+
if (nested) {
|
|
64
|
+
const joined = [nested.firstName, nested.lastName].filter(Boolean).join(' ').trim();
|
|
65
|
+
const name = pickString(nested.name, joined || undefined) ?? null;
|
|
66
|
+
const id = pickString(nested.id, nested.refereeId) ?? null;
|
|
67
|
+
if (name || id) {
|
|
68
|
+
return {
|
|
69
|
+
id,
|
|
70
|
+
name,
|
|
71
|
+
firstName: pickString(nested.firstName) ?? null,
|
|
72
|
+
lastName: pickString(nested.lastName) ?? null,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const name = pickString(r.refereeName) ?? null;
|
|
77
|
+
const id = pickString(r.refereeId) ?? null;
|
|
78
|
+
if (!name && !id)
|
|
79
|
+
return null;
|
|
80
|
+
return {
|
|
81
|
+
id,
|
|
82
|
+
name,
|
|
83
|
+
firstName: pickString(r.refereeFirstName) ?? null,
|
|
84
|
+
lastName: pickString(r.refereeLastName) ?? null,
|
|
85
|
+
};
|
|
86
|
+
})(),
|
|
29
87
|
rawStatus: pickString(r.status, r.state) ?? null,
|
|
30
88
|
};
|
|
31
89
|
}
|
|
@@ -45,18 +103,18 @@ function primaryPath(game, matchId) {
|
|
|
45
103
|
}
|
|
46
104
|
export const matchSummary = {
|
|
47
105
|
name: 'match_summary',
|
|
48
|
-
description: `COMPOSITE match card: scoreline, key context, player performances, and VOD/demo links when available.
|
|
49
|
-
|
|
50
|
-
When to use:
|
|
51
|
-
- Match recap / default match UI
|
|
52
|
-
- After user selects a live or completed matchId
|
|
53
|
-
|
|
54
|
-
Prefer over match_details for chat answers and default UIs.
|
|
55
|
-
Prefer match_details for timelines, full map trees, live state, advanced packages.
|
|
56
|
-
|
|
57
|
-
Do not use when: no matchId yet (resolve from live/schedule); pure pre-match → match_preview.
|
|
58
|
-
|
|
59
|
-
Parallel-safe: yes. Upstream cost: 2–5.
|
|
106
|
+
description: `COMPOSITE match card: scoreline, key context, player performances, and VOD/demo links when available.
|
|
107
|
+
|
|
108
|
+
When to use:
|
|
109
|
+
- Match recap / default match UI
|
|
110
|
+
- After user selects a live or completed matchId
|
|
111
|
+
|
|
112
|
+
Prefer over match_details for chat answers and default UIs.
|
|
113
|
+
Prefer match_details for timelines, full map trees, live state, advanced packages.
|
|
114
|
+
|
|
115
|
+
Do not use when: no matchId yet (resolve from live/schedule); pure pre-match → match_preview.
|
|
116
|
+
|
|
117
|
+
Parallel-safe: yes. Upstream cost: 2–5.
|
|
60
118
|
Example: { "game": "cs2", "matchId": "cs2-match-123", "view": "summary", "includePlayerStats": true }`,
|
|
61
119
|
inputSchema: {
|
|
62
120
|
type: 'object',
|
|
@@ -240,22 +298,22 @@ Example: { "game": "cs2", "matchId": "cs2-match-123", "view": "summary", "includ
|
|
|
240
298
|
};
|
|
241
299
|
export const matchDetails = {
|
|
242
300
|
name: 'match_details',
|
|
243
|
-
description: `Deep match package: optional timelines, advanced stats, live state/snapshots, full map/game tree, media inventory.
|
|
244
|
-
|
|
245
|
-
When to use:
|
|
246
|
-
- Analyst deep dive
|
|
247
|
-
- Live in-game window (LoL/CS2/UFC)
|
|
248
|
-
- Full demo list
|
|
249
|
-
|
|
250
|
-
Prefer over match_summary only when summary is insufficient.
|
|
251
|
-
Prefer match_summary for short answers and default cards.
|
|
252
|
-
|
|
253
|
-
Do not use when: first-pass live board (use live_matches + match_summary).
|
|
254
|
-
|
|
255
|
-
Section selection: pass includeTimeline / includeLiveState / includeAdvanced booleans, OR an explicit sections[] list.
|
|
256
|
-
If sections[] is non-empty it wins (booleans are ignored). LoL liveState/advanced require gameId.
|
|
257
|
-
|
|
258
|
-
Parallel-safe: yes. Upstream cost: 1–8 (section-gated).
|
|
301
|
+
description: `Deep match package: optional timelines, advanced stats, live state/snapshots, full map/game tree, media inventory.
|
|
302
|
+
|
|
303
|
+
When to use:
|
|
304
|
+
- Analyst deep dive
|
|
305
|
+
- Live in-game window (LoL/CS2/UFC)
|
|
306
|
+
- Full demo list
|
|
307
|
+
|
|
308
|
+
Prefer over match_summary only when summary is insufficient.
|
|
309
|
+
Prefer match_summary for short answers and default cards.
|
|
310
|
+
|
|
311
|
+
Do not use when: first-pass live board (use live_matches + match_summary).
|
|
312
|
+
|
|
313
|
+
Section selection: pass includeTimeline / includeLiveState / includeAdvanced booleans, OR an explicit sections[] list.
|
|
314
|
+
If sections[] is non-empty it wins (booleans are ignored). LoL liveState/advanced require gameId.
|
|
315
|
+
|
|
316
|
+
Parallel-safe: yes. Upstream cost: 1–8 (section-gated).
|
|
259
317
|
Example: { "game": "lol", "matchId": "lol-match-1", "includeTimeline": true, "includeLiveState": false }`,
|
|
260
318
|
inputSchema: {
|
|
261
319
|
type: 'object',
|
package/dist/tools/meta.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { extractRows, fetchJson, gameNotIncludedHint, present } from '../client.js';
|
|
5
5
|
import { errorEnvelope, mapHttpToCode, newRequestId, successEnvelope, } from '../envelope.js';
|
|
6
6
|
import { boolSchema, gameSchema, parseGame, PRIMARY_GAMES, stringSchema, } from './types.js';
|
|
7
|
-
|
|
7
|
+
import { PACKAGE_VERSION as CATALOG_VERSION } from '../version.js';
|
|
8
8
|
const TOOL_CATALOG = [
|
|
9
9
|
{
|
|
10
10
|
name: 'list_capabilities',
|
package/dist/tools/normalize.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cross-game row normalization for live/schedule boards and entity cards.
|
|
3
3
|
*/
|
|
4
|
-
import { asRecord, pickString } from '../client.js';
|
|
4
|
+
import { asRecord, pickString, unwrapPayload } from '../client.js';
|
|
5
5
|
function sideFrom(name, id, slug, score) {
|
|
6
6
|
if (!name && !id && !slug)
|
|
7
7
|
return null;
|
|
@@ -39,12 +39,25 @@ function scoreNum(v) {
|
|
|
39
39
|
return null;
|
|
40
40
|
}
|
|
41
41
|
export function normalizeMatch(game, row, forcedStatus) {
|
|
42
|
-
|
|
43
|
-
const
|
|
42
|
+
// Always peel { success, data } so UFC bout fighters[] / status are visible.
|
|
43
|
+
const r = asRecord(unwrapPayload(row)) ?? asRecord(row) ?? {};
|
|
44
|
+
const matchId = pickString(r.matchId, r.boutId, r.id, r.gameId, r.match_id, r.dataId, r.fightMetricId, r.ufcFightId) ?? 'unknown';
|
|
44
45
|
let team1 = nestedSide(r.team1) ??
|
|
45
46
|
sideFrom(pickString(r.team1Name, r.team_a_name, r.redName, r.fighter1Name, r.homeName), pickString(r.team1Id, r.team1_id, r.redId, r.fighter1Id), pickString(r.team1Slug, r.redSlug, r.fighter1Slug), scoreNum(r.team1Score ?? r.score1 ?? r.team1Maps ?? r.redScore));
|
|
46
47
|
let team2 = nestedSide(r.team2) ??
|
|
47
48
|
sideFrom(pickString(r.team2Name, r.team_b_name, r.blueName, r.fighter2Name, r.awayName), pickString(r.team2Id, r.team2_id, r.blueId, r.fighter2Id), pickString(r.team2Slug, r.blueSlug, r.fighter2Slug), scoreNum(r.team2Score ?? r.score2 ?? r.team2Maps ?? r.blueScore));
|
|
49
|
+
// CS2 rows nest team1/team2 objects that lack a score; a nested side must not
|
|
50
|
+
// shadow the flat score fields (team1Score/score1/team1Maps) with score:null.
|
|
51
|
+
if (team1 && team1.score == null) {
|
|
52
|
+
const s = scoreNum(r.team1Score ?? r.score1 ?? r.team1Maps ?? r.redScore);
|
|
53
|
+
if (s != null)
|
|
54
|
+
team1 = { ...team1, score: s };
|
|
55
|
+
}
|
|
56
|
+
if (team2 && team2.score == null) {
|
|
57
|
+
const s = scoreNum(r.team2Score ?? r.score2 ?? r.team2Maps ?? r.blueScore);
|
|
58
|
+
if (s != null)
|
|
59
|
+
team2 = { ...team2, score: s };
|
|
60
|
+
}
|
|
48
61
|
// UFC / live corners: red/blue objects, corner arrays, or fighters[] with corner field.
|
|
49
62
|
// Public API serializeBout uses fighters:[{ corner, fighterName, fighterSlug, profile:{name,slug} }].
|
|
50
63
|
// Live tracking uses red/blue + fighters[] (not team1/team2).
|
|
@@ -96,30 +109,66 @@ export function normalizeMatch(game, row, forcedStatus) {
|
|
|
96
109
|
team1 = team1 ?? nestedSide(r.teams[0]);
|
|
97
110
|
team2 = team2 ?? nestedSide(r.teams[1]);
|
|
98
111
|
}
|
|
99
|
-
const startTime = pickString(r.startTime, r.scheduledAt, r.startsAt, r.date, r.startDate, r.beginAt) ?? null;
|
|
100
|
-
const statusRaw = pickString(r.status, r.state, r.matchStatus)?.toLowerCase() ?? '';
|
|
112
|
+
const startTime = pickString(r.startTime, r.scheduledAt, r.startsAt, r.date, r.startDate, r.beginAt, asRecord(r.event)?.startsAt, asRecord(r.event)?.startTime, asRecord(r.event)?.date) ?? null;
|
|
113
|
+
const statusRaw = pickString(r.status, r.state, r.matchStatus, r.boutStatus)?.toLowerCase() ?? '';
|
|
101
114
|
let status = forcedStatus ?? 'unknown';
|
|
102
115
|
if (!forcedStatus) {
|
|
103
|
-
|
|
116
|
+
const hasResult = Boolean(pickString(r.winnerFighterSlug, r.winnerSlug, r.winner, r.method, r.result)) ||
|
|
117
|
+
r.resultRound != null ||
|
|
118
|
+
r.isComplete === true ||
|
|
119
|
+
r.completed === true;
|
|
120
|
+
if (/live|running|in_?progress|ongoing|started|watching/.test(statusRaw))
|
|
104
121
|
status = 'live';
|
|
105
|
-
else if (/
|
|
106
|
-
status = 'upcoming';
|
|
107
|
-
else if (/complete|finished|ended|final|closed|done/.test(statusRaw))
|
|
122
|
+
else if (/complete|finished|ended|final|closed|done|official|result/.test(statusRaw) || hasResult) {
|
|
108
123
|
status = 'completed';
|
|
109
|
-
|
|
110
|
-
|
|
124
|
+
}
|
|
125
|
+
else if (/upcoming|scheduled|not_started|pending|soon|booked|confirmed|announced/.test(statusRaw) ||
|
|
126
|
+
(game === 'ufc' && !hasResult && (startTime || statusRaw === '' || statusRaw === 'unknown'))) {
|
|
127
|
+
// UFC bouts often omit status or use sparse values; default upcoming when no result yet.
|
|
128
|
+
if (hasResult)
|
|
129
|
+
status = 'completed';
|
|
130
|
+
else if (startTime) {
|
|
131
|
+
const t = Date.parse(startTime);
|
|
132
|
+
status = Number.isFinite(t) && t < Date.now() - 3 * 3600_000 ? 'completed' : 'upcoming';
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
status = 'upcoming';
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
else if (hasResult)
|
|
139
|
+
status = 'completed';
|
|
140
|
+
}
|
|
141
|
+
// Winner → score 1-0 for UFC card display when numeric scores absent
|
|
142
|
+
if (game === 'ufc' && team1 && team2 && team1.score == null && team2.score == null) {
|
|
143
|
+
const winner = pickString(r.winnerFighterSlug, r.winnerSlug, r.winner)?.toLowerCase();
|
|
144
|
+
if (winner) {
|
|
145
|
+
const t1hit = [team1.slug, team1.id, team1.name].some((x) => x && (String(x).toLowerCase() === winner || String(x).toLowerCase().includes(winner) || winner.includes(String(x).toLowerCase())));
|
|
146
|
+
const t2hit = [team2.slug, team2.id, team2.name].some((x) => x && (String(x).toLowerCase() === winner || String(x).toLowerCase().includes(winner) || winner.includes(String(x).toLowerCase())));
|
|
147
|
+
if (t1hit && !t2hit) {
|
|
148
|
+
team1 = { ...team1, score: 1 };
|
|
149
|
+
team2 = { ...team2, score: 0 };
|
|
150
|
+
}
|
|
151
|
+
else if (t2hit && !t1hit) {
|
|
152
|
+
team1 = { ...team1, score: 0 };
|
|
153
|
+
team2 = { ...team2, score: 1 };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
111
156
|
}
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
const
|
|
157
|
+
const ev = asRecord(r.event);
|
|
158
|
+
const eventName = pickString(ev?.name, ev?.title, r.eventName, typeof r.event === 'string' ? r.event : undefined, asRecord(r.tournament)?.name, asRecord(r.tournament)?.title, r.tournamentName);
|
|
159
|
+
const eventId = pickString(ev?.id, r.eventId, asRecord(r.tournament)?.id, r.tournamentId);
|
|
160
|
+
const eventSlug = pickString(ev?.slug, r.eventSlug, asRecord(r.tournament)?.slug);
|
|
115
161
|
const leagueName = pickString(asRecord(r.league)?.name, r.leagueName, r.league, r.leagueSlug);
|
|
116
162
|
const leagueId = pickString(asRecord(r.league)?.id, r.leagueId);
|
|
117
163
|
const leagueSlug = pickString(asRecord(r.league)?.slug, r.leagueSlug);
|
|
118
164
|
// Prefer explicit title/label when present, but never keep the placeholder
|
|
119
165
|
// "? vs ?" once fighter/team names were resolved (UFC fighters[] / red-blue).
|
|
166
|
+
// When both sides lack names, prefer event / weight / bout id over opaque "? vs ?"
|
|
167
|
+
// so agent boards (live_matches, upcoming_schedule, event_card) stay legible.
|
|
120
168
|
const vsLabel = `${team1?.name ?? '?'} vs ${team2?.name ?? '?'}`;
|
|
121
169
|
const explicit = pickString(r.label, r.title, r.name);
|
|
122
170
|
const explicitIsPlaceholder = explicit != null && /^\?\s*vs\s*\?$/i.test(explicit.trim());
|
|
171
|
+
const weightOrClass = pickString(r.weightClass, r.division, r.weight_class, r.boutClass);
|
|
123
172
|
let label;
|
|
124
173
|
if (explicit && !explicitIsPlaceholder) {
|
|
125
174
|
label = explicit;
|
|
@@ -128,7 +177,11 @@ export function normalizeMatch(game, row, forcedStatus) {
|
|
|
128
177
|
label = vsLabel;
|
|
129
178
|
}
|
|
130
179
|
else {
|
|
131
|
-
|
|
180
|
+
// Identity fallbacks — never invent fighter names, but avoid bare "? vs ?"
|
|
181
|
+
label =
|
|
182
|
+
pickString(eventName, eventSlug, weightOrClass) ??
|
|
183
|
+
(matchId && matchId !== 'unknown' ? `Bout ${matchId}` : null) ??
|
|
184
|
+
vsLabel;
|
|
132
185
|
}
|
|
133
186
|
return {
|
|
134
187
|
game,
|
|
@@ -148,7 +201,8 @@ export function entityRef(row, type, game) {
|
|
|
148
201
|
pickString(r.slug) ??
|
|
149
202
|
'unknown';
|
|
150
203
|
const slug = pickString(r.slug, r.orgSlug, r.teamSlug);
|
|
151
|
-
|
|
204
|
+
// Events often use `title` not `name` (UFC)
|
|
205
|
+
const name = pickString(r.name, r.title, r.nickname, r.displayName, r.tag, r.code, slug, id) ?? id;
|
|
152
206
|
const nickname = pickString(r.nickname, asRecord(r.profile)?.nickname);
|
|
153
207
|
return {
|
|
154
208
|
game,
|
package/dist/tools/player.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* player_profile
|
|
3
3
|
*/
|
|
4
|
-
import { clampInt, extractRows, fetchJson, gameNotIncludedHint, asRecord, pickString, } from '../client.js';
|
|
4
|
+
import { clampInt, extractRows, fetchJson, gameNotIncludedHint, asRecord, pickString, unwrapPayload, } from '../client.js';
|
|
5
5
|
import { errorEnvelope, mapHttpToCode, newRequestId, partialFromRejection, successEnvelope, } from '../envelope.js';
|
|
6
6
|
import { normalizeMatch } from './normalize.js';
|
|
7
7
|
import { boolSchema, gameSchema, isPrimaryGame, limitSchema, parseGame, stringSchema, } from './types.js';
|
|
@@ -165,12 +165,7 @@ Example: { "game": "cs2", "playerId": "cs2-player-1", "recentLimit": 10, "includ
|
|
|
165
165
|
httpStatus: res.status,
|
|
166
166
|
});
|
|
167
167
|
}
|
|
168
|
-
playerRaw = res.data;
|
|
169
|
-
// some APIs wrap in data
|
|
170
|
-
const obj = asRecord(res.data);
|
|
171
|
-
if (obj?.data && typeof obj.data === 'object' && !Array.isArray(obj.data)) {
|
|
172
|
-
playerRaw = obj.data;
|
|
173
|
-
}
|
|
168
|
+
playerRaw = unwrapPayload(res.data);
|
|
174
169
|
}
|
|
175
170
|
const player = identityFrom(game, playerRaw, playerId || undefined, slug || undefined);
|
|
176
171
|
let currentTeam = player.team;
|
|
@@ -306,13 +301,15 @@ Example: { "game": "cs2", "playerId": "cs2-player-1", "recentLimit": 10, "includ
|
|
|
306
301
|
})());
|
|
307
302
|
}
|
|
308
303
|
if (game === 'ufc') {
|
|
304
|
+
const fighterKey = slug || idOrSlug;
|
|
309
305
|
tasks.push((async () => {
|
|
310
|
-
const res = await fetchJson(ctx, `/ufc/fighters/${encodeURIComponent(
|
|
306
|
+
const res = await fetchJson(ctx, `/ufc/fighters/${encodeURIComponent(fighterKey)}/stats`);
|
|
311
307
|
upstreamCalls += 1;
|
|
312
308
|
rateLimit = { ...rateLimit, ...res.headers };
|
|
313
309
|
if (res.ok) {
|
|
314
|
-
|
|
315
|
-
|
|
310
|
+
const metrics = unwrapPayload(res.data);
|
|
311
|
+
form = { summary: 'UFC fighter stats', trend: null, window: null, metrics };
|
|
312
|
+
seasonStats = metrics;
|
|
316
313
|
}
|
|
317
314
|
else {
|
|
318
315
|
partial.push(partialFromRejection('form', {
|
|
@@ -322,6 +319,53 @@ Example: { "game": "cs2", "playerId": "cs2-player-1", "recentLimit": 10, "includ
|
|
|
322
319
|
}));
|
|
323
320
|
}
|
|
324
321
|
})());
|
|
322
|
+
tasks.push((async () => {
|
|
323
|
+
// Fight-by-fight history — /fights and /history are aliases on the API
|
|
324
|
+
const res = await fetchJson(ctx, `/ufc/fighters/${encodeURIComponent(fighterKey)}/fights`, {
|
|
325
|
+
query: { limit: recentLimit },
|
|
326
|
+
});
|
|
327
|
+
upstreamCalls += 1;
|
|
328
|
+
rateLimit = { ...rateLimit, ...res.headers };
|
|
329
|
+
if (res.ok) {
|
|
330
|
+
recentMatches = extractRows(res.data)
|
|
331
|
+
.slice(0, recentLimit)
|
|
332
|
+
.map((row) => {
|
|
333
|
+
const r = asRecord(row) ?? {};
|
|
334
|
+
const bout = asRecord(r.bout) ?? row;
|
|
335
|
+
const m = normalizeMatch('ufc', bout, 'completed');
|
|
336
|
+
return {
|
|
337
|
+
...m,
|
|
338
|
+
result: pickString(r.result, r.outcome, asRecord(bout)?.result) ?? null,
|
|
339
|
+
opponent: pickString(r.opponentName, asRecord(r.opponent)?.name) ?? null,
|
|
340
|
+
method: pickString(asRecord(bout)?.method, r.method) ?? null,
|
|
341
|
+
};
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
// fallback history path
|
|
346
|
+
const hist = await fetchJson(ctx, `/ufc/fighters/${encodeURIComponent(fighterKey)}/history`, {
|
|
347
|
+
query: { limit: recentLimit },
|
|
348
|
+
});
|
|
349
|
+
upstreamCalls += 1;
|
|
350
|
+
rateLimit = { ...rateLimit, ...hist.headers };
|
|
351
|
+
if (hist.ok) {
|
|
352
|
+
recentMatches = extractRows(hist.data)
|
|
353
|
+
.slice(0, recentLimit)
|
|
354
|
+
.map((row) => {
|
|
355
|
+
const r = asRecord(row) ?? {};
|
|
356
|
+
const bout = asRecord(r.bout) ?? row;
|
|
357
|
+
return normalizeMatch('ufc', bout, 'completed');
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
partial.push(partialFromRejection('recentMatches', {
|
|
362
|
+
code: mapHttpToCode(res.status),
|
|
363
|
+
message: `fighter fights HTTP ${res.status}`,
|
|
364
|
+
httpStatus: res.status,
|
|
365
|
+
}));
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
})());
|
|
325
369
|
}
|
|
326
370
|
await Promise.all(tasks);
|
|
327
371
|
if (view === 'summary' && recentMatches.length > recentLimit) {
|