cito-mcp 0.3.8 → 0.3.9
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/dist/tools/player.js +98 -7
- package/dist/tools/resolve.js +49 -3
- package/package.json +1 -1
package/dist/tools/player.js
CHANGED
|
@@ -9,9 +9,20 @@ function identityFrom(game, raw, idHint, slugHint) {
|
|
|
9
9
|
const r = asRecord(raw) ?? {};
|
|
10
10
|
const id = pickString(r.id, r.playerId, r.lolPlayerId, r.codPlayerId, idHint, slugHint) ?? 'unknown';
|
|
11
11
|
const slug = pickString(r.slug, slugHint);
|
|
12
|
-
|
|
12
|
+
// currentIgn FIRST for LoL. The row carries currentIgn "Faker" and realName
|
|
13
|
+
// "Sanghyeok Lee" but no `name`, so this fell through every key to the id and
|
|
14
|
+
// served a raw UUID as the player's display name.
|
|
15
|
+
const name = pickString(r.name, r.currentIgn, r.ign, r.full_name, r.nickname, r.displayName, r.tag, r.realName, slug, id) ?? id;
|
|
16
|
+
// currentTeam is a STRING on the LoL row ("T1"), not an object, so asRecord
|
|
17
|
+
// returned null and the profile reported a team of "?" while the slug sat
|
|
18
|
+
// right there in currentTeamSlug.
|
|
19
|
+
const currentTeamName = pickString(r.currentTeam);
|
|
20
|
+
const currentTeamSlug = pickString(r.currentTeamSlug, r.teamSlug, r.orgSlug);
|
|
13
21
|
const team = asRecord(r.team) ??
|
|
14
22
|
asRecord(r.currentTeam) ??
|
|
23
|
+
(currentTeamName || currentTeamSlug
|
|
24
|
+
? { name: currentTeamName ?? currentTeamSlug, slug: currentTeamSlug, id: currentTeamSlug }
|
|
25
|
+
: null) ??
|
|
15
26
|
(r.teamName || r.orgSlug
|
|
16
27
|
? { name: r.teamName, slug: r.orgSlug, id: r.teamId }
|
|
17
28
|
: null);
|
|
@@ -24,7 +35,7 @@ function identityFrom(game, raw, idHint, slugHint) {
|
|
|
24
35
|
? {
|
|
25
36
|
id: pickString(asRecord(team)?.id, asRecord(team)?.teamId),
|
|
26
37
|
slug: pickString(asRecord(team)?.slug, asRecord(team)?.orgSlug),
|
|
27
|
-
name: pickString(asRecord(team)?.name) ?? '?',
|
|
38
|
+
name: pickString(asRecord(team)?.name, asRecord(team)?.slug) ?? '?',
|
|
28
39
|
}
|
|
29
40
|
: null,
|
|
30
41
|
role: pickString(r.role, r.position) ?? null,
|
|
@@ -204,11 +215,30 @@ Example: { "game": "cs2", "playerId": "cs2-player-1", "recentLimit": 10, "includ
|
|
|
204
215
|
const rows = extractRows(res.data);
|
|
205
216
|
const first = asRecord(rows[0]);
|
|
206
217
|
if (first) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
218
|
+
// NEVER let team history overwrite the player row's own team.
|
|
219
|
+
// This endpoint returns showmatch and novelty orgs alongside the
|
|
220
|
+
// real one, all marked status "current" — for Faker the first row
|
|
221
|
+
// is literally "Faker", then "Captain Faker", then "KR with
|
|
222
|
+
// Influencers". Taking rows[0] reported the player as their own
|
|
223
|
+
// team. The player row's currentTeamSlug is the answer; history is
|
|
224
|
+
// only a fallback, and then only for a row that looks like a real
|
|
225
|
+
// club rather than the player's own name.
|
|
226
|
+
const rows2 = rows.map((row) => asRecord(row) ?? {});
|
|
227
|
+
const preferred = (player.team?.slug
|
|
228
|
+
? rows2.find((x) => pickString(x.orgSlug, x.slug) === player.team?.slug)
|
|
229
|
+
: null) ??
|
|
230
|
+
rows2.find((x) => x.status === 'current' && pickString(x.teamName, x.name) !== player.name) ??
|
|
231
|
+
first;
|
|
232
|
+
const histName = pickString(preferred.name, preferred.teamName, preferred.orgName);
|
|
233
|
+
const histSlug = pickString(preferred.slug, preferred.orgSlug, preferred.teamSlug);
|
|
234
|
+
const alreadyResolved = Boolean(currentTeam?.slug && currentTeam.name && currentTeam.name !== '?');
|
|
235
|
+
if (!alreadyResolved && (histName || histSlug)) {
|
|
236
|
+
currentTeam = {
|
|
237
|
+
id: pickString(preferred.id, preferred.teamId) ?? histSlug ?? undefined,
|
|
238
|
+
slug: histSlug ?? undefined,
|
|
239
|
+
name: histName ?? histSlug ?? '?',
|
|
240
|
+
};
|
|
241
|
+
}
|
|
212
242
|
}
|
|
213
243
|
}
|
|
214
244
|
else {
|
|
@@ -220,6 +250,67 @@ Example: { "game": "cs2", "playerId": "cs2-player-1", "recentLimit": 10, "includ
|
|
|
220
250
|
}
|
|
221
251
|
})());
|
|
222
252
|
}
|
|
253
|
+
if (game === 'lol') {
|
|
254
|
+
// /lol/players/{id}/matches exists and returns the player's real match
|
|
255
|
+
// index; only tennis was wired to its equivalent, so every LoL profile
|
|
256
|
+
// shipped recentMatches: [].
|
|
257
|
+
tasks.push((async () => {
|
|
258
|
+
const res = await fetchJson(ctx, `/lol/players/${encodeURIComponent(idOrSlug)}/matches`, {
|
|
259
|
+
query: { limit: String(recentLimit) },
|
|
260
|
+
});
|
|
261
|
+
upstreamCalls += 1;
|
|
262
|
+
rateLimit = { ...rateLimit, ...res.headers };
|
|
263
|
+
if (res.ok) {
|
|
264
|
+
const startMs = (row) => {
|
|
265
|
+
const r = asRecord(row) ?? {};
|
|
266
|
+
const t = Date.parse(String(r.startTime ?? r.date ?? ''));
|
|
267
|
+
return Number.isFinite(t) ? t : -Infinity;
|
|
268
|
+
};
|
|
269
|
+
const picked = extractRows(res.data)
|
|
270
|
+
.slice()
|
|
271
|
+
.sort((a, b) => startMs(b) - startMs(a))
|
|
272
|
+
.slice(0, recentLimit);
|
|
273
|
+
// The index carries team SLUGS and no names, so labels read
|
|
274
|
+
// "hle vs t1". Resolve the distinct slugs once — deduplicated and
|
|
275
|
+
// capped — rather than a lookup per match, and fall back to the
|
|
276
|
+
// slug when a lookup fails so a label is never blank.
|
|
277
|
+
const slugs = [
|
|
278
|
+
...new Set(picked.flatMap((row) => {
|
|
279
|
+
const r = asRecord(row) ?? {};
|
|
280
|
+
return [pickString(r.team1Slug), pickString(r.team2Slug)];
|
|
281
|
+
}).filter((x) => Boolean(x))),
|
|
282
|
+
].slice(0, 12);
|
|
283
|
+
const names = new Map();
|
|
284
|
+
await Promise.all(slugs.map(async (sl) => {
|
|
285
|
+
const t = await fetchJson(ctx, `/lol/teams/${encodeURIComponent(sl)}`);
|
|
286
|
+
upstreamCalls += 1;
|
|
287
|
+
if (!t.ok)
|
|
288
|
+
return;
|
|
289
|
+
const body = asRecord(t.data) ?? {};
|
|
290
|
+
const nm = pickString(body.name, asRecord(body.data)?.name);
|
|
291
|
+
if (nm)
|
|
292
|
+
names.set(sl, nm);
|
|
293
|
+
}));
|
|
294
|
+
recentMatches = picked.map((row) => {
|
|
295
|
+
const r = asRecord(row) ?? {};
|
|
296
|
+
const s1 = pickString(r.team1Slug);
|
|
297
|
+
const s2 = pickString(r.team2Slug);
|
|
298
|
+
return normalizeMatch('lol', {
|
|
299
|
+
...r,
|
|
300
|
+
...(s1 ? { team1: { slug: s1, name: names.get(s1) ?? s1, score: r.team1Score } } : {}),
|
|
301
|
+
...(s2 ? { team2: { slug: s2, name: names.get(s2) ?? s2, score: r.team2Score } } : {}),
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
partial.push(partialFromRejection('recentMatches', {
|
|
307
|
+
code: mapHttpToCode(res.status),
|
|
308
|
+
message: `player matches HTTP ${res.status}`,
|
|
309
|
+
httpStatus: res.status,
|
|
310
|
+
}));
|
|
311
|
+
}
|
|
312
|
+
})());
|
|
313
|
+
}
|
|
223
314
|
if (game === 'tennis') {
|
|
224
315
|
if (includeTrends) {
|
|
225
316
|
tasks.push((async () => {
|
package/dist/tools/resolve.js
CHANGED
|
@@ -5,6 +5,20 @@ import { clampInt, decodeCursor, encodeCursor, extractRows, fetchJson, gameNotIn
|
|
|
5
5
|
import { DEFAULT_PAGE_LIMIT, MAX_PAGE_LIMIT, errorEnvelope, mapHttpToCode, newRequestId, partialFromRejection, successEnvelope, } from '../envelope.js';
|
|
6
6
|
import { entityRef, rankScore } from './normalize.js';
|
|
7
7
|
import { boolSchema, gameSchema, isPrimaryGame, limitSchema, parseGame, PRIMARY_GAMES, stringSchema, } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Query aliases for orgs whose common name is not their slug. Kept deliberately
|
|
10
|
+
* small and one-directional: these map what a person types onto the canonical
|
|
11
|
+
* slug, and are only used to grant an exact-match boost, never to rewrite a
|
|
12
|
+
* result. SK Telecom T1 became T1 in 2019; sk-telecom-t1-k and -s remain
|
|
13
|
+
* separate historical rosters and are NOT folded in.
|
|
14
|
+
*/
|
|
15
|
+
const normalizeQueryKey = (v) => String(v || '').toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
16
|
+
const ORG_ALIASES = {
|
|
17
|
+
skt: 't1',
|
|
18
|
+
sktt1: 't1',
|
|
19
|
+
sktelecomt1: 't1',
|
|
20
|
+
skt1: 't1',
|
|
21
|
+
};
|
|
8
22
|
function pushCandidates(out, game, type, rows, q, limit) {
|
|
9
23
|
for (const row of rows) {
|
|
10
24
|
const ref = entityRef(row, type, game);
|
|
@@ -268,15 +282,33 @@ async function searchGame(ctx, game, q, type, limit) {
|
|
|
268
282
|
const tasks = [];
|
|
269
283
|
if (type === 'any' || type === 'team') {
|
|
270
284
|
tasks.push((async () => {
|
|
271
|
-
|
|
285
|
+
// Over-fetch before ranking. The upstream returns search hits in
|
|
286
|
+
// alphabetical order, so asking for `limit` rows put the exact
|
|
287
|
+
// match outside the window: "T1" returned mt1-esports, mt1-vision
|
|
288
|
+
// and two SK Telecom rosters while the real t1 sat sixth and was
|
|
289
|
+
// never seen. Rank a wide window, then cut.
|
|
290
|
+
const wide = String(Math.max(50, limit * 5));
|
|
291
|
+
const res = await fetchJson(ctx, '/lol/teams', { query: { search: q, limit: wide } });
|
|
272
292
|
calls += 1;
|
|
273
293
|
if (res.ok)
|
|
274
294
|
pushCandidates(candidates, game, 'team', extractRows(res.data), q, limit);
|
|
295
|
+
// An alias is a name the API does not know. "SKT" returns no t1 row
|
|
296
|
+
// at all, so ranking cannot help; search the canonical slug too and
|
|
297
|
+
// score those rows against the canonical name.
|
|
298
|
+
const aliasTarget = ORG_ALIASES[normalizeQueryKey(q)];
|
|
299
|
+
if (aliasTarget) {
|
|
300
|
+
const alt = await fetchJson(ctx, '/lol/teams', { query: { search: aliasTarget, limit: wide } });
|
|
301
|
+
calls += 1;
|
|
302
|
+
if (alt.ok)
|
|
303
|
+
pushCandidates(candidates, game, 'team', extractRows(alt.data), aliasTarget, limit);
|
|
304
|
+
}
|
|
275
305
|
})());
|
|
276
306
|
}
|
|
277
307
|
if (type === 'any' || type === 'player') {
|
|
278
308
|
tasks.push((async () => {
|
|
279
|
-
const res = await fetchJson(ctx, '/lol/players', {
|
|
309
|
+
const res = await fetchJson(ctx, '/lol/players', {
|
|
310
|
+
query: { search: q, limit: String(Math.max(50, limit * 5)) },
|
|
311
|
+
});
|
|
280
312
|
calls += 1;
|
|
281
313
|
if (res.ok)
|
|
282
314
|
pushCandidates(candidates, game, 'player', extractRows(res.data), q, limit);
|
|
@@ -444,8 +476,22 @@ Example: { "q": "T1", "game": "lol", "type": "team", "limit": 5 }`,
|
|
|
444
476
|
all.sort((a, b) => b.score - a.score);
|
|
445
477
|
const candidates = all.slice(0, limit);
|
|
446
478
|
const best = candidates[0] ?? null;
|
|
479
|
+
// An exact identity hit is not ambiguous, whatever else scored near it.
|
|
480
|
+
// "T1" ties with t1-rookies and t1-challengers on fuzzy score, which left
|
|
481
|
+
// the caller with needsDisambiguation and no decision to act on even though
|
|
482
|
+
// one candidate matched the query exactly. Aliases count: "SKT" resolves
|
|
483
|
+
// through ORG_ALIASES to the canonical org.
|
|
484
|
+
const qKey = normalizeQueryKey(q);
|
|
485
|
+
const aliasKey = ORG_ALIASES[qKey] ? normalizeQueryKey(ORG_ALIASES[qKey]) : null;
|
|
486
|
+
const bestIsExact = !!best &&
|
|
487
|
+
[best.slug, best.id, best.name].some((v) => {
|
|
488
|
+
const n = normalizeQueryKey(v);
|
|
489
|
+
return n && (n === qKey || (aliasKey !== null && n === aliasKey));
|
|
490
|
+
});
|
|
447
491
|
const needsDisambiguation = !best ||
|
|
448
|
-
(
|
|
492
|
+
(!bestIsExact &&
|
|
493
|
+
candidates.length > 1 &&
|
|
494
|
+
(candidates[1].score >= best.score - 5 || best.score < 50));
|
|
449
495
|
return successEnvelope({
|
|
450
496
|
source: 'resolve_entity',
|
|
451
497
|
game: best?.game ?? gameParse.game ?? null,
|
package/package.json
CHANGED