fix: 修正 QQBot FFLogs 国服查询
This commit is contained in:
parent
a85db5e045
commit
b1249517e8
@ -84,10 +84,10 @@ FF14_UNIVERSALIS_BASE_URL=https://universalis.app/api/v2
|
||||
FF14_DEFAULT_WORLD=中国
|
||||
FF14_MARKET_CACHE_TTL_MS=60000
|
||||
|
||||
FFLOGS_BASE_URL=https://www.fflogs.com
|
||||
FFLOGS_BASE_URL=https://cn.fflogs.com
|
||||
FFLOGS_WEB_BASE_URL=https://cn.fflogs.com
|
||||
FFLOGS_GRAPHQL_URL=https://www.fflogs.com/api/v2/client
|
||||
FFLOGS_TOKEN_URL=https://www.fflogs.com/oauth/token
|
||||
FFLOGS_GRAPHQL_URL=https://cn.fflogs.com/api/v2/client
|
||||
FFLOGS_TOKEN_URL=https://cn.fflogs.com/oauth/token
|
||||
FFLOGS_CLIENT_ID=
|
||||
FFLOGS_CLIENT_SECRET=
|
||||
FFLOGS_DEFAULT_SERVER_REGION=CN
|
||||
|
||||
@ -16,10 +16,10 @@ ENV LOKI_BATCH_INTERVAL_SECONDS=5
|
||||
ENV LOKI_BATCH_MAX_BUFFER_SIZE=10000
|
||||
ENV LOKI_QUERY_MAX_LIMIT=1000
|
||||
ENV LOKI_SILENCE_ERRORS=true
|
||||
ENV FFLOGS_BASE_URL=https://www.fflogs.com
|
||||
ENV FFLOGS_BASE_URL=https://cn.fflogs.com
|
||||
ENV FFLOGS_WEB_BASE_URL=https://cn.fflogs.com
|
||||
ENV FFLOGS_GRAPHQL_URL=https://www.fflogs.com/api/v2/client
|
||||
ENV FFLOGS_TOKEN_URL=https://www.fflogs.com/oauth/token
|
||||
ENV FFLOGS_GRAPHQL_URL=https://cn.fflogs.com/api/v2/client
|
||||
ENV FFLOGS_TOKEN_URL=https://cn.fflogs.com/oauth/token
|
||||
ENV FFLOGS_DEFAULT_SERVER_REGION=CN
|
||||
ENV FFLOGS_REQUEST_TIMEOUT_MS=10000
|
||||
|
||||
|
||||
@ -545,11 +545,6 @@ ON DUPLICATE KEY UPDATE
|
||||
|
||||
INSERT INTO `admin_dict` (`id`, `dict_code`, `label`, `value`, `children_code`, `sort`, `status`)
|
||||
VALUES
|
||||
(2041700000000300601, 'FFLOGS_ENCOUNTER_LABEL', 'M9S Vamp Fatale', 'vampfatale', NULL, 1, 1),
|
||||
(2041700000000300602, 'FFLOGS_ENCOUNTER_LABEL', 'M10S Red Hot and Deep Blue', 'redhotanddeepblue', NULL, 2, 1),
|
||||
(2041700000000300603, 'FFLOGS_ENCOUNTER_LABEL', 'M11S The Tyrant', 'thetyrant', NULL, 3, 1),
|
||||
(2041700000000300604, 'FFLOGS_ENCOUNTER_LABEL', 'M12S P1 Lindwurm', 'lindwurm', NULL, 4, 1),
|
||||
(2041700000000300605, 'FFLOGS_ENCOUNTER_LABEL', 'M12S P2 Lindwurm II', 'lindwurmii', NULL, 5, 1),
|
||||
(2041700000000300701, 'FFLOGS_JOB_LABEL', '骑士', 'paladin', NULL, 1, 1),
|
||||
(2041700000000300702, 'FFLOGS_JOB_LABEL', '战士', 'warrior', NULL, 2, 1),
|
||||
(2041700000000300703, 'FFLOGS_JOB_LABEL', '暗黑骑士', 'darkknight', NULL, 3, 1),
|
||||
|
||||
@ -15,8 +15,6 @@ import {
|
||||
import type { QqbotFf14MarketCatalog } from '../plugins/ff14Market/qqbot-ff14-market.types';
|
||||
import type { QqbotCommandMatchResult } from '../qqbot.types';
|
||||
|
||||
const QQBOT_FFLOGS_ENCOUNTER_DICT_CODE = 'FFLOGS_ENCOUNTER_LABEL';
|
||||
|
||||
@Injectable()
|
||||
export class QqbotCommandParserService {
|
||||
constructor(private readonly dictService: DictService) {}
|
||||
@ -208,9 +206,8 @@ export class QqbotCommandParserService {
|
||||
}
|
||||
|
||||
if (!encounterName && remainingPositionals.length > 2) {
|
||||
const picked = await this.pickFflogsPositionalsByKnownWorld(
|
||||
remainingPositionals,
|
||||
);
|
||||
const picked =
|
||||
await this.pickFflogsPositionalsByKnownWorld(remainingPositionals);
|
||||
if (picked) {
|
||||
characterName = characterName || picked.characterName;
|
||||
serverSlug = serverSlug || picked.serverSlug;
|
||||
@ -219,16 +216,6 @@ export class QqbotCommandParserService {
|
||||
}
|
||||
}
|
||||
|
||||
if (!encounterName && remainingPositionals.length > 1) {
|
||||
const picked = await this.pickTrailingFflogsEncounter(
|
||||
remainingPositionals,
|
||||
);
|
||||
if (picked) {
|
||||
encounterName = picked.encounterName;
|
||||
remainingPositionals = picked.positionals;
|
||||
}
|
||||
}
|
||||
|
||||
if (!characterName && remainingPositionals.length) {
|
||||
const joined = remainingPositionals.join(' ');
|
||||
if (joined.includes('@')) {
|
||||
@ -350,44 +337,6 @@ export class QqbotCommandParserService {
|
||||
return null;
|
||||
}
|
||||
|
||||
private async pickTrailingFflogsEncounter(positional: string[]) {
|
||||
const catalog = await this.getFflogsEncounterCatalog();
|
||||
for (let index = 1; index < positional.length; index++) {
|
||||
const encounterName = positional.slice(index).join(' ').trim();
|
||||
if (!this.isFflogsEncounterName(catalog, encounterName)) continue;
|
||||
return {
|
||||
encounterName,
|
||||
positionals: positional.slice(0, index),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async getFflogsEncounterCatalog() {
|
||||
const dicts = await this.dictService.getDictItemsByKey(
|
||||
QQBOT_FFLOGS_ENCOUNTER_DICT_CODE,
|
||||
);
|
||||
const keys = new Set<string>();
|
||||
for (const item of dicts) {
|
||||
for (const key of this.buildFflogsLookupKeys(item.label, item.value)) {
|
||||
keys.add(key);
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
private isFflogsEncounterName(catalog: Set<string>, value: string) {
|
||||
const keys = this.buildFflogsLookupKeys(value);
|
||||
return keys.some(
|
||||
(key) =>
|
||||
catalog.has(key) ||
|
||||
(key.length >= 3 &&
|
||||
[...catalog].some(
|
||||
(candidate) => candidate.startsWith(key) || candidate.includes(key),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
private async getFf14MarketCatalog() {
|
||||
const treeCatalog = buildQqbotFf14MarketCatalogFromTree(
|
||||
await this.dictService.relationTree({
|
||||
@ -441,22 +390,4 @@ export class QqbotCommandParserService {
|
||||
if (value === true) return '';
|
||||
return `${value || ''}`.trim();
|
||||
}
|
||||
|
||||
private buildFflogsLookupKeys(...values: string[]) {
|
||||
const keys = values
|
||||
.flatMap((value) => {
|
||||
const normalized = this.normalizeFflogsLookupKey(value);
|
||||
const withoutAnd = normalized.replace(/and/g, '');
|
||||
return [normalized, withoutAnd];
|
||||
})
|
||||
.filter(Boolean);
|
||||
return [...new Set(keys)];
|
||||
}
|
||||
|
||||
private normalizeFflogsLookupKey(value: string) {
|
||||
return `${value || ''}`
|
||||
.normalize('NFKC')
|
||||
.toLowerCase()
|
||||
.replace(/[^\p{L}\p{N}]+/gu, '');
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import * as http from 'node:http';
|
||||
import * as https from 'node:https';
|
||||
import { DictService } from '../../../admin/dict/dict.service';
|
||||
import type {
|
||||
FflogsCharacterEncounterRankingsResponse,
|
||||
FflogsCharacterSummaryResponse,
|
||||
FflogsEncounterFightCandidate,
|
||||
FflogsEncounterLookup,
|
||||
@ -22,13 +23,20 @@ import type {
|
||||
} from './qqbot-fflogs.types';
|
||||
|
||||
const FFLOGS_LOCALIZATION_DICT_CODES = {
|
||||
encounter: 'FFLOGS_ENCOUNTER_LABEL',
|
||||
job: 'FFLOGS_JOB_LABEL',
|
||||
metric: 'FFLOGS_METRIC_LABEL',
|
||||
role: 'FFLOGS_ROLE_LABEL',
|
||||
serverRegion: 'FFLOGS_SERVER_REGION_LABEL',
|
||||
};
|
||||
|
||||
type FflogsEncounterCatalogItem = {
|
||||
displayName: string;
|
||||
encounterId: number;
|
||||
keys: string[];
|
||||
zoneId?: number;
|
||||
zoneName?: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class QqbotFflogsClientService {
|
||||
private accessToken = '';
|
||||
@ -36,6 +44,10 @@ export class QqbotFflogsClientService {
|
||||
private readonly baseUrl: string;
|
||||
private readonly clientId: string;
|
||||
private readonly clientSecret: string;
|
||||
private encounterCatalogCache?: {
|
||||
entries: FflogsEncounterCatalogItem[];
|
||||
expiresAt: number;
|
||||
};
|
||||
private readonly graphqlUrl: string;
|
||||
private readonly tokenUrl: string;
|
||||
private readonly webBaseUrl: string;
|
||||
@ -44,12 +56,14 @@ export class QqbotFflogsClientService {
|
||||
private readonly configService: ConfigService,
|
||||
private readonly dictService: DictService,
|
||||
) {
|
||||
this.baseUrl = this.normalizeBaseUrl(
|
||||
this.configService.get<string>('FFLOGS_BASE_URL') ||
|
||||
'https://www.fflogs.com',
|
||||
);
|
||||
this.webBaseUrl = this.normalizeBaseUrl(
|
||||
this.configService.get<string>('FFLOGS_WEB_BASE_URL') ||
|
||||
this.configService.get<string>('FFLOGS_BASE_URL') ||
|
||||
'https://cn.fflogs.com',
|
||||
);
|
||||
this.baseUrl = this.normalizeBaseUrl(
|
||||
this.configService.get<string>('FFLOGS_BASE_URL') ||
|
||||
this.webBaseUrl ||
|
||||
'https://cn.fflogs.com',
|
||||
);
|
||||
this.graphqlUrl =
|
||||
@ -250,6 +264,7 @@ export class QqbotFflogsClientService {
|
||||
name
|
||||
slug
|
||||
}
|
||||
zoneRankings(metric: dps)
|
||||
recentReports(limit: $reportsLimit) {
|
||||
data {
|
||||
code
|
||||
@ -291,21 +306,58 @@ export class QqbotFflogsClientService {
|
||||
character.server?.slug || serverSlug,
|
||||
character.name || characterName,
|
||||
);
|
||||
const replyUrl = this.buildCharacterEncounterUrl(
|
||||
url,
|
||||
encounterLookup,
|
||||
params.partition,
|
||||
);
|
||||
const difficulty = this.toOptionalNumber(params.difficulty);
|
||||
const encounterNameById = this.buildRankingEncounterNameById(
|
||||
character.zoneRankings,
|
||||
);
|
||||
const candidates = this.pickEncounterFightCandidates(
|
||||
character.recentReports?.data || [],
|
||||
encounterLookup,
|
||||
difficulty,
|
||||
encounterNameById,
|
||||
);
|
||||
const metricCandidates = candidates.slice(0, Math.min(limit * 3, 30));
|
||||
const encounterName = this.pickText(
|
||||
encounterLookup.displayName,
|
||||
candidates[0]?.fight?.name,
|
||||
);
|
||||
const encounterSuggestions = candidates.length
|
||||
? []
|
||||
: this.pickRecentEncounterSuggestions(
|
||||
character.recentReports?.data || [],
|
||||
encounterNameById,
|
||||
);
|
||||
const rankingSuggestions = candidates.length
|
||||
? []
|
||||
: this.pickRankingEncounterSuggestions(character.zoneRankings);
|
||||
|
||||
const logs = (
|
||||
const rankingLogs =
|
||||
encounterLookup.encounterId !== undefined
|
||||
? await this.getEncounterRankingLogs({
|
||||
characterName: character.name || characterName,
|
||||
encounterLookup,
|
||||
limit,
|
||||
partition: params.partition,
|
||||
serverRegion,
|
||||
serverSlug: character.server?.slug || serverSlug,
|
||||
timeframe: params.timeframe,
|
||||
})
|
||||
: [];
|
||||
const fallbackLogs = rankingLogs.length
|
||||
? []
|
||||
: (
|
||||
await Promise.all(
|
||||
metricCandidates.map((candidate) =>
|
||||
this.getEncounterFightLog({
|
||||
candidate,
|
||||
characterId: character.id,
|
||||
characterName: character.name || characterName,
|
||||
encounterNameById,
|
||||
encounterLookup,
|
||||
localizationMaps,
|
||||
serverName,
|
||||
@ -317,33 +369,160 @@ export class QqbotFflogsClientService {
|
||||
)
|
||||
.filter(Boolean)
|
||||
.slice(0, limit);
|
||||
const logs = rankingLogs.length ? rankingLogs : fallbackLogs;
|
||||
|
||||
return {
|
||||
characterId: character.id,
|
||||
characterName: character.name || characterName,
|
||||
encounterName: encounterLookup.displayName,
|
||||
encounterName,
|
||||
encounterSuggestions,
|
||||
logs,
|
||||
rankingSuggestions,
|
||||
rankings: [],
|
||||
replyText: this.buildEncounterLogsReplyText({
|
||||
characterId: character.id,
|
||||
characterName: character.name || characterName,
|
||||
encounterName: encounterLookup.displayName,
|
||||
encounterName,
|
||||
encounterSuggestions,
|
||||
logs,
|
||||
localizationMaps,
|
||||
rankingSuggestions,
|
||||
serverName,
|
||||
serverRegion,
|
||||
url,
|
||||
url: replyUrl,
|
||||
}),
|
||||
serverName,
|
||||
serverRegion,
|
||||
url,
|
||||
url: replyUrl,
|
||||
};
|
||||
}
|
||||
|
||||
private async getEncounterRankingLogs(params: {
|
||||
characterName: string;
|
||||
encounterLookup: FflogsEncounterLookup;
|
||||
limit: number;
|
||||
partition?: number | string;
|
||||
serverRegion: string;
|
||||
serverSlug: string;
|
||||
timeframe?: string;
|
||||
}) {
|
||||
if (params.encounterLookup.encounterId === undefined) return [];
|
||||
const data =
|
||||
await this.requestGraphql<FflogsCharacterEncounterRankingsResponse>(
|
||||
`query QqbotFflogsCharacterEncounterRankings(
|
||||
$characterName: String!
|
||||
$serverSlug: String!
|
||||
$serverRegion: String!
|
||||
$encounterID: Int!
|
||||
$partition: Int
|
||||
$timeframe: RankingTimeframeType
|
||||
) {
|
||||
characterData {
|
||||
character(
|
||||
name: $characterName
|
||||
serverSlug: $serverSlug
|
||||
serverRegion: $serverRegion
|
||||
) {
|
||||
dpsRankings: encounterRankings(
|
||||
encounterID: $encounterID
|
||||
metric: dps
|
||||
partition: $partition
|
||||
timeframe: $timeframe
|
||||
)
|
||||
hpsRankings: encounterRankings(
|
||||
encounterID: $encounterID
|
||||
metric: hps
|
||||
partition: $partition
|
||||
timeframe: $timeframe
|
||||
)
|
||||
}
|
||||
}
|
||||
}`,
|
||||
{
|
||||
characterName: params.characterName,
|
||||
encounterID: params.encounterLookup.encounterId,
|
||||
partition: this.toOptionalNumber(params.partition),
|
||||
serverRegion: params.serverRegion.toUpperCase(),
|
||||
serverSlug: params.serverSlug,
|
||||
timeframe: this.normalizeTimeframe(params.timeframe) || 'Historical',
|
||||
},
|
||||
);
|
||||
const character = data.characterData?.character;
|
||||
if (!character) return [];
|
||||
const dpsPayload = this.normalizeJsonPayload(character.dpsRankings) as any;
|
||||
const hpsPayload = this.normalizeJsonPayload(character.hpsRankings) as any;
|
||||
const dpsRanks = Array.isArray(dpsPayload?.ranks) ? dpsPayload.ranks : [];
|
||||
const hpsRanks = Array.isArray(hpsPayload?.ranks) ? hpsPayload.ranks : [];
|
||||
const hpsByFight = new Map<string, any>();
|
||||
for (const rank of hpsRanks) {
|
||||
const key = this.buildRankingFightKey(rank);
|
||||
if (key) hpsByFight.set(key, rank);
|
||||
}
|
||||
return dpsRanks
|
||||
.map((rank) =>
|
||||
this.buildEncounterRankingLogItem(
|
||||
rank,
|
||||
hpsByFight.get(this.buildRankingFightKey(rank)),
|
||||
params.encounterLookup,
|
||||
),
|
||||
)
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => Number(b.startTime || 0) - Number(a.startTime || 0))
|
||||
.slice(0, params.limit);
|
||||
}
|
||||
|
||||
private buildEncounterRankingLogItem(
|
||||
damageRank: any,
|
||||
healingRank: any,
|
||||
encounterLookup: FflogsEncounterLookup,
|
||||
): QqbotFflogsEncounterLogItem | null {
|
||||
const code = `${damageRank?.report?.code || ''}`.trim();
|
||||
const fightId = this.toOptionalNumber(damageRank?.report?.fightID);
|
||||
if (!code || fightId === undefined) return null;
|
||||
const damageScore = this.pickNumber(
|
||||
damageRank?.rankPercent,
|
||||
damageRank?.historicalPercent,
|
||||
damageRank?.todayPercent,
|
||||
);
|
||||
const healingScore = this.pickNumber(
|
||||
healingRank?.rankPercent,
|
||||
healingRank?.historicalPercent,
|
||||
healingRank?.todayPercent,
|
||||
);
|
||||
return {
|
||||
adps: this.pickNumber(damageRank?.aDPS, damageRank?.cDPS),
|
||||
color: this.getParseColor(damageScore),
|
||||
damageScore,
|
||||
dps: this.pickNumber(damageRank?.amount, damageRank?.pDPS),
|
||||
durationMs: this.pickNumber(damageRank?.duration),
|
||||
encounterName: encounterLookup.displayName,
|
||||
fightId,
|
||||
healingColor: this.getParseColor(healingScore),
|
||||
healingScore,
|
||||
hps: this.pickNumber(healingRank?.amount),
|
||||
kill: true,
|
||||
logCode: code,
|
||||
logUrl: this.buildReportFightUrl(code, fightId),
|
||||
ndps: this.pickNumber(damageRank?.nDPS),
|
||||
rdps: this.pickNumber(damageRank?.rDPS),
|
||||
startTime: this.pickNumber(
|
||||
damageRank?.startTime,
|
||||
damageRank?.report?.startTime,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
private buildRankingFightKey(rank: any) {
|
||||
const code = `${rank?.report?.code || ''}`.trim();
|
||||
const fightId = this.toOptionalNumber(rank?.report?.fightID);
|
||||
return code && fightId !== undefined ? `${code}#${fightId}` : '';
|
||||
}
|
||||
|
||||
private async getEncounterFightLog(params: {
|
||||
candidate: FflogsEncounterFightCandidate;
|
||||
characterId?: number;
|
||||
characterName: string;
|
||||
encounterNameById: Map<number, string>;
|
||||
encounterLookup: FflogsEncounterLookup;
|
||||
localizationMaps: FflogsLocalizationMaps;
|
||||
serverName: string;
|
||||
@ -380,11 +559,13 @@ export class QqbotFflogsClientService {
|
||||
dataType: DamageDone
|
||||
encounterID: $encounterID
|
||||
fightIDs: $fightIDs
|
||||
translate: true
|
||||
)
|
||||
healing: table(
|
||||
dataType: Healing
|
||||
encounterID: $encounterID
|
||||
fightIDs: $fightIDs
|
||||
translate: true
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -431,11 +612,11 @@ export class QqbotFflogsClientService {
|
||||
);
|
||||
const encounterName = this.localizeEncounter(
|
||||
this.pickText(
|
||||
params.encounterNameById.get(encounterId),
|
||||
candidate.fight.name,
|
||||
params.encounterLookup.displayName,
|
||||
`任务 ${candidate.fight.encounterID || ''}`,
|
||||
),
|
||||
params.localizationMaps,
|
||||
);
|
||||
|
||||
return {
|
||||
@ -599,7 +780,13 @@ export class QqbotFflogsClientService {
|
||||
),
|
||||
].join('\n')
|
||||
: '公开排名:暂无公开排名数据';
|
||||
return [header, idText, params.allStarText, rankingText, params.url]
|
||||
return [
|
||||
header,
|
||||
idText,
|
||||
params.allStarText,
|
||||
rankingText,
|
||||
this.formatDisplayUrl(params.url),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
}
|
||||
@ -608,8 +795,10 @@ export class QqbotFflogsClientService {
|
||||
characterId?: number;
|
||||
characterName: string;
|
||||
encounterName: string;
|
||||
encounterSuggestions?: string[];
|
||||
localizationMaps: FflogsLocalizationMaps;
|
||||
logs: QqbotFflogsEncounterLogItem[];
|
||||
rankingSuggestions?: string[];
|
||||
serverName: string;
|
||||
serverRegion: string;
|
||||
url: string;
|
||||
@ -618,19 +807,40 @@ export class QqbotFflogsClientService {
|
||||
params.serverRegion,
|
||||
params.localizationMaps,
|
||||
);
|
||||
const header = `FFLogs 最近记录:${params.characterName} @ ${params.serverName}(${region})`;
|
||||
const encounterText = `高难任务:${params.encounterName}`;
|
||||
const header = `FFLogs 最近10次记录`;
|
||||
const characterText = `角色:${params.characterName} @ ${params.serverName}(${region})`;
|
||||
const encounterText = `任务:${params.encounterName}`;
|
||||
const idText = params.characterId ? `角色ID:${params.characterId}` : '';
|
||||
const logText = params.logs.length
|
||||
? [
|
||||
'最近10次:',
|
||||
...params.logs.map((item, index) =>
|
||||
this.formatEncounterLogLine(item, index),
|
||||
),
|
||||
].join('\n')
|
||||
: '最近10次:暂无匹配的公开记录';
|
||||
: [
|
||||
'暂无匹配的公开记录',
|
||||
this.formatSuggestionLine(
|
||||
'最近报告中可查',
|
||||
params.encounterSuggestions,
|
||||
),
|
||||
this.formatSuggestionLine(
|
||||
'公开排名中可查',
|
||||
params.rankingSuggestions,
|
||||
),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
|
||||
return [header, encounterText, idText, logText, params.url]
|
||||
return [
|
||||
header,
|
||||
characterText,
|
||||
encounterText,
|
||||
idText,
|
||||
'',
|
||||
logText,
|
||||
'',
|
||||
this.formatDisplayUrl(params.url),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
}
|
||||
@ -650,23 +860,25 @@ export class QqbotFflogsClientService {
|
||||
? `${this.formatNumber(item.healingScore)}`
|
||||
: '-';
|
||||
const metrics = [
|
||||
`DPS ${this.formatMetricNumber(item.dps)}`,
|
||||
`aDPS ${this.formatMetricNumber(item.adps)}`,
|
||||
`rDPS ${this.formatMetricNumber(item.rdps)}`,
|
||||
`nDPS ${this.formatMetricNumber(item.ndps)}`,
|
||||
`HPS ${this.formatMetricNumber(item.hps)}`,
|
||||
].join(' / ');
|
||||
return `${index + 1}. ${this.formatLogTime(
|
||||
item.startTime,
|
||||
)}|${status}|颜色 ${item.color}|输出 ${damageScore}|治疗 ${
|
||||
item.healingColor
|
||||
} ${healingScore}|${metrics}|log ${item.logCode}#${item.fightId}`;
|
||||
`D${this.formatMetricNumber(item.dps)}`,
|
||||
`aD${this.formatMetricNumber(item.adps)}`,
|
||||
`rD${this.formatMetricNumber(item.rdps)}`,
|
||||
`nD${this.formatMetricNumber(item.ndps)}`,
|
||||
`H${this.formatMetricNumber(item.hps)}`,
|
||||
].join('/');
|
||||
return [
|
||||
`${index + 1}. ${this.formatLogTime(item.startTime)}|${status}|${item.encounterName}`,
|
||||
` 颜色:D${item.color}/H${item.healingColor}|评分:D${damageScore}/H${healingScore}`,
|
||||
` ${metrics}`,
|
||||
` ${this.formatDisplayUrl(item.logUrl)}`,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
private pickEncounterFightCandidates(
|
||||
reports: FflogsRecentReport[],
|
||||
encounterLookup: FflogsEncounterLookup,
|
||||
difficulty?: number,
|
||||
encounterNameById = new Map<number, string>(),
|
||||
) {
|
||||
return reports
|
||||
.flatMap((report) =>
|
||||
@ -677,7 +889,9 @@ export class QqbotFflogsClientService {
|
||||
report,
|
||||
})),
|
||||
)
|
||||
.filter(({ fight }) => this.matchEncounterFight(fight, encounterLookup))
|
||||
.filter(({ fight }) =>
|
||||
this.matchEncounterFight(fight, encounterLookup, encounterNameById),
|
||||
)
|
||||
.filter(
|
||||
({ fight }) =>
|
||||
difficulty === undefined || Number(fight.difficulty) === difficulty,
|
||||
@ -685,18 +899,81 @@ export class QqbotFflogsClientService {
|
||||
.sort((a, b) => b.absoluteStartTime - a.absoluteStartTime);
|
||||
}
|
||||
|
||||
private pickRecentEncounterSuggestions(
|
||||
reports: FflogsRecentReport[],
|
||||
encounterNameById = new Map<number, string>(),
|
||||
) {
|
||||
const names = reports.flatMap((report) =>
|
||||
(report.fights || []).map((fight) =>
|
||||
this.pickText(
|
||||
encounterNameById.get(this.toOptionalNumber(fight.encounterID) || 0),
|
||||
fight.name,
|
||||
),
|
||||
),
|
||||
);
|
||||
return this.pickDistinctSuggestions(names, 8);
|
||||
}
|
||||
|
||||
private pickRankingEncounterSuggestions(payload: unknown) {
|
||||
const rankingsPayload = this.normalizeJsonPayload(payload) as any;
|
||||
const rankings = this.pickRankings(rankingsPayload);
|
||||
const names = rankings.map((item) =>
|
||||
this.pickText(item.encounter?.name, item.encounterName, item.name),
|
||||
);
|
||||
return this.pickDistinctSuggestions(names, 8);
|
||||
}
|
||||
|
||||
private buildRankingEncounterNameById(payload: unknown) {
|
||||
const rankingsPayload = this.normalizeJsonPayload(payload) as any;
|
||||
const rankings = this.pickRankings(rankingsPayload);
|
||||
const map = new Map<number, string>();
|
||||
for (const item of rankings) {
|
||||
const id = this.pickNumber(item.encounter?.id, item.encounterID, item.id);
|
||||
const name = this.pickText(
|
||||
item.encounter?.name,
|
||||
item.encounterName,
|
||||
item.name,
|
||||
);
|
||||
if (id !== undefined && name) map.set(id, name);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private pickDistinctSuggestions(values: any[], limit: number) {
|
||||
const suggestions: string[] = [];
|
||||
const keys = new Set<string>();
|
||||
for (const value of values) {
|
||||
const text = `${value || ''}`.trim();
|
||||
if (!text || text.toLowerCase() === 'unknown') continue;
|
||||
const key = this.normalizeLookupKey(text);
|
||||
if (!key || keys.has(key)) continue;
|
||||
keys.add(key);
|
||||
suggestions.push(text);
|
||||
if (suggestions.length >= limit) break;
|
||||
}
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
private formatSuggestionLine(label: string, values?: string[]) {
|
||||
const list = (values || []).filter(Boolean);
|
||||
return list.length ? `${label}:${list.join('、')}` : '';
|
||||
}
|
||||
|
||||
private matchEncounterFight(
|
||||
fight: FflogsReportFight,
|
||||
encounterLookup: FflogsEncounterLookup,
|
||||
encounterNameById = new Map<number, string>(),
|
||||
) {
|
||||
const encounterId = this.toOptionalNumber(fight.encounterID);
|
||||
if (
|
||||
encounterLookup.encounterId !== undefined &&
|
||||
Number(fight.encounterID) === encounterLookup.encounterId
|
||||
encounterId === encounterLookup.encounterId
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
const fightKeys = this.buildLookupKeys(
|
||||
`${fight.name || ''}`,
|
||||
encounterId !== undefined ? encounterNameById.get(encounterId) || '' : '',
|
||||
`${fight.encounterID || ''}`,
|
||||
);
|
||||
return encounterLookup.keys.some((key) => fightKeys.includes(key));
|
||||
@ -707,31 +984,17 @@ export class QqbotFflogsClientService {
|
||||
): Promise<FflogsEncounterLookup> {
|
||||
const raw = `${input || ''}`.trim();
|
||||
const inputKeys = this.buildLookupKeys(raw);
|
||||
const dicts = await this.dictService.getDictItemsByKey(
|
||||
FFLOGS_LOCALIZATION_DICT_CODES.encounter,
|
||||
);
|
||||
const entries = dicts.map((item) => ({
|
||||
displayName: `${item.label || item.value}`.trim(),
|
||||
encounterId: this.toOptionalNumber(item.value),
|
||||
keys: this.buildLookupKeys(`${item.label}`, `${item.value}`),
|
||||
}));
|
||||
const exact = entries.find((entry) =>
|
||||
inputKeys.some((inputKey) => entry.keys.includes(inputKey)),
|
||||
);
|
||||
const prefix = exact
|
||||
? undefined
|
||||
: entries.find((entry) =>
|
||||
inputKeys.some(
|
||||
(inputKey) =>
|
||||
inputKey.length >= 3 &&
|
||||
entry.keys.some(
|
||||
(key) => key.startsWith(inputKey) || key.includes(inputKey),
|
||||
),
|
||||
),
|
||||
);
|
||||
const matched = exact || prefix;
|
||||
|
||||
if (!matched) {
|
||||
const catalog = await this.getFflogsEncounterCatalog();
|
||||
const matched = this.findEncounterCatalogMatch(inputKeys, catalog);
|
||||
if (matched) {
|
||||
return {
|
||||
displayName: matched.displayName,
|
||||
encounterId: matched.encounterId,
|
||||
input: raw,
|
||||
keys: [...new Set([...inputKeys, ...matched.keys])],
|
||||
zoneId: matched.zoneId,
|
||||
};
|
||||
}
|
||||
return {
|
||||
displayName: raw,
|
||||
encounterId: this.toOptionalNumber(raw),
|
||||
@ -740,12 +1003,81 @@ export class QqbotFflogsClientService {
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
displayName: matched.displayName,
|
||||
encounterId: matched.encounterId,
|
||||
input: raw,
|
||||
keys: [...new Set([...matched.keys, ...inputKeys])],
|
||||
private async getFflogsEncounterCatalog() {
|
||||
if (
|
||||
this.encounterCatalogCache &&
|
||||
Date.now() < this.encounterCatalogCache.expiresAt
|
||||
) {
|
||||
return this.encounterCatalogCache.entries;
|
||||
}
|
||||
const data = await this.requestGraphql<{
|
||||
worldData?: {
|
||||
zones?: Array<{
|
||||
encounters?: Array<{ id?: number; name?: string }>;
|
||||
id?: number;
|
||||
name?: string;
|
||||
}>;
|
||||
};
|
||||
}>(
|
||||
`query QqbotFflogsEncounterCatalog {
|
||||
worldData {
|
||||
zones {
|
||||
id
|
||||
name
|
||||
encounters {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
{},
|
||||
);
|
||||
const entries = (data.worldData?.zones || []).flatMap((zone) =>
|
||||
(zone.encounters || [])
|
||||
.map((encounter) => {
|
||||
const encounterId = this.toOptionalNumber(encounter.id);
|
||||
const displayName = `${encounter.name || ''}`.trim();
|
||||
if (encounterId === undefined || !displayName) return undefined;
|
||||
return {
|
||||
displayName,
|
||||
encounterId,
|
||||
keys: this.buildLookupKeys(
|
||||
displayName,
|
||||
`${encounterId}`,
|
||||
`${zone.name || ''}`,
|
||||
),
|
||||
zoneId: this.toOptionalNumber(zone.id),
|
||||
zoneName: zone.name,
|
||||
} satisfies FflogsEncounterCatalogItem;
|
||||
})
|
||||
.filter(Boolean),
|
||||
) as FflogsEncounterCatalogItem[];
|
||||
this.encounterCatalogCache = {
|
||||
entries,
|
||||
expiresAt: Date.now() + 24 * 60 * 60 * 1000,
|
||||
};
|
||||
return entries;
|
||||
}
|
||||
|
||||
private findEncounterCatalogMatch(
|
||||
inputKeys: string[],
|
||||
catalog: FflogsEncounterCatalogItem[],
|
||||
) {
|
||||
const exact = catalog.find((entry) =>
|
||||
inputKeys.some((inputKey) => entry.keys.includes(inputKey)),
|
||||
);
|
||||
if (exact) return exact;
|
||||
return catalog.find((entry) =>
|
||||
inputKeys.some((inputKey) =>
|
||||
entry.keys.some(
|
||||
(key) =>
|
||||
inputKey.length >= 2 &&
|
||||
key.length >= 2 &&
|
||||
(key.includes(inputKey) || inputKey.includes(key)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private findRankingCharacter(
|
||||
@ -845,7 +1177,6 @@ export class QqbotFflogsClientService {
|
||||
item.name,
|
||||
`记录 ${index + 1}`,
|
||||
),
|
||||
localizationMaps,
|
||||
);
|
||||
const percent = this.pickNumber(
|
||||
item.rankPercent,
|
||||
@ -925,6 +1256,14 @@ export class QqbotFflogsClientService {
|
||||
return value;
|
||||
}
|
||||
|
||||
private formatDisplayUrl(value: string) {
|
||||
try {
|
||||
return decodeURI(value);
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
private buildCharacterUrl(
|
||||
serverRegion: string,
|
||||
serverSlug: string,
|
||||
@ -935,6 +1274,22 @@ export class QqbotFflogsClientService {
|
||||
)}/${encodeURIComponent(serverSlug)}/${encodeURIComponent(characterName)}`;
|
||||
}
|
||||
|
||||
private buildCharacterEncounterUrl(
|
||||
url: string,
|
||||
encounterLookup: FflogsEncounterLookup,
|
||||
partition?: number | string,
|
||||
) {
|
||||
if (encounterLookup.encounterId === undefined) return url;
|
||||
const searchParams = new URLSearchParams();
|
||||
if (encounterLookup.zoneId !== undefined) {
|
||||
searchParams.set('zone', `${encounterLookup.zoneId}`);
|
||||
}
|
||||
searchParams.set('boss', `${encounterLookup.encounterId}`);
|
||||
const partitionValue = this.toOptionalNumber(partition);
|
||||
searchParams.set('partition', `${partitionValue ?? 0}`);
|
||||
return `${url}?${searchParams.toString()}`;
|
||||
}
|
||||
|
||||
private buildReportFightUrl(code: string, fightId: number) {
|
||||
return `${this.webBaseUrl}/reports/${encodeURIComponent(
|
||||
code,
|
||||
@ -1083,8 +1438,7 @@ export class QqbotFflogsClientService {
|
||||
}
|
||||
|
||||
private async getLocalizationMaps(): Promise<FflogsLocalizationMaps> {
|
||||
const [encounter, job, metric, role, serverRegion] = await Promise.all([
|
||||
this.getNormalizedDictMap(FFLOGS_LOCALIZATION_DICT_CODES.encounter),
|
||||
const [job, metric, role, serverRegion] = await Promise.all([
|
||||
this.getNormalizedDictMap(FFLOGS_LOCALIZATION_DICT_CODES.job),
|
||||
this.getNormalizedDictMap(FFLOGS_LOCALIZATION_DICT_CODES.metric),
|
||||
this.getNormalizedDictMap(FFLOGS_LOCALIZATION_DICT_CODES.role),
|
||||
@ -1092,7 +1446,6 @@ export class QqbotFflogsClientService {
|
||||
]);
|
||||
|
||||
return {
|
||||
encounter,
|
||||
job,
|
||||
metric,
|
||||
role,
|
||||
@ -1111,13 +1464,8 @@ export class QqbotFflogsClientService {
|
||||
return map;
|
||||
}
|
||||
|
||||
private localizeEncounter(
|
||||
value: string,
|
||||
localizationMaps: FflogsLocalizationMaps,
|
||||
) {
|
||||
return (
|
||||
localizationMaps.encounter.get(this.normalizeLookupKey(value)) || value
|
||||
);
|
||||
private localizeEncounter(value: string) {
|
||||
return value;
|
||||
}
|
||||
|
||||
private localizeMetric(
|
||||
|
||||
@ -45,7 +45,8 @@ export class QqbotFflogsPluginService {
|
||||
properties: {
|
||||
characterName: { description: '角色名', type: 'string' },
|
||||
encounter: {
|
||||
description: '高难任务名,支持字典维护的中文名/英文名/缩写',
|
||||
description:
|
||||
'高难任务名,按 FFLogs 公开报告中的任务名或 encounterID 匹配',
|
||||
type: 'string',
|
||||
},
|
||||
limit: {
|
||||
|
||||
@ -18,6 +18,17 @@ export type FflogsCharacterSummaryResponse = {
|
||||
};
|
||||
};
|
||||
|
||||
export type FflogsCharacterEncounterRankingsResponse = {
|
||||
characterData?: {
|
||||
character?:
|
||||
| (FflogsCharacter & {
|
||||
dpsRankings?: unknown;
|
||||
hpsRankings?: unknown;
|
||||
})
|
||||
| null;
|
||||
};
|
||||
};
|
||||
|
||||
export type FflogsEncounterFightCandidate = {
|
||||
absoluteStartTime: number;
|
||||
fight: FflogsReportFight;
|
||||
@ -29,6 +40,7 @@ export type FflogsEncounterLookup = {
|
||||
encounterId?: number;
|
||||
input: string;
|
||||
keys: string[];
|
||||
zoneId?: number;
|
||||
};
|
||||
|
||||
export type FflogsGraphqlResponse<T> = {
|
||||
@ -38,12 +50,7 @@ export type FflogsGraphqlResponse<T> = {
|
||||
|
||||
export type FflogsHttpMethod = 'GET' | 'POST';
|
||||
|
||||
export type FflogsLocalizationKey =
|
||||
| 'encounter'
|
||||
| 'job'
|
||||
| 'metric'
|
||||
| 'role'
|
||||
| 'serverRegion';
|
||||
export type FflogsLocalizationKey = 'job' | 'metric' | 'role' | 'serverRegion';
|
||||
|
||||
export type FflogsLocalizationMaps = Record<
|
||||
FflogsLocalizationKey,
|
||||
@ -124,7 +131,9 @@ export type QqbotFflogsCharacterSummaryResult = {
|
||||
characterId?: number;
|
||||
characterName: string;
|
||||
encounterName?: string;
|
||||
encounterSuggestions?: string[];
|
||||
logs?: QqbotFflogsEncounterLogItem[];
|
||||
rankingSuggestions?: string[];
|
||||
rankings: FflogsRankingItem[];
|
||||
replyText: string;
|
||||
serverName: string;
|
||||
|
||||
@ -35,15 +35,7 @@ describe('QqbotCommandParserService FFLogs parser', () => {
|
||||
} as QqbotCommand;
|
||||
|
||||
const dictService = {
|
||||
getDictItemsByKey: jest.fn(async (dictKey: string) => {
|
||||
if (dictKey === 'FFLOGS_ENCOUNTER_LABEL') {
|
||||
return [
|
||||
{ label: 'M9S 吸血鬼偶像', value: 'vampfatale' },
|
||||
{ label: 'M10S Red Hot and Deep Blue', value: 'redhotanddeepblue' },
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}),
|
||||
getDictItemsByKey: jest.fn(async () => []),
|
||||
relationTree: jest.fn(async () => [
|
||||
{
|
||||
children: [
|
||||
@ -72,27 +64,34 @@ describe('QqbotCommandParserService FFLogs parser', () => {
|
||||
|
||||
const service = new QqbotCommandParserService(dictService);
|
||||
|
||||
it('parses Chinese encounter name after character and server', async () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('parses Chinese encounter name after character and server without encounter dict', async () => {
|
||||
const matched = await service.match(command, {
|
||||
messageText: '/logs Anbbo 琥珀原 吸血鬼偶像',
|
||||
messageText: '/logs Kwi柊司 琥珀原 上位护锁刃龙',
|
||||
} as any);
|
||||
|
||||
expect(matched?.input).toMatchObject({
|
||||
characterName: 'Anbbo',
|
||||
encounterName: '吸血鬼偶像',
|
||||
characterName: 'Kwi柊司',
|
||||
encounterName: '上位护锁刃龙',
|
||||
serverSlug: '琥珀原',
|
||||
});
|
||||
expect(dictService.getDictItemsByKey).not.toHaveBeenCalledWith(
|
||||
'FFLOGS_ENCOUNTER_LABEL',
|
||||
);
|
||||
});
|
||||
|
||||
it('parses space separated encounter name when server is explicit', async () => {
|
||||
it('keeps character summary parsing when no encounter is provided', async () => {
|
||||
const matched = await service.match(command, {
|
||||
messageText: '/logs Kwi 柊司 M10S Red Hot and Deep Blue server=琥珀原',
|
||||
messageText: '/logs Kwi柊司 琥珀原',
|
||||
} as any);
|
||||
|
||||
expect(matched?.input).toMatchObject({
|
||||
characterName: 'Kwi 柊司',
|
||||
encounterName: 'M10S Red Hot and Deep Blue',
|
||||
characterName: 'Kwi柊司',
|
||||
serverSlug: '琥珀原',
|
||||
});
|
||||
expect((matched?.input as any).encounterName).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
@ -27,10 +27,6 @@ import { QqbotFflogsClientService } from '@/qqbot/plugins/fflogs/qqbot-fflogs-cl
|
||||
|
||||
describe('QqbotFflogsClientService', () => {
|
||||
const dicts = {
|
||||
FFLOGS_ENCOUNTER_LABEL: [
|
||||
{ label: 'M9S 吸血鬼偶像', value: 'vampfatale' },
|
||||
{ label: 'M12S P2 Lindwurm II', value: 'lindwurmii' },
|
||||
],
|
||||
FFLOGS_JOB_LABEL: [{ label: '机工士', value: 'machinist' }],
|
||||
FFLOGS_METRIC_LABEL: [
|
||||
{ label: 'DPS', value: 'dps' },
|
||||
@ -54,6 +50,13 @@ describe('QqbotFflogsClientService', () => {
|
||||
} as unknown as DictService,
|
||||
);
|
||||
|
||||
it('uses cn FFLogs API endpoints by default', () => {
|
||||
expect((service as any).graphqlUrl).toBe(
|
||||
'https://cn.fflogs.com/api/v2/client',
|
||||
);
|
||||
expect((service as any).tokenUrl).toBe('https://cn.fflogs.com/oauth/token');
|
||||
});
|
||||
|
||||
it('formats character rankings with dict labels', async () => {
|
||||
const localizationMaps = await (service as any).getLocalizationMaps();
|
||||
const replyText = (service as any).buildReplyText({
|
||||
@ -65,26 +68,26 @@ describe('QqbotFflogsClientService', () => {
|
||||
rankings: [
|
||||
{
|
||||
bestAmount: 36635,
|
||||
encounter: { name: 'Vamp Fatale' },
|
||||
encounter: { name: '上位护锁刃龙' },
|
||||
rankPercent: 72.4,
|
||||
spec: 'Machinist',
|
||||
},
|
||||
{
|
||||
bestAmount: 0,
|
||||
encounter: { name: 'Lindwurm II' },
|
||||
encounter: { name: '下位护锁刃龙' },
|
||||
rankPercent: 0,
|
||||
},
|
||||
],
|
||||
serverName: '琥珀原',
|
||||
serverRegion: 'CN',
|
||||
url: 'https://cn.fflogs.com/character/cn/example/Kwi',
|
||||
url: 'https://cn.fflogs.com/character/cn/%E7%90%A5%E7%8F%80%E5%8E%9F/Kwi%E6%9F%8A%E5%8F%B8?zone=67&boss=1082&partition=0',
|
||||
});
|
||||
|
||||
expect(replyText).toContain('FFLogs 战绩:Kwi柊司 @ 琥珀原(国服)');
|
||||
expect(replyText).toContain(
|
||||
'1. M9S 吸血鬼偶像:72.4% | DPS 36,635 | 机工士',
|
||||
'1. 上位护锁刃龙:72.4% | DPS 36,635 | 机工士',
|
||||
);
|
||||
expect(replyText).toContain('2. M12S P2 Lindwurm II:暂无有效排名');
|
||||
expect(replyText).toContain('2. 下位护锁刃龙:暂无有效排名');
|
||||
});
|
||||
|
||||
it('formats recent encounter logs with Chinese encounter label and colors', async () => {
|
||||
@ -100,7 +103,7 @@ describe('QqbotFflogsClientService', () => {
|
||||
color: '蓝',
|
||||
damageScore: 66.5,
|
||||
dps: 37560.7,
|
||||
encounterName: 'M9S 吸血鬼偶像',
|
||||
encounterName: '上位护锁刃龙',
|
||||
fightId: 14,
|
||||
healingColor: '灰',
|
||||
healingScore: 0,
|
||||
@ -115,21 +118,37 @@ describe('QqbotFflogsClientService', () => {
|
||||
],
|
||||
serverName: '琥珀原',
|
||||
serverRegion: 'CN',
|
||||
url: 'https://cn.fflogs.com/character/cn/example/Kwi',
|
||||
url: 'https://cn.fflogs.com/character/cn/%E7%90%A5%E7%8F%80%E5%8E%9F/Kwi%E6%9F%8A%E5%8F%B8?zone=67&boss=1082&partition=0',
|
||||
});
|
||||
|
||||
expect(replyText).toContain('高难任务:M9S 吸血鬼偶像');
|
||||
expect(replyText).toContain('颜色 蓝|输出 66.5|治疗 灰 0');
|
||||
expect(replyText).toContain('任务:M9S 吸血鬼偶像');
|
||||
expect(replyText).toContain('1. 05/01 20:30|击杀|上位护锁刃龙');
|
||||
expect(replyText).toContain('颜色:D蓝/H灰|评分:D66.5/H0');
|
||||
expect(replyText).toContain('D37,561/aD37,561/rD36,635/nD36,635/H0');
|
||||
expect(replyText).toContain(
|
||||
'DPS 37,561 / aDPS 37,561 / rDPS 36,635 / nDPS 36,635 / HPS 0',
|
||||
'https://cn.fflogs.com/reports/CgvFRqyxJhLtmND7#fight=14',
|
||||
);
|
||||
expect(replyText).toContain(
|
||||
'https://cn.fflogs.com/character/cn/琥珀原/Kwi柊司?zone=67&boss=1082&partition=0',
|
||||
);
|
||||
expect(replyText).toContain('log CgvFRqyxJhLtmND7#14');
|
||||
});
|
||||
|
||||
it('resolves Chinese encounter input from dict labels', async () => {
|
||||
const lookup = await (service as any).resolveEncounterLookup('吸血鬼偶像');
|
||||
it('resolves Chinese encounter input from FFLogs encounter catalog', async () => {
|
||||
jest.spyOn(service as any, 'getFflogsEncounterCatalog').mockResolvedValue([
|
||||
{
|
||||
displayName: '护锁刃龙',
|
||||
encounterId: 1082,
|
||||
keys: ['护锁刃龙', '1082'],
|
||||
zoneId: 67,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(lookup.displayName).toBe('M9S 吸血鬼偶像');
|
||||
expect(lookup.keys).toContain('vampfatale');
|
||||
const lookup = await (service as any).resolveEncounterLookup(
|
||||
'上位护锁刃龙',
|
||||
);
|
||||
|
||||
expect(lookup.displayName).toBe('护锁刃龙');
|
||||
expect(lookup.encounterId).toBe(1082);
|
||||
expect(lookup.zoneId).toBe(67);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user