feat: add epic games package archiving support

This commit is contained in:
daydreamer-json
2026-01-28 12:56:25 +09:00
parent 1b0f1cfa97
commit 96563865a9
20 changed files with 1442 additions and 662 deletions

View File

@@ -79,112 +79,149 @@ async function mainCmdHandler() {
const cfg = appConfig.network.api.akEndfield;
const channelStr = String(cfg.channel.osWinRel);
await (async () => {
logger.debug('Fetching latestGame ...');
const rsp = await apiUtils.apiAkEndfield.launcher.latestGame(
cfg.appCode.game.osWinRel,
cfg.appCode.launcher.osWinRel,
cfg.channel.osWinRel,
cfg.channel.osWinRel,
cfg.channel.osWinRel,
null,
);
logger.info(
`Fetched latestGame: v${rsp.version}, ${mathUtils.formatFileSize(
parseInt(rsp.pkg.total_size) - mathUtils.arrayTotal(rsp.pkg.packs.map((e) => parseInt(e.package_size))),
{
decimals: 2,
decimalPadding: true,
useBinaryUnit: true,
useBitUnit: false,
unitVisible: true,
unit: null,
},
)}`,
);
const prettyRsp = {
req: {
appCode: cfg.appCode.game.osWinRel,
launcherAppCode: cfg.appCode.launcher.osWinRel,
channel: cfg.channel.osWinRel,
subChannel: cfg.channel.osWinRel,
launcherSubChannel: cfg.channel.osWinRel,
},
rsp,
};
const gameTargets = [
{
name: 'Official',
launcherAppCode: cfg.appCode.launcher.osWinRel,
subChannel: cfg.channel.osWinRel,
launcherSubChannel: cfg.channel.osWinRel,
dirName: String(cfg.channel.osWinRel),
},
{
name: 'Epic',
launcherAppCode: cfg.appCode.launcher.osWinRelEpic,
subChannel: cfg.subChannel.osWinRelEpic,
launcherSubChannel: cfg.subChannel.osWinRelEpic,
dirName: String(cfg.subChannel.osWinRelEpic),
},
];
await saveResult(['akEndfield', 'launcher', 'game', channelStr], rsp.version, prettyRsp);
})();
await (async () => {
logger.debug('Fetching latestGame (patch) ...');
const gameAllJson = await Bun.file(
path.join(argvUtils.getArgv()['outputDir'], 'akEndfield', 'launcher', 'game', channelStr, 'all.json'),
).json();
const patchAllJson = await Bun.file(
path.join(argvUtils.getArgv()['outputDir'], 'akEndfield', 'launcher', 'game', channelStr, 'all_patch.json'),
).json();
const versionList = ([...new Set(gameAllJson.map((e: any) => e.rsp.version))] as string[])
.sort((a, b) => semver.compare(b, a))
.slice(1);
let needWrite: boolean = false;
const queue = new PQueue({ concurrency: appConfig.threadCount.network });
for (const ver of versionList) {
queue.add(async () => {
const rsp = await apiUtils.apiAkEndfield.launcher.latestGame(
cfg.appCode.game.osWinRel,
cfg.appCode.launcher.osWinRel,
cfg.channel.osWinRel,
cfg.channel.osWinRel,
cfg.channel.osWinRel,
ver,
);
const prettyRsp = {
req: {
appCode: cfg.appCode.game.osWinRel,
launcherAppCode: cfg.appCode.launcher.osWinRel,
channel: cfg.channel.osWinRel,
subChannel: cfg.channel.osWinRel,
launcherSubChannel: cfg.channel.osWinRel,
version: ver,
},
rsp,
};
if (rsp.patch === null) return;
if (
patchAllJson
.map((e: any) => JSON.stringify({ req: e.req, rsp: e.rsp }))
.includes(JSON.stringify(prettyRsp)) === false
) {
logger.debug(
`Fetched latestGame (patch): v${rsp.request_version} -> v${rsp.version}, ${mathUtils.formatFileSize(
parseInt(rsp.patch.total_size) - parseInt(rsp.patch.package_size),
{
decimals: 2,
decimalPadding: true,
unitVisible: true,
useBinaryUnit: true,
useBitUnit: false,
unit: null,
},
)}`,
);
patchAllJson.push({
updatedAt: DateTime.now().toISO(),
...prettyRsp,
});
needWrite = true;
}
});
}
await queue.onIdle();
if (needWrite) {
await Bun.write(
path.join(argvUtils.getArgv()['outputDir'], 'akEndfield', 'launcher', 'game', channelStr, 'all_patch.json'),
JSON.stringify(patchAllJson, null, 2),
for (const target of gameTargets) {
await (async () => {
logger.debug(`Fetching latestGame (${target.name}) ...`);
const rsp = await apiUtils.apiAkEndfield.launcher.latestGame(
cfg.appCode.game.osWinRel,
target.launcherAppCode,
cfg.channel.osWinRel,
target.subChannel,
target.launcherSubChannel,
null,
);
}
})();
logger.info(
`Fetched latestGame (${target.name}): v${rsp.version}, ${mathUtils.formatFileSize(
parseInt(rsp.pkg.total_size) - mathUtils.arrayTotal(rsp.pkg.packs.map((e) => parseInt(e.package_size))),
{
decimals: 2,
decimalPadding: true,
useBinaryUnit: true,
useBitUnit: false,
unitVisible: true,
unit: null,
},
)}`,
);
const prettyRsp = {
req: {
appCode: cfg.appCode.game.osWinRel,
launcherAppCode: target.launcherAppCode,
channel: cfg.channel.osWinRel,
subChannel: target.subChannel,
launcherSubChannel: target.launcherSubChannel,
},
rsp,
};
await saveResult(['akEndfield', 'launcher', 'game', target.dirName], rsp.version, prettyRsp);
})();
}
for (const target of gameTargets) {
await (async () => {
logger.debug(`Fetching latestGame (patch) (${target.name}) ...`);
const gameAllJsonPath = path.join(
argvUtils.getArgv()['outputDir'],
'akEndfield',
'launcher',
'game',
target.dirName,
'all.json',
);
const patchAllJsonPath = path.join(
argvUtils.getArgv()['outputDir'],
'akEndfield',
'launcher',
'game',
target.dirName,
'all_patch.json',
);
if (!(await Bun.file(gameAllJsonPath).exists())) return;
const gameAllJson = await Bun.file(gameAllJsonPath).json();
let patchAllJson: any[] = [];
if (await Bun.file(patchAllJsonPath).exists()) {
patchAllJson = await Bun.file(patchAllJsonPath).json();
}
const versionList = ([...new Set(gameAllJson.map((e: any) => e.rsp.version))] as string[])
.sort((a, b) => semver.compare(b, a))
.slice(1);
let needWrite: boolean = false;
const queue = new PQueue({ concurrency: appConfig.threadCount.network });
for (const ver of versionList) {
queue.add(async () => {
const rsp = await apiUtils.apiAkEndfield.launcher.latestGame(
cfg.appCode.game.osWinRel,
target.launcherAppCode,
cfg.channel.osWinRel,
target.subChannel,
target.launcherSubChannel,
ver,
);
const prettyRsp = {
req: {
appCode: cfg.appCode.game.osWinRel,
launcherAppCode: target.launcherAppCode,
channel: cfg.channel.osWinRel,
subChannel: target.subChannel,
launcherSubChannel: target.launcherSubChannel,
version: ver,
},
rsp,
};
if (rsp.patch === null) return;
if (
patchAllJson
.map((e: any) => JSON.stringify({ req: e.req, rsp: e.rsp }))
.includes(JSON.stringify(prettyRsp)) === false
) {
logger.debug(
`Fetched latestGame (patch) (${target.name}): v${rsp.request_version} -> v${rsp.version}, ${mathUtils.formatFileSize(
parseInt(rsp.patch.total_size) - parseInt(rsp.patch.package_size),
{
decimals: 2,
decimalPadding: true,
unitVisible: true,
useBinaryUnit: true,
useBitUnit: false,
unit: null,
},
)}`,
);
patchAllJson.push({
updatedAt: DateTime.now().toISO(),
...prettyRsp,
});
needWrite = true;
}
});
}
await queue.onIdle();
if (needWrite) {
await Bun.write(patchAllJsonPath, JSON.stringify(patchAllJson, null, 2));
}
})();
}
await (async () => {
logger.debug('Fetching latestGameRes ...');
@@ -283,103 +320,126 @@ async function mainCmdHandler() {
await (async () => {
//* Markdown generate
await (async () => {
const gameAllJson = await Bun.file(
path.join(argvUtils.getArgv()['outputDir'], 'akEndfield', 'launcher', 'game', channelStr, 'all.json'),
).json();
const mdTexts: string[] = [];
mdTexts.push(
...[
'# Game Packages\n',
...gameAllJson.map(
(e: any) =>
`- [${e.rsp.version} (${DateTime.fromISO(e.updatedAt, { setZone: true }).setZone('UTC+8').toFormat('yyyy/MM/dd HH:mm:ss')})](#ver-${e.rsp.version}-${Math.ceil(DateTime.fromISO(e.updatedAt).toSeconds())})`,
),
'',
],
...gameAllJson.map((e: any) =>
[
`<h2 id="ver-${e.rsp.version}-${Math.ceil(DateTime.fromISO(e.updatedAt).toSeconds())}">${e.rsp.version} (${DateTime.fromISO(e.updatedAt, { setZone: true }).setZone('UTC+8').toFormat('yyyy/MM/dd HH:mm:ss')})</h2>\n`,
`<table>`,
` <tr><td>Unpacked Size</td><td style="text-align: right;"><b>${mathUtils.formatFileSize(
e.rsp.pkg.total_size - mathUtils.arrayTotal(e.rsp.pkg.packs.map((f: any) => parseInt(f.package_size))),
{
decimals: 2,
decimalPadding: true,
unitVisible: true,
useBinaryUnit: true,
useBitUnit: false,
unit: null,
},
)}</b></td></tr>`,
` <tr><td>Packed Size</td><td style="text-align: right;"><b>${mathUtils.formatFileSize(mathUtils.arrayTotal(e.rsp.pkg.packs.map((f: any) => parseInt(f.package_size))), { decimals: 2, decimalPadding: true, unitVisible: true, useBinaryUnit: true, useBitUnit: false, unit: null })}</b></td></tr>`,
`</table>\n`,
`|File|MD5 Checksum|Size|`,
`|:--|:--|--:|`,
...e.rsp.pkg.packs.map((f: any) => [
`|[${new URL(f.url).pathname.split('/').pop() ?? ''}](${f.url})|\`${f.md5}\`|${mathUtils.formatFileSize(parseInt(f.package_size), { decimals: 2, decimalPadding: true, unitVisible: true, useBinaryUnit: true, useBitUnit: false, unit: null })}|`,
]),
for (const target of gameTargets) {
await (async () => {
const gameAllJsonPath = path.join(
argvUtils.getArgv()['outputDir'],
'akEndfield',
'launcher',
'game',
target.dirName,
'all.json',
);
if (!(await Bun.file(gameAllJsonPath).exists())) return;
const gameAllJson = await Bun.file(gameAllJsonPath).json();
const mdTexts: string[] = [];
mdTexts.push(
...[
`# Game Packages (${target.name})\n`,
...gameAllJson.map(
(e: any) =>
`- [${e.rsp.version} (${DateTime.fromISO(e.updatedAt, { setZone: true }).setZone('UTC+8').toFormat('yyyy/MM/dd HH:mm:ss')})](#ver-${e.rsp.version}-${Math.ceil(DateTime.fromISO(e.updatedAt).toSeconds())})`,
),
'',
].join('\n'),
),
);
await Bun.write(
path.join(argvUtils.getArgv()['outputDir'], 'akEndfield', 'launcher', 'game', channelStr, 'list.md'),
mdTexts.join('\n'),
);
})();
],
...gameAllJson.map((e: any) =>
[
`<h2 id="ver-${e.rsp.version}-${Math.ceil(DateTime.fromISO(e.updatedAt).toSeconds())}">${e.rsp.version} (${DateTime.fromISO(e.updatedAt, { setZone: true }).setZone('UTC+8').toFormat('yyyy/MM/dd HH:mm:ss')})</h2>\n`,
`<table>`,
` <tr><td>Unpacked Size</td><td style="text-align: right;"><b>${mathUtils.formatFileSize(
e.rsp.pkg.total_size - mathUtils.arrayTotal(e.rsp.pkg.packs.map((f: any) => parseInt(f.package_size))),
{
decimals: 2,
decimalPadding: true,
unitVisible: true,
useBinaryUnit: true,
useBitUnit: false,
unit: null,
},
)}</b></td></tr>`,
` <tr><td>Packed Size</td><td style="text-align: right;"><b>${mathUtils.formatFileSize(mathUtils.arrayTotal(e.rsp.pkg.packs.map((f: any) => parseInt(f.package_size))), { decimals: 2, decimalPadding: true, unitVisible: true, useBinaryUnit: true, useBitUnit: false, unit: null })}</b></td></tr>`,
`</table>\n`,
`|File|MD5 Checksum|Size|`,
`|:--|:--|--:|`,
...e.rsp.pkg.packs.map((f: any) => [
`|[${new URL(f.url).pathname.split('/').pop() ?? ''}](${f.url})|\`${f.md5}\`|${mathUtils.formatFileSize(parseInt(f.package_size), { decimals: 2, decimalPadding: true, unitVisible: true, useBinaryUnit: true, useBitUnit: false, unit: null })}|`,
]),
'',
].join('\n'),
),
);
await Bun.write(
path.join(argvUtils.getArgv()['outputDir'], 'akEndfield', 'launcher', 'game', target.dirName, 'list.md'),
mdTexts.join('\n'),
);
})();
await (async () => {
const gameAllJson = await Bun.file(
path.join(argvUtils.getArgv()['outputDir'], 'akEndfield', 'launcher', 'game', channelStr, 'all_patch.json'),
).json();
const mdTexts: string[] = [];
mdTexts.push(
...[
'# Game Patch Packages\n',
...gameAllJson.map(
(e: any) =>
`- [${e.rsp.request_version}${e.rsp.version} (${DateTime.fromISO(e.updatedAt, { setZone: true }).setZone('UTC+8').toFormat('yyyy/MM/dd HH:mm:ss')})](#ver-${e.rsp.request_version}-${e.rsp.version}-${Math.ceil(DateTime.fromISO(e.updatedAt).toSeconds())})`,
),
'',
],
...gameAllJson.map((e: any) =>
[
`<h2 id="ver-${e.rsp.request_version}-${e.rsp.version}-${Math.ceil(DateTime.fromISO(e.updatedAt).toSeconds())}">${e.rsp.request_version}${e.rsp.version} (${DateTime.fromISO(e.updatedAt, { setZone: true }).setZone('UTC+8').toFormat('yyyy/MM/dd HH:mm:ss')})</h2>\n`,
`<table>`,
` <tr><td>Unpacked Size</td><td style="text-align: right;"><b>${mathUtils.formatFileSize(
e.rsp.patch.total_size -
mathUtils.arrayTotal(e.rsp.patch.patches.map((f: any) => parseInt(f.package_size))),
{
decimals: 2,
decimalPadding: true,
unitVisible: true,
useBinaryUnit: true,
useBitUnit: false,
unit: null,
},
)}</b></td></tr>`,
` <tr><td>Packed Size</td><td style="text-align: right;"><b>${mathUtils.formatFileSize(mathUtils.arrayTotal(e.rsp.patch.patches.map((f: any) => parseInt(f.package_size))), { decimals: 2, decimalPadding: true, unitVisible: true, useBinaryUnit: true, useBitUnit: false, unit: null })}</b></td></tr>`,
`</table>\n`,
`|File|MD5 Checksum|Size|`,
`|:--|:--|--:|`,
...(e.rsp.patch.url
? [
`|[${new URL(e.rsp.patch.url).pathname.split('/').pop() ?? ''}](${e.rsp.patch.url})|\`${e.rsp.patch.md5}\`|${mathUtils.formatFileSize(parseInt(e.rsp.patch.package_size), { decimals: 2, decimalPadding: true, unitVisible: true, useBinaryUnit: true, useBitUnit: false, unit: null })}|`,
]
: []),
...e.rsp.patch.patches.map((f: any) => [
`|[${new URL(f.url).pathname.split('/').pop() ?? ''}](${f.url})|\`${f.md5}\`|${mathUtils.formatFileSize(parseInt(f.package_size), { decimals: 2, decimalPadding: true, unitVisible: true, useBinaryUnit: true, useBitUnit: false, unit: null })}|`,
]),
await (async () => {
const patchAllJsonPath = path.join(
argvUtils.getArgv()['outputDir'],
'akEndfield',
'launcher',
'game',
target.dirName,
'all_patch.json',
);
if (!(await Bun.file(patchAllJsonPath).exists())) return;
const gameAllJson = await Bun.file(patchAllJsonPath).json();
const mdTexts: string[] = [];
mdTexts.push(
...[
`# Game Patch Packages (${target.name})\n`,
...gameAllJson.map(
(e: any) =>
`- [${e.rsp.request_version}${e.rsp.version} (${DateTime.fromISO(e.updatedAt, { setZone: true }).setZone('UTC+8').toFormat('yyyy/MM/dd HH:mm:ss')})](#ver-${e.rsp.request_version}-${e.rsp.version}-${Math.ceil(DateTime.fromISO(e.updatedAt).toSeconds())})`,
),
'',
].join('\n'),
),
);
await Bun.write(
path.join(argvUtils.getArgv()['outputDir'], 'akEndfield', 'launcher', 'game', channelStr, 'list_patch.md'),
mdTexts.join('\n'),
);
})();
],
...gameAllJson.map((e: any) =>
[
`<h2 id="ver-${e.rsp.request_version}-${e.rsp.version}-${Math.ceil(DateTime.fromISO(e.updatedAt).toSeconds())}">${e.rsp.request_version}${e.rsp.version} (${DateTime.fromISO(e.updatedAt, { setZone: true }).setZone('UTC+8').toFormat('yyyy/MM/dd HH:mm:ss')})</h2>\n`,
`<table>`,
` <tr><td>Unpacked Size</td><td style="text-align: right;"><b>${mathUtils.formatFileSize(
e.rsp.patch.total_size -
mathUtils.arrayTotal(e.rsp.patch.patches.map((f: any) => parseInt(f.package_size))),
{
decimals: 2,
decimalPadding: true,
unitVisible: true,
useBinaryUnit: true,
useBitUnit: false,
unit: null,
},
)}</b></td></tr>`,
` <tr><td>Packed Size</td><td style="text-align: right;"><b>${mathUtils.formatFileSize(mathUtils.arrayTotal(e.rsp.patch.patches.map((f: any) => parseInt(f.package_size))), { decimals: 2, decimalPadding: true, unitVisible: true, useBinaryUnit: true, useBitUnit: false, unit: null })}</b></td></tr>`,
`</table>\n`,
`|File|MD5 Checksum|Size|`,
`|:--|:--|--:|`,
...(e.rsp.patch.url
? [
`|[${new URL(e.rsp.patch.url).pathname.split('/').pop() ?? ''}](${e.rsp.patch.url})|\`${e.rsp.patch.md5}\`|${mathUtils.formatFileSize(parseInt(e.rsp.patch.package_size), { decimals: 2, decimalPadding: true, unitVisible: true, useBinaryUnit: true, useBitUnit: false, unit: null })}|`,
]
: []),
...e.rsp.patch.patches.map((f: any) => [
`|[${new URL(f.url).pathname.split('/').pop() ?? ''}](${f.url})|\`${f.md5}\`|${mathUtils.formatFileSize(parseInt(f.package_size), { decimals: 2, decimalPadding: true, unitVisible: true, useBinaryUnit: true, useBitUnit: false, unit: null })}|`,
]),
'',
].join('\n'),
),
);
await Bun.write(
path.join(
argvUtils.getArgv()['outputDir'],
'akEndfield',
'launcher',
'game',
target.dirName,
'list_patch.md',
),
mdTexts.join('\n'),
);
})();
}
await (async () => {
const mdTexts: string[] = [];

View File

@@ -1,461 +1,9 @@
import ky from 'ky';
import semver from 'semver';
import * as TypesApiAkEndfield from '../types/api/akEndfield/Api.js';
import appConfig from './config.js';
const defaultKySettings = {
headers: {
'User-Agent': appConfig.network.userAgent.minimum,
},
timeout: appConfig.network.timeout,
retry: { limit: appConfig.network.retryCount },
};
const launcherWebLang = [
'de-de',
'en-us',
'es-mx',
'fr-fr',
'id-id',
'it-it',
'ja-jp',
'ko-kr',
'pt-br',
'ru-ru',
'th-th',
'vi-vn',
'zh-cn',
'zh-tw',
] as const;
import accountService from './api/accountService.js';
import binding from './api/binding.js';
import launcher from './api/launcher.js';
import u8 from './api/u8.js';
import webview from './api/webview.js';
export default {
defaultKySettings,
apiAkEndfield: {
launcher: {
latestGame: async (
appCode: string,
launcherAppCode: string,
channel: number,
subChannel: number,
launcherSubChannel: number,
version: string | null,
): Promise<TypesApiAkEndfield.LauncherLatestGame> => {
if (version !== null && !semver.valid(version)) throw new Error(`Invalid version string (${version})`);
const rsp = await ky
.get(`https://${appConfig.network.api.akEndfield.base.launcher}/game/get_latest`, {
...defaultKySettings,
searchParams: {
appcode: appCode,
launcher_appcode: launcherAppCode,
channel,
sub_channel: subChannel,
launcher_sub_channel: launcherSubChannel,
version: version ?? undefined,
},
})
.json();
return rsp as TypesApiAkEndfield.LauncherLatestGame;
},
latestGameResources: async (
appCode: string,
gameVersion: string, // example: 1.0
version: string,
randStr: string,
platform: 'Windows' | 'Android' | 'iOS' | 'PlayStation',
): Promise<TypesApiAkEndfield.LauncherLatestGameResources> => {
if (!semver.valid(version)) throw new Error(`Invalid version string (${version})`);
const rsp = await ky
.get(`https://${appConfig.network.api.akEndfield.base.launcher}/game/get_latest_resources`, {
...defaultKySettings,
searchParams: {
appcode: appCode,
game_version: gameVersion,
version: version,
platform,
rand_str: randStr,
},
})
.json();
return rsp as TypesApiAkEndfield.LauncherLatestGameResources;
},
latestLauncher: async (
appCode: string,
channel: number,
subChannel: number,
version: string | null,
targetApp: 'EndField' | null,
): Promise<TypesApiAkEndfield.LauncherLatestLauncher> => {
if (version !== null && !semver.valid(version)) throw new Error(`Invalid version string (${version})`);
const rsp = await ky
.get(`https://${appConfig.network.api.akEndfield.base.launcher}/launcher/get_latest`, {
...defaultKySettings,
searchParams: {
appcode: appCode,
channel,
sub_channel: subChannel,
version: version ?? undefined,
targetApp: targetApp ?? undefined,
},
})
.json();
return rsp as TypesApiAkEndfield.LauncherLatestLauncher;
},
web: {
sidebar: async (
appCode: string,
channel: number,
subChannel: number,
language: (typeof launcherWebLang)[number],
platform: 'Windows' = 'Windows',
): Promise<TypesApiAkEndfield.LauncherWebSidebar> => {
const rsp = await ky
.post(`https://${appConfig.network.api.akEndfield.base.launcher}/proxy/web/batch_proxy`, {
...defaultKySettings,
json: {
proxy_reqs: [
{
kind: 'get_sidebar',
get_sidebar_req: {
appcode: appCode,
channel: String(channel),
sub_channel: String(subChannel),
language,
platform,
source: 'launcher',
},
},
],
},
})
.json();
return (rsp as any).proxy_rsps[0].get_sidebar_rsp as TypesApiAkEndfield.LauncherWebSidebar;
},
singleEnt: async (
appCode: string,
channel: number,
subChannel: number,
language: (typeof launcherWebLang)[number],
platform: 'Windows' = 'Windows',
): Promise<TypesApiAkEndfield.LauncherWebSingleEnt> => {
const rsp = await ky
.post(`https://${appConfig.network.api.akEndfield.base.launcher}/proxy/web/batch_proxy`, {
...defaultKySettings,
json: {
proxy_reqs: [
{
kind: 'get_single_ent',
get_single_ent_req: {
appcode: appCode,
channel: String(channel),
sub_channel: String(subChannel),
language,
platform,
source: 'launcher',
},
},
],
},
})
.json();
return (rsp as any).proxy_rsps[0].get_single_ent_rsp as TypesApiAkEndfield.LauncherWebSingleEnt;
},
mainBgImage: async (
appCode: string,
channel: number,
subChannel: number,
language: (typeof launcherWebLang)[number],
platform: 'Windows' = 'Windows',
): Promise<TypesApiAkEndfield.LauncherWebMainBgImage> => {
const rsp = await ky
.post(`https://${appConfig.network.api.akEndfield.base.launcher}/proxy/web/batch_proxy`, {
...defaultKySettings,
json: {
proxy_reqs: [
{
kind: 'get_main_bg_image',
get_main_bg_image_req: {
appcode: appCode,
channel: String(channel),
sub_channel: String(subChannel),
language,
platform,
source: 'launcher',
},
},
],
},
})
.json();
return (rsp as any).proxy_rsps[0].get_main_bg_image_rsp as TypesApiAkEndfield.LauncherWebMainBgImage;
},
banner: async (
appCode: string,
channel: number,
subChannel: number,
language: (typeof launcherWebLang)[number],
platform: 'Windows' = 'Windows',
): Promise<TypesApiAkEndfield.LauncherWebBanner> => {
const rsp = await ky
.post(`https://${appConfig.network.api.akEndfield.base.launcher}/proxy/web/batch_proxy`, {
...defaultKySettings,
json: {
proxy_reqs: [
{
kind: 'get_banner',
get_banner_req: {
appcode: appCode,
channel: String(channel),
sub_channel: String(subChannel),
language,
platform,
source: 'launcher',
},
},
],
},
})
.json();
return (rsp as any).proxy_rsps[0].get_banner_rsp as TypesApiAkEndfield.LauncherWebBanner;
},
announcement: async (
appCode: string,
channel: number,
subChannel: number,
language: (typeof launcherWebLang)[number],
platform: 'Windows' = 'Windows',
): Promise<TypesApiAkEndfield.LauncherWebAnnouncement> => {
const rsp = await ky
.post(`https://${appConfig.network.api.akEndfield.base.launcher}/proxy/web/batch_proxy`, {
...defaultKySettings,
json: {
proxy_reqs: [
{
kind: 'get_announcement',
get_announcement_req: {
appcode: appCode,
channel: String(channel),
sub_channel: String(subChannel),
language,
platform,
source: 'launcher',
},
},
],
},
})
.json();
return (rsp as any).proxy_rsps[0].get_announcement_rsp as TypesApiAkEndfield.LauncherWebAnnouncement;
},
},
},
accountService: {
user: {
auth: {
v1: {
tokenByEmailPassword: async (
email: string,
password: string,
from: number = 0,
): Promise<TypesApiAkEndfield.AccSrvUserAuthV1TokenByEmail> => {
const rsp = await ky
.post(
`https://${appConfig.network.api.akEndfield.base.accountService}/user/auth/v1/token_by_email_password`,
{ ...defaultKySettings, json: { email, from, password } },
)
.json();
return rsp as TypesApiAkEndfield.AccSrvUserAuthV1TokenByEmail;
},
},
},
oauth2: {
v2: {
grant: async <T extends 0 | 1 = 0>(
appCode: string,
token: string,
type: T = 0 as any, // 0 = return grant uid (Gxxxxxxxxx) and code, 1 = return hgId and token
): Promise<
T extends 0 ? TypesApiAkEndfield.AccSrvUserOAuth2V2Grant : TypesApiAkEndfield.AccSrvUserOAuth2V2GrantType1
> => {
const rsp = await ky
.post(`https://${appConfig.network.api.akEndfield.base.accountService}/user/oauth2/v2/grant`, {
...defaultKySettings,
json: { appCode, token, type },
})
.json();
return rsp as any;
},
},
},
info: {
v1: {
basic: async (appCode: string, token: string): Promise<TypesApiAkEndfield.AccSrvUserInfoV1Basic> => {
const rsp = await ky
.get(`https://${appConfig.network.api.akEndfield.base.accountService}/user/info/v1/basic`, {
...defaultKySettings,
searchParams: { appCode, token },
})
.json();
return rsp as TypesApiAkEndfield.AccSrvUserInfoV1Basic;
},
thirdParty: async (
appCode: string,
token: string,
): Promise<TypesApiAkEndfield.AccSrvUserInfoV1ThirdParty> => {
const rsp = await ky
.get(`https://${appConfig.network.api.akEndfield.base.accountService}/user/info/v1/third_party`, {
...defaultKySettings,
searchParams: { appCode, token },
})
.json();
return rsp as TypesApiAkEndfield.AccSrvUserInfoV1ThirdParty;
},
},
},
},
},
u8: {
user: {
auth: {
v2: {
tokenByChToken: async (
appCode: string,
channelMasterId: number,
channelToken: string,
platform: number = 2,
type: number = 0,
): Promise<TypesApiAkEndfield.U8UserAuthV2ChToken> => {
const rsp = await ky
.post(`https://${appConfig.network.api.akEndfield.base.u8}/u8/user/auth/v2/token_by_channel_token`, {
...defaultKySettings,
json: {
appCode,
channelMasterId,
channelToken: JSON.stringify({
code: channelToken,
type: 1,
isSuc: true,
}),
type,
platform,
},
})
.json();
return rsp as TypesApiAkEndfield.U8UserAuthV2ChToken;
},
grant: async (
token: string,
platform: number = 2,
type: number = 0,
): Promise<TypesApiAkEndfield.U8UserAuthV2Grant> => {
const rsp = await ky
.post(`https://${appConfig.network.api.akEndfield.base.u8}/u8/user/auth/v2/grant`, {
...defaultKySettings,
json: { token, type, platform },
})
.json();
return rsp as TypesApiAkEndfield.U8UserAuthV2Grant;
},
},
},
},
game: {
server: {
v1: {
serverList: async (token: string): Promise<TypesApiAkEndfield.U8GameServerV1ServerList> => {
const rsp = await ky
.post(`https://${appConfig.network.api.akEndfield.base.u8}/game/server/v1/server_list`, {
...defaultKySettings,
json: { token },
})
.json();
return rsp as TypesApiAkEndfield.U8GameServerV1ServerList;
},
},
},
role: {
v1: {
confirmServer: async (
token: string,
serverId: number,
): Promise<TypesApiAkEndfield.U8GameRoleV1ConfirmServer> => {
const rsp = await ky
.post(`https://${appConfig.network.api.akEndfield.base.u8}/game/role/v1/confirm_server`, {
...defaultKySettings,
json: { token, serverId: String(serverId) },
})
.json();
return rsp as TypesApiAkEndfield.U8GameRoleV1ConfirmServer;
},
},
},
},
},
binding: {
account: {
binding: {
v1: {
bindingList: async (
// appCode: 'arknights' | 'endfield',
token: string,
): Promise<TypesApiAkEndfield.BindApiAccBindV1BindList> => {
const rsp = await ky
.get(`https://${appConfig.network.api.akEndfield.base.binding}/account/binding/v1/binding_list`, {
...defaultKySettings,
searchParams: { token },
})
.json();
return rsp as TypesApiAkEndfield.BindApiAccBindV1BindList;
},
u8TokenByUid: async (
token: string,
uid: string,
): Promise<TypesApiAkEndfield.BindApiAccBindV1U8TokenByUid> => {
const rsp = await ky
.post(`https://${appConfig.network.api.akEndfield.base.binding}/account/binding/v1/u8_token_by_uid`, {
...defaultKySettings,
json: { token, uid },
})
.json();
return rsp as TypesApiAkEndfield.BindApiAccBindV1U8TokenByUid;
},
},
},
},
},
webview: {
record: {
char: async (
token: string,
serverId: number, // 2 or 3
poolType:
| 'E_CharacterGachaPoolType_Beginner'
| 'E_CharacterGachaPoolType_Standard'
| 'E_CharacterGachaPoolType_Special',
seqId: string | null,
lang: (typeof launcherWebLang)[number] = 'ja-jp',
): Promise<TypesApiAkEndfield.WebViewRecordChar> => {
const rsp = await ky
.get(`https://${appConfig.network.api.akEndfield.base.webview}/api/record/char`, {
...defaultKySettings,
searchParams: { lang, seq_id: seqId ?? undefined, pool_type: poolType, token, server_id: serverId },
})
.json();
return rsp as TypesApiAkEndfield.WebViewRecordChar;
},
},
content: async (
serverId: number, // 2 or 3
poolId: string,
lang: (typeof launcherWebLang)[number] = 'ja-jp',
): Promise<TypesApiAkEndfield.WebViewRecordContent> => {
const rsp = await ky
.get(`https://${appConfig.network.api.akEndfield.base.webview}/api/content`, {
...defaultKySettings,
searchParams: { lang, pool_id: poolId, server_id: serverId },
})
.json();
return rsp as TypesApiAkEndfield.WebViewRecordContent;
},
},
},
apiAkEndfield: { launcher, accountService, u8, binding, webview },
};

View File

@@ -0,0 +1,67 @@
import ky from 'ky';
import * as TypesApiAkEndfield from '../../types/api/akEndfield/Api.js';
import appConfig from '../config.js';
import defaultSettings from './defaultSettings.js';
export default {
user: {
auth: {
v1: {
tokenByEmailPassword: async (
email: string,
password: string,
from: number = 0,
): Promise<TypesApiAkEndfield.AccSrvUserAuthV1TokenByEmail> => {
const rsp = await ky
.post(
`https://${appConfig.network.api.akEndfield.base.accountService}/user/auth/v1/token_by_email_password`,
{ ...defaultSettings.ky, json: { email, from, password } },
)
.json();
return rsp as TypesApiAkEndfield.AccSrvUserAuthV1TokenByEmail;
},
},
},
oauth2: {
v2: {
grant: async <T extends 0 | 1 = 0>(
appCode: string,
token: string,
type: T = 0 as any, // 0 = return grant uid (Gxxxxxxxxx) and code, 1 = return hgId and token
): Promise<
T extends 0 ? TypesApiAkEndfield.AccSrvUserOAuth2V2Grant : TypesApiAkEndfield.AccSrvUserOAuth2V2GrantType1
> => {
const rsp = await ky
.post(`https://${appConfig.network.api.akEndfield.base.accountService}/user/oauth2/v2/grant`, {
...defaultSettings.ky,
json: { appCode, token, type },
})
.json();
return rsp as any;
},
},
},
info: {
v1: {
basic: async (appCode: string, token: string): Promise<TypesApiAkEndfield.AccSrvUserInfoV1Basic> => {
const rsp = await ky
.get(`https://${appConfig.network.api.akEndfield.base.accountService}/user/info/v1/basic`, {
...defaultSettings.ky,
searchParams: { appCode, token },
})
.json();
return rsp as TypesApiAkEndfield.AccSrvUserInfoV1Basic;
},
thirdParty: async (appCode: string, token: string): Promise<TypesApiAkEndfield.AccSrvUserInfoV1ThirdParty> => {
const rsp = await ky
.get(`https://${appConfig.network.api.akEndfield.base.accountService}/user/info/v1/third_party`, {
...defaultSettings.ky,
searchParams: { appCode, token },
})
.json();
return rsp as TypesApiAkEndfield.AccSrvUserInfoV1ThirdParty;
},
},
},
},
};

34
src/utils/api/binding.ts Normal file
View File

@@ -0,0 +1,34 @@
import ky from 'ky';
import * as TypesApiAkEndfield from '../../types/api/akEndfield/Api.js';
import appConfig from '../config.js';
import defaultSettings from './defaultSettings.js';
export default {
account: {
binding: {
v1: {
bindingList: async (
// appCode: 'arknights' | 'endfield',
token: string,
): Promise<TypesApiAkEndfield.BindApiAccBindV1BindList> => {
const rsp = await ky
.get(`https://${appConfig.network.api.akEndfield.base.binding}/account/binding/v1/binding_list`, {
...defaultSettings.ky,
searchParams: { token },
})
.json();
return rsp as TypesApiAkEndfield.BindApiAccBindV1BindList;
},
u8TokenByUid: async (token: string, uid: string): Promise<TypesApiAkEndfield.BindApiAccBindV1U8TokenByUid> => {
const rsp = await ky
.post(`https://${appConfig.network.api.akEndfield.base.binding}/account/binding/v1/u8_token_by_uid`, {
...defaultSettings.ky,
json: { token, uid },
})
.json();
return rsp as TypesApiAkEndfield.BindApiAccBindV1U8TokenByUid;
},
},
},
},
};

View File

@@ -0,0 +1,27 @@
import appConfig from '../config.js';
export default {
ky: {
headers: {
'User-Agent': appConfig.network.userAgent.minimum,
},
timeout: appConfig.network.timeout,
retry: { limit: appConfig.network.retryCount },
},
launcherWebLang: [
'de-de',
'en-us',
'es-mx',
'fr-fr',
'id-id',
'it-it',
'ja-jp',
'ko-kr',
'pt-br',
'ru-ru',
'th-th',
'vi-vn',
'zh-cn',
'zh-tw',
] as const,
};

79
src/utils/api/launcher.ts Normal file
View File

@@ -0,0 +1,79 @@
import ky from 'ky';
import semver from 'semver';
import * as TypesApiAkEndfield from '../../types/api/akEndfield/Api.js';
import appConfig from '../config.js';
import defaultSettings from './defaultSettings.js';
import launcherWeb from './launcherWeb.js';
export default {
latestGame: async (
appCode: string,
launcherAppCode: string,
channel: number,
subChannel: number,
launcherSubChannel: number,
version: string | null,
): Promise<TypesApiAkEndfield.LauncherLatestGame> => {
if (version !== null && !semver.valid(version)) throw new Error(`Invalid version string (${version})`);
const rsp = await ky
.get(`https://${appConfig.network.api.akEndfield.base.launcher}/game/get_latest`, {
...defaultSettings.ky,
searchParams: {
appcode: appCode,
launcher_appcode: launcherAppCode,
channel,
sub_channel: subChannel,
launcher_sub_channel: launcherSubChannel,
version: version ?? undefined,
},
})
.json();
return rsp as TypesApiAkEndfield.LauncherLatestGame;
},
latestGameResources: async (
appCode: string,
gameVersion: string, // example: 1.0
version: string,
randStr: string,
platform: 'Windows' | 'Android' | 'iOS' | 'PlayStation',
): Promise<TypesApiAkEndfield.LauncherLatestGameResources> => {
if (!semver.valid(version)) throw new Error(`Invalid version string (${version})`);
const rsp = await ky
.get(`https://${appConfig.network.api.akEndfield.base.launcher}/game/get_latest_resources`, {
...defaultSettings.ky,
searchParams: {
appcode: appCode,
game_version: gameVersion,
version: version,
platform,
rand_str: randStr,
},
})
.json();
return rsp as TypesApiAkEndfield.LauncherLatestGameResources;
},
latestLauncher: async (
appCode: string,
channel: number,
subChannel: number,
version: string | null,
targetApp: 'EndField' | null,
): Promise<TypesApiAkEndfield.LauncherLatestLauncher> => {
if (version !== null && !semver.valid(version)) throw new Error(`Invalid version string (${version})`);
const rsp = await ky
.get(`https://${appConfig.network.api.akEndfield.base.launcher}/launcher/get_latest`, {
...defaultSettings.ky,
searchParams: {
appcode: appCode,
channel,
sub_channel: subChannel,
version: version ?? undefined,
targetApp: targetApp ?? undefined,
},
})
.json();
return rsp as TypesApiAkEndfield.LauncherLatestLauncher;
},
web: launcherWeb,
};

View File

@@ -0,0 +1,152 @@
import ky from 'ky';
import * as TypesApiAkEndfield from '../../types/api/akEndfield/Api.js';
import appConfig from '../config.js';
import defaultSettings from './defaultSettings.js';
export default {
sidebar: async (
appCode: string,
channel: number,
subChannel: number,
language: (typeof defaultSettings.launcherWebLang)[number],
platform: 'Windows' = 'Windows',
): Promise<TypesApiAkEndfield.LauncherWebSidebar> => {
const rsp = await ky
.post(`https://${appConfig.network.api.akEndfield.base.launcher}/proxy/web/batch_proxy`, {
...defaultSettings.ky,
json: {
proxy_reqs: [
{
kind: 'get_sidebar',
get_sidebar_req: {
appcode: appCode,
channel: String(channel),
sub_channel: String(subChannel),
language,
platform,
source: 'launcher',
},
},
],
},
})
.json();
return (rsp as any).proxy_rsps[0].get_sidebar_rsp as TypesApiAkEndfield.LauncherWebSidebar;
},
singleEnt: async (
appCode: string,
channel: number,
subChannel: number,
language: (typeof defaultSettings.launcherWebLang)[number],
platform: 'Windows' = 'Windows',
): Promise<TypesApiAkEndfield.LauncherWebSingleEnt> => {
const rsp = await ky
.post(`https://${appConfig.network.api.akEndfield.base.launcher}/proxy/web/batch_proxy`, {
...defaultSettings.ky,
json: {
proxy_reqs: [
{
kind: 'get_single_ent',
get_single_ent_req: {
appcode: appCode,
channel: String(channel),
sub_channel: String(subChannel),
language,
platform,
source: 'launcher',
},
},
],
},
})
.json();
return (rsp as any).proxy_rsps[0].get_single_ent_rsp as TypesApiAkEndfield.LauncherWebSingleEnt;
},
mainBgImage: async (
appCode: string,
channel: number,
subChannel: number,
language: (typeof defaultSettings.launcherWebLang)[number],
platform: 'Windows' = 'Windows',
): Promise<TypesApiAkEndfield.LauncherWebMainBgImage> => {
const rsp = await ky
.post(`https://${appConfig.network.api.akEndfield.base.launcher}/proxy/web/batch_proxy`, {
...defaultSettings.ky,
json: {
proxy_reqs: [
{
kind: 'get_main_bg_image',
get_main_bg_image_req: {
appcode: appCode,
channel: String(channel),
sub_channel: String(subChannel),
language,
platform,
source: 'launcher',
},
},
],
},
})
.json();
return (rsp as any).proxy_rsps[0].get_main_bg_image_rsp as TypesApiAkEndfield.LauncherWebMainBgImage;
},
banner: async (
appCode: string,
channel: number,
subChannel: number,
language: (typeof defaultSettings.launcherWebLang)[number],
platform: 'Windows' = 'Windows',
): Promise<TypesApiAkEndfield.LauncherWebBanner> => {
const rsp = await ky
.post(`https://${appConfig.network.api.akEndfield.base.launcher}/proxy/web/batch_proxy`, {
...defaultSettings.ky,
json: {
proxy_reqs: [
{
kind: 'get_banner',
get_banner_req: {
appcode: appCode,
channel: String(channel),
sub_channel: String(subChannel),
language,
platform,
source: 'launcher',
},
},
],
},
})
.json();
return (rsp as any).proxy_rsps[0].get_banner_rsp as TypesApiAkEndfield.LauncherWebBanner;
},
announcement: async (
appCode: string,
channel: number,
subChannel: number,
language: (typeof defaultSettings.launcherWebLang)[number],
platform: 'Windows' = 'Windows',
): Promise<TypesApiAkEndfield.LauncherWebAnnouncement> => {
const rsp = await ky
.post(`https://${appConfig.network.api.akEndfield.base.launcher}/proxy/web/batch_proxy`, {
...defaultSettings.ky,
json: {
proxy_reqs: [
{
kind: 'get_announcement',
get_announcement_req: {
appcode: appCode,
channel: String(channel),
sub_channel: String(subChannel),
language,
platform,
source: 'launcher',
},
},
],
},
})
.json();
return (rsp as any).proxy_rsps[0].get_announcement_rsp as TypesApiAkEndfield.LauncherWebAnnouncement;
},
};

82
src/utils/api/u8.ts Normal file
View File

@@ -0,0 +1,82 @@
import ky from 'ky';
import * as TypesApiAkEndfield from '../../types/api/akEndfield/Api.js';
import appConfig from '../config.js';
import defaultSettings from './defaultSettings.js';
export default {
user: {
auth: {
v2: {
tokenByChToken: async (
appCode: string,
channelMasterId: number,
channelToken: string,
platform: number = 2,
type: number = 0,
): Promise<TypesApiAkEndfield.U8UserAuthV2ChToken> => {
const rsp = await ky
.post(`https://${appConfig.network.api.akEndfield.base.u8}/u8/user/auth/v2/token_by_channel_token`, {
...defaultSettings.ky,
json: {
appCode,
channelMasterId,
channelToken: JSON.stringify({
code: channelToken,
type: 1,
isSuc: true,
}),
type,
platform,
},
})
.json();
return rsp as TypesApiAkEndfield.U8UserAuthV2ChToken;
},
grant: async (
token: string,
platform: number = 2,
type: number = 0,
): Promise<TypesApiAkEndfield.U8UserAuthV2Grant> => {
const rsp = await ky
.post(`https://${appConfig.network.api.akEndfield.base.u8}/u8/user/auth/v2/grant`, {
...defaultSettings.ky,
json: { token, type, platform },
})
.json();
return rsp as TypesApiAkEndfield.U8UserAuthV2Grant;
},
},
},
},
game: {
server: {
v1: {
serverList: async (token: string): Promise<TypesApiAkEndfield.U8GameServerV1ServerList> => {
const rsp = await ky
.post(`https://${appConfig.network.api.akEndfield.base.u8}/game/server/v1/server_list`, {
...defaultSettings.ky,
json: { token },
})
.json();
return rsp as TypesApiAkEndfield.U8GameServerV1ServerList;
},
},
},
role: {
v1: {
confirmServer: async (
token: string,
serverId: number,
): Promise<TypesApiAkEndfield.U8GameRoleV1ConfirmServer> => {
const rsp = await ky
.post(`https://${appConfig.network.api.akEndfield.base.u8}/game/role/v1/confirm_server`, {
...defaultSettings.ky,
json: { token, serverId: String(serverId) },
})
.json();
return rsp as TypesApiAkEndfield.U8GameRoleV1ConfirmServer;
},
},
},
},
};

40
src/utils/api/webview.ts Normal file
View File

@@ -0,0 +1,40 @@
import ky from 'ky';
import * as TypesApiAkEndfield from '../../types/api/akEndfield/Api.js';
import appConfig from '../config.js';
import defaultSettings from './defaultSettings.js';
export default {
record: {
char: async (
token: string,
serverId: number, // 2 or 3
poolType:
| 'E_CharacterGachaPoolType_Beginner'
| 'E_CharacterGachaPoolType_Standard'
| 'E_CharacterGachaPoolType_Special',
seqId: string | null,
lang: (typeof defaultSettings.launcherWebLang)[number] = 'ja-jp',
): Promise<TypesApiAkEndfield.WebViewRecordChar> => {
const rsp = await ky
.get(`https://${appConfig.network.api.akEndfield.base.webview}/api/record/char`, {
...defaultSettings.ky,
searchParams: { lang, seq_id: seqId ?? undefined, pool_type: poolType, token, server_id: serverId },
})
.json();
return rsp as TypesApiAkEndfield.WebViewRecordChar;
},
},
content: async (
serverId: number, // 2 or 3
poolId: string,
lang: (typeof defaultSettings.launcherWebLang)[number] = 'ja-jp',
): Promise<TypesApiAkEndfield.WebViewRecordContent> => {
const rsp = await ky
.get(`https://${appConfig.network.api.akEndfield.base.webview}/api/content`, {
...defaultSettings.ky,
searchParams: { lang, pool_id: poolId, server_id: serverId },
})
.json();
return rsp as TypesApiAkEndfield.WebViewRecordContent;
},
};

View File

@@ -17,11 +17,12 @@ type ConfigType = AllRequired<
akEndfield: {
appCode: {
game: { osWinRel: string };
launcher: { osWinRel: string };
launcher: { osWinRel: string; osWinRelEpic: string };
accountService: { osWinRel: string; skport: string; binding: string };
u8: { osWinRel: string };
};
channel: { osWinRel: number };
subChannel: { osWinRel: number; osWinRelEpic: number };
base: {
accountService: string;
launcher: string;
@@ -63,11 +64,12 @@ const initialConfig: ConfigType = {
akEndfield: {
appCode: {
game: { osWinRel: 'YDUTE5gscDZ229CW' },
launcher: { osWinRel: 'TiaytKBUIEdoEwRT' },
launcher: { osWinRel: 'TiaytKBUIEdoEwRT', osWinRelEpic: 'BBWoqCzuZ2bZ1Dro' },
accountService: { osWinRel: 'd9f6dbb6bbd6bb33', skport: '6eb76d4e13aa36e6', binding: '3dacefa138426cfe' },
u8: { osWinRel: '973bd727dd11cbb6ead8' },
},
channel: { osWinRel: 6 },
subChannel: { osWinRel: 6, osWinRelEpic: 801 },
base: {
accountService: 'YXMuZ3J5cGhsaW5lLmNvbQ==',
launcher: 'bGF1bmNoZXIuZ3J5cGhsaW5lLmNvbS9hcGk=',