feat: 扩展 QQBot FF14 查价区服解析
This commit is contained in:
parent
31119bbc93
commit
92462ae736
@ -1,6 +1,13 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import type { QqbotCommand } from './qqbot-command.entity';
|
import type { QqbotCommand } from './qqbot-command.entity';
|
||||||
import type { QqbotNormalizedMessage } from '../qqbot.types';
|
import type { QqbotNormalizedMessage } from '../qqbot.types';
|
||||||
|
import {
|
||||||
|
isQqbotFf14DataCenterName,
|
||||||
|
isQqbotFf14LocationName,
|
||||||
|
isQqbotFf14RegionName,
|
||||||
|
isQqbotFf14WorldName,
|
||||||
|
splitQqbotFf14WorldPath,
|
||||||
|
} from '../plugins/ff14Market/qqbot-ff14-worlds';
|
||||||
|
|
||||||
export type QqbotCommandMatchResult = {
|
export type QqbotCommandMatchResult = {
|
||||||
alias: string;
|
alias: string;
|
||||||
@ -68,9 +75,9 @@ export class QqbotCommandParserService {
|
|||||||
const positional: string[] = [];
|
const positional: string[] = [];
|
||||||
|
|
||||||
for (const token of tokens) {
|
for (const token of tokens) {
|
||||||
if (/^(hq|HQ)$/.test(token)) {
|
if (/^hq$/i.test(token)) {
|
||||||
flags.set('hq', true);
|
flags.set('hq', true);
|
||||||
} else if (/^(nq|NQ)$/.test(token)) {
|
} else if (/^nq$/i.test(token)) {
|
||||||
flags.set('hq', 'false');
|
flags.set('hq', 'false');
|
||||||
} else if (token.includes('=')) {
|
} else if (token.includes('=')) {
|
||||||
const [key, ...rest] = token.split('=');
|
const [key, ...rest] = token.split('=');
|
||||||
@ -80,27 +87,105 @@ export class QqbotCommandParserService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let world = this.normalizeString(flags.get('world') || flags.get('server'));
|
let region = this.normalizeString(flags.get('region') || flags.get('地区'));
|
||||||
|
let dataCenter = this.normalizeString(
|
||||||
|
flags.get('dataCenter') ||
|
||||||
|
flags.get('datacenter') ||
|
||||||
|
flags.get('dc') ||
|
||||||
|
flags.get('大区'),
|
||||||
|
);
|
||||||
|
let world = this.normalizeString(
|
||||||
|
flags.get('world') ||
|
||||||
|
flags.get('server') ||
|
||||||
|
flags.get('服务器') ||
|
||||||
|
flags.get('小区'),
|
||||||
|
);
|
||||||
let item = positional.join(' ');
|
let item = positional.join(' ');
|
||||||
if (!world && positional.length > 1) {
|
|
||||||
world = positional[positional.length - 1];
|
const worldPath = splitQqbotFf14WorldPath(world);
|
||||||
item = positional.slice(0, -1).join(' ');
|
if (worldPath.dataCenter && worldPath.world) {
|
||||||
|
dataCenter = dataCenter || worldPath.dataCenter;
|
||||||
|
region = region || worldPath.region || '';
|
||||||
|
world = worldPath.world;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!world && !dataCenter && positional.length > 1) {
|
||||||
|
const picked = this.pickTrailingFf14Location(positional);
|
||||||
|
if (picked) {
|
||||||
|
dataCenter = picked.dataCenter || dataCenter;
|
||||||
|
item = picked.item;
|
||||||
|
region = picked.region || region;
|
||||||
|
world = picked.world || world;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (item.includes('@')) {
|
if (item.includes('@')) {
|
||||||
const [itemName, worldName] = item.split('@');
|
const [itemName, worldName] = item.split('@');
|
||||||
|
const itemWorldPath = splitQqbotFf14WorldPath(worldName);
|
||||||
item = itemName.trim();
|
item = itemName.trim();
|
||||||
world = world || worldName?.trim();
|
dataCenter = dataCenter || itemWorldPath.dataCenter || '';
|
||||||
|
region = region || itemWorldPath.region || '';
|
||||||
|
world = world || itemWorldPath.world || worldName?.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
dataCenter,
|
||||||
hq: this.normalizeHq(flags.get('hq')),
|
hq: this.normalizeHq(flags.get('hq')),
|
||||||
item,
|
item,
|
||||||
language: this.normalizeString(flags.get('lang')) || 'zh',
|
language: this.normalizeString(flags.get('lang')) || 'zh',
|
||||||
raw: rawArgs,
|
raw: rawArgs,
|
||||||
|
region,
|
||||||
world,
|
world,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private pickTrailingFf14Location(positional: string[]) {
|
||||||
|
const last = positional[positional.length - 1];
|
||||||
|
if (!isQqbotFf14LocationName(last)) return null;
|
||||||
|
|
||||||
|
const path = splitQqbotFf14WorldPath(last);
|
||||||
|
if (path.dataCenter && path.world) {
|
||||||
|
return {
|
||||||
|
dataCenter: path.dataCenter,
|
||||||
|
item: positional.slice(0, -1).join(' '),
|
||||||
|
region: path.region,
|
||||||
|
world: path.world,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const previous = positional[positional.length - 2];
|
||||||
|
const beforePrevious = positional[positional.length - 3];
|
||||||
|
if (
|
||||||
|
previous &&
|
||||||
|
isQqbotFf14DataCenterName(previous) &&
|
||||||
|
isQqbotFf14WorldName(last)
|
||||||
|
) {
|
||||||
|
const hasRegion = beforePrevious && isQqbotFf14RegionName(beforePrevious);
|
||||||
|
return {
|
||||||
|
dataCenter: previous,
|
||||||
|
item: positional.slice(0, hasRegion ? -3 : -2).join(' '),
|
||||||
|
region: hasRegion ? beforePrevious : undefined,
|
||||||
|
world: last,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
previous &&
|
||||||
|
isQqbotFf14RegionName(previous) &&
|
||||||
|
isQqbotFf14DataCenterName(last)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
dataCenter: last,
|
||||||
|
item: positional.slice(0, -2).join(' '),
|
||||||
|
region: previous,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
item: positional.slice(0, -1).join(' '),
|
||||||
|
world: last,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private normalizeHq(value?: string | true) {
|
private normalizeHq(value?: string | true) {
|
||||||
if (value === undefined) return undefined;
|
if (value === undefined) return undefined;
|
||||||
if (value === true) return true;
|
if (value === true) return true;
|
||||||
|
|||||||
@ -2,12 +2,14 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import * as http from 'node:http';
|
import * as http from 'node:http';
|
||||||
import * as https from 'node:https';
|
import * as https from 'node:https';
|
||||||
|
import { resolveQqbotFf14MarketTarget } from './qqbot-ff14-worlds';
|
||||||
|
|
||||||
type HttpMethod = 'GET';
|
type HttpMethod = 'GET';
|
||||||
|
|
||||||
type XivapiSearchItem = {
|
type XivapiSearchItem = {
|
||||||
fields?: {
|
fields?: {
|
||||||
Icon?: string;
|
Icon?: string;
|
||||||
|
IsUntradable?: boolean;
|
||||||
LevelItem?: number;
|
LevelItem?: number;
|
||||||
Name?: string;
|
Name?: string;
|
||||||
};
|
};
|
||||||
@ -40,6 +42,7 @@ type UniversalisMarketResponse = {
|
|||||||
|
|
||||||
export type QqbotFf14ResolvedItem = {
|
export type QqbotFf14ResolvedItem = {
|
||||||
icon?: string;
|
icon?: string;
|
||||||
|
isUntradable?: boolean;
|
||||||
itemId: number;
|
itemId: number;
|
||||||
itemLevel?: number;
|
itemLevel?: number;
|
||||||
name: string;
|
name: string;
|
||||||
@ -60,6 +63,13 @@ export type QqbotFf14PriceResult = {
|
|||||||
export class QqbotFf14ClientService {
|
export class QqbotFf14ClientService {
|
||||||
private readonly xivapiBaseUrl: string;
|
private readonly xivapiBaseUrl: string;
|
||||||
private readonly universalisBaseUrl: string;
|
private readonly universalisBaseUrl: string;
|
||||||
|
private readonly cnItemAliases = new Map<
|
||||||
|
string,
|
||||||
|
{ itemId: number; name: string }
|
||||||
|
>([
|
||||||
|
['小鸣鼠', { itemId: 43590, name: '小鸣鼠角笛' }],
|
||||||
|
['小鸣鼠角笛', { itemId: 43590, name: '小鸣鼠角笛' }],
|
||||||
|
]);
|
||||||
|
|
||||||
constructor(private readonly configService: ConfigService) {
|
constructor(private readonly configService: ConfigService) {
|
||||||
this.xivapiBaseUrl =
|
this.xivapiBaseUrl =
|
||||||
@ -75,22 +85,31 @@ export class QqbotFf14ClientService {
|
|||||||
itemId?: number | string;
|
itemId?: number | string;
|
||||||
language?: string;
|
language?: string;
|
||||||
}): Promise<QqbotFf14ResolvedItem> {
|
}): Promise<QqbotFf14ResolvedItem> {
|
||||||
|
const language = this.normalizeXivapiLanguage(params.language);
|
||||||
const itemId = Number(params.itemId || params.item);
|
const itemId = Number(params.itemId || params.item);
|
||||||
if (Number.isInteger(itemId) && itemId > 0) {
|
if (Number.isInteger(itemId) && itemId > 0) {
|
||||||
return this.getItemById(itemId, params.language);
|
return this.getItemById(itemId, language);
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyword = `${params.item || ''}`.trim();
|
const keyword = `${params.item || ''}`.trim();
|
||||||
if (!keyword) throw new Error('请提供 FF14 物品名称或物品 ID');
|
if (!keyword) throw new Error('请提供 FF14 物品名称或物品 ID');
|
||||||
|
|
||||||
|
const alias = this.resolveCnItemAlias(keyword);
|
||||||
|
if (alias) {
|
||||||
|
return this.getItemById(alias.itemId, language, alias.name);
|
||||||
|
}
|
||||||
|
|
||||||
const url = new URL(`${this.xivapiBaseUrl}/search`);
|
const url = new URL(`${this.xivapiBaseUrl}/search`);
|
||||||
url.searchParams.set('sheets', 'Item');
|
url.searchParams.set('sheets', 'Item');
|
||||||
url.searchParams.set('query', keyword);
|
url.searchParams.set('fields', 'Name,Icon,LevelItem,IsUntradable');
|
||||||
url.searchParams.set('language', params.language || 'zh');
|
url.searchParams.set('query', `Name~"${this.escapeXivapiValue(keyword)}"`);
|
||||||
|
url.searchParams.set('language', language);
|
||||||
|
url.searchParams.set('limit', '10');
|
||||||
|
|
||||||
const data = await this.requestJson<{ results?: XivapiSearchItem[] }>(
|
const data = await this.requestJson<{ results?: XivapiSearchItem[] }>(
|
||||||
url,
|
url,
|
||||||
'GET',
|
'GET',
|
||||||
|
'XIVAPI 物品解析',
|
||||||
);
|
);
|
||||||
const item = (data.results || []).find(
|
const item = (data.results || []).find(
|
||||||
(result) =>
|
(result) =>
|
||||||
@ -100,6 +119,7 @@ export class QqbotFf14ClientService {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
icon: item.fields?.Icon,
|
icon: item.fields?.Icon,
|
||||||
|
isUntradable: item.fields?.IsUntradable,
|
||||||
itemId: Number(item.row_id || item.id),
|
itemId: Number(item.row_id || item.id),
|
||||||
itemLevel: item.fields?.LevelItem,
|
itemLevel: item.fields?.LevelItem,
|
||||||
name: item.fields?.Name || item.name || keyword,
|
name: item.fields?.Name || item.name || keyword,
|
||||||
@ -107,23 +127,49 @@ export class QqbotFf14ClientService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getPrice(params: {
|
async getPrice(params: {
|
||||||
|
dataCenter?: string;
|
||||||
hq?: boolean;
|
hq?: boolean;
|
||||||
item?: string;
|
item?: string;
|
||||||
itemId?: number | string;
|
itemId?: number | string;
|
||||||
language?: string;
|
language?: string;
|
||||||
|
region?: string;
|
||||||
world?: string;
|
world?: string;
|
||||||
}): Promise<QqbotFf14PriceResult> {
|
}): Promise<QqbotFf14PriceResult> {
|
||||||
const world = this.normalizeWorld(params.world);
|
const marketTarget = this.resolveMarketTarget(params);
|
||||||
const item = await this.resolveItem(params);
|
const item = await this.resolveItem(params);
|
||||||
const url = new URL(`${this.universalisBaseUrl}/${world}/${item.itemId}`);
|
if (item.isUntradable) {
|
||||||
|
return {
|
||||||
|
hq: params.hq,
|
||||||
|
item,
|
||||||
|
listings: [],
|
||||||
|
replyText: `FF14 查价:${item.name}\n该物品不可交易,暂无市场价格。`,
|
||||||
|
world: marketTarget.label,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(
|
||||||
|
`${this.universalisBaseUrl}/${encodeURIComponent(
|
||||||
|
marketTarget.target,
|
||||||
|
)}/${item.itemId}`,
|
||||||
|
);
|
||||||
url.searchParams.set('entries', '5');
|
url.searchParams.set('entries', '5');
|
||||||
url.searchParams.set('listings', '5');
|
url.searchParams.set('listings', '5');
|
||||||
if (params.hq !== undefined) url.searchParams.set('hq', `${params.hq}`);
|
if (params.hq !== undefined) url.searchParams.set('hq', `${params.hq}`);
|
||||||
|
|
||||||
const data = await this.requestJson<UniversalisMarketResponse>(url, 'GET');
|
const data = await this.requestJson<UniversalisMarketResponse>(
|
||||||
const minPrice = this.pickPrice(data, params.hq, 'min');
|
url,
|
||||||
const averagePrice = this.pickPrice(data, params.hq, 'average');
|
'GET',
|
||||||
|
'Universalis 市场查询',
|
||||||
|
);
|
||||||
const listings = (data.listings || []).slice(0, 5);
|
const listings = (data.listings || []).slice(0, 5);
|
||||||
|
const minPrice = this.normalizeMarketPrice(
|
||||||
|
this.pickPrice(data, params.hq, 'min'),
|
||||||
|
listings,
|
||||||
|
);
|
||||||
|
const averagePrice = this.normalizeMarketPrice(
|
||||||
|
this.pickPrice(data, params.hq, 'average'),
|
||||||
|
listings,
|
||||||
|
);
|
||||||
const updatedAt = data.lastUploadTime
|
const updatedAt = data.lastUploadTime
|
||||||
? new Date(data.lastUploadTime).toISOString()
|
? new Date(data.lastUploadTime).toISOString()
|
||||||
: undefined;
|
: undefined;
|
||||||
@ -141,26 +187,33 @@ export class QqbotFf14ClientService {
|
|||||||
listings,
|
listings,
|
||||||
minPrice,
|
minPrice,
|
||||||
updatedAt,
|
updatedAt,
|
||||||
world,
|
world: marketTarget.label,
|
||||||
}),
|
}),
|
||||||
updatedAt,
|
updatedAt,
|
||||||
world,
|
world: marketTarget.label,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getItemById(
|
private async getItemById(
|
||||||
itemId: number,
|
itemId: number,
|
||||||
language = 'zh',
|
language = 'zh',
|
||||||
|
displayName?: string,
|
||||||
): Promise<QqbotFf14ResolvedItem> {
|
): Promise<QqbotFf14ResolvedItem> {
|
||||||
const url = new URL(`${this.xivapiBaseUrl}/sheet/Item/${itemId}`);
|
const url = new URL(`${this.xivapiBaseUrl}/sheet/Item/${itemId}`);
|
||||||
url.searchParams.set('language', language);
|
url.searchParams.set('fields', 'Name,Icon,LevelItem,IsUntradable');
|
||||||
const data = await this.requestJson<Record<string, any>>(url, 'GET');
|
url.searchParams.set('language', this.normalizeXivapiLanguage(language));
|
||||||
|
const data = await this.requestJson<Record<string, any>>(
|
||||||
|
url,
|
||||||
|
'GET',
|
||||||
|
'XIVAPI 物品解析',
|
||||||
|
);
|
||||||
const fields = data.fields || data;
|
const fields = data.fields || data;
|
||||||
return {
|
return {
|
||||||
icon: fields.Icon,
|
icon: fields.Icon,
|
||||||
|
isUntradable: fields.IsUntradable,
|
||||||
itemId,
|
itemId,
|
||||||
itemLevel: fields.LevelItem,
|
itemLevel: fields.LevelItem,
|
||||||
name: fields.Name || `${itemId}`,
|
name: displayName || fields.Name || `${itemId}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -182,7 +235,7 @@ export class QqbotFf14ClientService {
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
`FF14 查价:${result.item.name}${quality}`,
|
`FF14 查价:${result.item.name}${quality}`,
|
||||||
`服务器:${result.world}`,
|
`查询范围:${result.world}`,
|
||||||
`最低价:${minPrice}`,
|
`最低价:${minPrice}`,
|
||||||
`均价:${averagePrice}`,
|
`均价:${averagePrice}`,
|
||||||
`近期挂单:`,
|
`近期挂单:`,
|
||||||
@ -209,13 +262,47 @@ export class QqbotFf14ClientService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private normalizeMarketPrice(
|
||||||
|
price: number | undefined,
|
||||||
|
listings: UniversalisListing[],
|
||||||
|
) {
|
||||||
|
if (!listings.length && (!price || price <= 0)) return undefined;
|
||||||
|
return price;
|
||||||
|
}
|
||||||
|
|
||||||
private normalizeWorld(world?: string) {
|
private normalizeWorld(world?: string) {
|
||||||
const raw =
|
const raw =
|
||||||
`${world || this.configService.get<string>('FF14_DEFAULT_WORLD') || '中国'}`.trim();
|
`${world || this.configService.get<string>('FF14_DEFAULT_WORLD') || '中国'}`.trim();
|
||||||
return encodeURIComponent(raw);
|
return raw;
|
||||||
}
|
}
|
||||||
|
|
||||||
private requestJson<T>(url: URL, method: HttpMethod) {
|
private resolveMarketTarget(params: {
|
||||||
|
dataCenter?: string;
|
||||||
|
region?: string;
|
||||||
|
world?: string;
|
||||||
|
}) {
|
||||||
|
return resolveQqbotFf14MarketTarget({
|
||||||
|
dataCenter: params.dataCenter,
|
||||||
|
fallback: this.normalizeWorld(params.world),
|
||||||
|
region: params.region,
|
||||||
|
world: params.world,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeXivapiLanguage(language?: string) {
|
||||||
|
const value = `${language || 'en'}`.trim().toLowerCase();
|
||||||
|
return ['en', 'ja', 'de', 'fr'].includes(value) ? value : 'en';
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolveCnItemAlias(keyword: string) {
|
||||||
|
return this.cnItemAliases.get(keyword.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
private escapeXivapiValue(value: string) {
|
||||||
|
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||||
|
}
|
||||||
|
|
||||||
|
private requestJson<T>(url: URL, method: HttpMethod, context: string) {
|
||||||
return new Promise<T>((resolve, reject) => {
|
return new Promise<T>((resolve, reject) => {
|
||||||
const client = url.protocol === 'http:' ? http : https;
|
const client = url.protocol === 'http:' ? http : https;
|
||||||
const request = client.request(
|
const request = client.request(
|
||||||
@ -236,7 +323,9 @@ export class QqbotFf14ClientService {
|
|||||||
});
|
});
|
||||||
response.on('end', () => {
|
response.on('end', () => {
|
||||||
if ((response.statusCode || 500) >= 400) {
|
if ((response.statusCode || 500) >= 400) {
|
||||||
reject(new Error(`FF14 接口请求失败:${response.statusCode}`));
|
reject(
|
||||||
|
new Error(`${context}失败:${response.statusCode}`),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -60,13 +60,15 @@ export class QqbotFf14MarketPluginService {
|
|||||||
description: '查询指定服务器的 FF14 市场最低价、均价与近期挂单。',
|
description: '查询指定服务器的 FF14 市场最低价、均价与近期挂单。',
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
properties: {
|
properties: {
|
||||||
|
dataCenter: { description: '大区名,如陆行鸟', type: 'string' },
|
||||||
hq: { description: '是否只查 HQ', type: 'boolean' },
|
hq: { description: '是否只查 HQ', type: 'boolean' },
|
||||||
item: { description: '物品名称或 ID', type: 'string' },
|
item: { description: '物品名称或 ID', type: 'string' },
|
||||||
itemId: { description: '物品 ID', type: 'number' },
|
itemId: { description: '物品 ID', type: 'number' },
|
||||||
language: { default: 'zh', type: 'string' },
|
language: { default: 'zh', type: 'string' },
|
||||||
world: { description: '服务器名或大区名', type: 'string' },
|
region: { description: '地区名,如中国', type: 'string' },
|
||||||
|
world: { description: '小区/服务器名,如红玉海', type: 'string' },
|
||||||
},
|
},
|
||||||
required: ['item', 'world'],
|
required: ['item'],
|
||||||
type: 'object',
|
type: 'object',
|
||||||
},
|
},
|
||||||
key: 'ff14.market.price',
|
key: 'ff14.market.price',
|
||||||
|
|||||||
205
src/qqbot/plugins/ff14Market/qqbot-ff14-worlds.ts
Normal file
205
src/qqbot/plugins/ff14Market/qqbot-ff14-worlds.ts
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
export type QqbotFf14DataCenter = {
|
||||||
|
name: string;
|
||||||
|
region: string;
|
||||||
|
worlds: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type QqbotFf14MarketTarget = {
|
||||||
|
dataCenter?: string;
|
||||||
|
label: string;
|
||||||
|
region?: string;
|
||||||
|
target: string;
|
||||||
|
world?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const QQBOT_FF14_DEFAULT_REGION = '中国';
|
||||||
|
|
||||||
|
export const QQBOT_FF14_CHINA_DATA_CENTERS: QqbotFf14DataCenter[] = [
|
||||||
|
{
|
||||||
|
name: '陆行鸟',
|
||||||
|
region: QQBOT_FF14_DEFAULT_REGION,
|
||||||
|
worlds: [
|
||||||
|
'红玉海',
|
||||||
|
'神意之地',
|
||||||
|
'拉诺西亚',
|
||||||
|
'幻影群岛',
|
||||||
|
'萌芽池',
|
||||||
|
'宇宙和音',
|
||||||
|
'沃仙曦染',
|
||||||
|
'晨曦王座',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '莫古力',
|
||||||
|
region: QQBOT_FF14_DEFAULT_REGION,
|
||||||
|
worlds: [
|
||||||
|
'白银乡',
|
||||||
|
'白金幻象',
|
||||||
|
'神拳痕',
|
||||||
|
'潮风亭',
|
||||||
|
'旅人栈桥',
|
||||||
|
'拂晓之间',
|
||||||
|
'龙巢神殿',
|
||||||
|
'梦羽宝境',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '猫小胖',
|
||||||
|
region: QQBOT_FF14_DEFAULT_REGION,
|
||||||
|
worlds: [
|
||||||
|
'紫水栈桥',
|
||||||
|
'延夏',
|
||||||
|
'静语庄园',
|
||||||
|
'摩杜纳',
|
||||||
|
'海猫茶屋',
|
||||||
|
'柔风海湾',
|
||||||
|
'琥珀原',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '豆豆柴',
|
||||||
|
region: QQBOT_FF14_DEFAULT_REGION,
|
||||||
|
worlds: ['水晶塔', '银泪湖', '太阳海岸', '伊修加德', '红茶川'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function isQqbotFf14DataCenterName(value?: string) {
|
||||||
|
const name = normalizeQqbotFf14WorldValue(value);
|
||||||
|
return QQBOT_FF14_CHINA_DATA_CENTERS.some((item) => item.name === name);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isQqbotFf14RegionName(value?: string) {
|
||||||
|
return normalizeQqbotFf14WorldValue(value) === QQBOT_FF14_DEFAULT_REGION;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isQqbotFf14WorldName(value?: string) {
|
||||||
|
const name = normalizeQqbotFf14WorldValue(value);
|
||||||
|
return QQBOT_FF14_CHINA_DATA_CENTERS.some((item) =>
|
||||||
|
item.worlds.includes(name),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isQqbotFf14LocationName(value?: string) {
|
||||||
|
const name = normalizeQqbotFf14WorldValue(value);
|
||||||
|
const path = splitQqbotFf14WorldPath(name);
|
||||||
|
return (
|
||||||
|
isQqbotFf14RegionName(name) ||
|
||||||
|
isQqbotFf14DataCenterName(name) ||
|
||||||
|
isQqbotFf14WorldName(name) ||
|
||||||
|
(!!path.dataCenter && !!path.world)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function splitQqbotFf14WorldPath(value?: string) {
|
||||||
|
const raw = normalizeQqbotFf14WorldValue(value);
|
||||||
|
if (!raw) return {};
|
||||||
|
|
||||||
|
const parts = raw
|
||||||
|
.split(/\s*(?:->|=>|>|\/|\\|:|:)\s*/)
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
if (parts.length < 2) return {};
|
||||||
|
|
||||||
|
if (parts.length === 2) {
|
||||||
|
return {
|
||||||
|
dataCenter: parts[0],
|
||||||
|
world: parts[1],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
dataCenter: parts[parts.length - 2],
|
||||||
|
region: parts[0],
|
||||||
|
world: parts[parts.length - 1],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findQqbotFf14DataCenterByWorld(world?: string) {
|
||||||
|
const worldName = normalizeQqbotFf14WorldValue(world);
|
||||||
|
return QQBOT_FF14_CHINA_DATA_CENTERS.find((item) =>
|
||||||
|
item.worlds.includes(worldName),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveQqbotFf14MarketTarget(params: {
|
||||||
|
dataCenter?: string;
|
||||||
|
fallback?: string;
|
||||||
|
region?: string;
|
||||||
|
world?: string;
|
||||||
|
}): QqbotFf14MarketTarget {
|
||||||
|
const fallback = normalizeQqbotFf14WorldValue(params.fallback);
|
||||||
|
const path = splitQqbotFf14WorldPath(params.world);
|
||||||
|
const region = normalizeQqbotFf14WorldValue(params.region || path.region);
|
||||||
|
const dataCenter = normalizeQqbotFf14WorldValue(
|
||||||
|
params.dataCenter || path.dataCenter,
|
||||||
|
);
|
||||||
|
const rawWorld = normalizeQqbotFf14WorldValue(path.world || params.world);
|
||||||
|
const world =
|
||||||
|
dataCenter && rawWorld === QQBOT_FF14_DEFAULT_REGION ? '' : rawWorld;
|
||||||
|
const raw =
|
||||||
|
world || dataCenter || region || fallback || QQBOT_FF14_DEFAULT_REGION;
|
||||||
|
|
||||||
|
if (region && dataCenter && (!world || world === region)) {
|
||||||
|
return {
|
||||||
|
dataCenter,
|
||||||
|
label: `${region} / ${dataCenter}`,
|
||||||
|
region,
|
||||||
|
target: dataCenter,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (raw === QQBOT_FF14_DEFAULT_REGION) {
|
||||||
|
return {
|
||||||
|
label: QQBOT_FF14_DEFAULT_REGION,
|
||||||
|
region: QQBOT_FF14_DEFAULT_REGION,
|
||||||
|
target: QQBOT_FF14_DEFAULT_REGION,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dataCenter && world && world !== dataCenter) {
|
||||||
|
const matchedDataCenter = QQBOT_FF14_CHINA_DATA_CENTERS.find(
|
||||||
|
(item) => item.name === dataCenter,
|
||||||
|
);
|
||||||
|
if (matchedDataCenter && !matchedDataCenter.worlds.includes(world)) {
|
||||||
|
throw new Error(`服务器 ${world} 不属于大区 ${dataCenter}`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
dataCenter,
|
||||||
|
label: region
|
||||||
|
? `${region} / ${dataCenter} / ${world}`
|
||||||
|
: `${dataCenter} / ${world}`,
|
||||||
|
region,
|
||||||
|
target: world,
|
||||||
|
world,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchedWorldDataCenter = findQqbotFf14DataCenterByWorld(raw);
|
||||||
|
if (matchedWorldDataCenter) {
|
||||||
|
return {
|
||||||
|
dataCenter: matchedWorldDataCenter.name,
|
||||||
|
label: `${matchedWorldDataCenter.region} / ${matchedWorldDataCenter.name} / ${raw}`,
|
||||||
|
region: matchedWorldDataCenter.region,
|
||||||
|
target: raw,
|
||||||
|
world: raw,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isQqbotFf14DataCenterName(raw)) {
|
||||||
|
return {
|
||||||
|
dataCenter: raw,
|
||||||
|
label: `${QQBOT_FF14_DEFAULT_REGION} / ${raw}`,
|
||||||
|
region: QQBOT_FF14_DEFAULT_REGION,
|
||||||
|
target: raw,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
label: raw,
|
||||||
|
target: raw,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeQqbotFf14WorldValue(value?: string) {
|
||||||
|
return `${value || ''}`.trim();
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user