UNPKG

koishi-plugin-enka-fork

Version:
398 lines (397 loc) 18.4 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Config = exports.inject = exports.name = void 0; exports.apply = apply; const promises_1 = __importDefault(require("fs/promises")); const path_1 = __importDefault(require("path")); const koishi_1 = require("koishi"); const puppeteer_page_proxy_1 = __importDefault(require("puppeteer-page-proxy")); const localeMap_json_1 = __importDefault(require("./locales/localeMap.json")); const zh_json_1 = __importDefault(require("./locales/zh.json")); const types_1 = require("./types"); const UUIDRegExp = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/; exports.name = 'enka'; exports.inject = ['puppeteer', 'database']; exports.Config = koishi_1.Schema.object({ agent: koishi_1.Schema.union([ koishi_1.Schema.const(types_1.EnkaAgent.ENKA).description('默认(Enka)'), koishi_1.Schema.const(types_1.EnkaAgent.ENKA).description('默认(Enka)'), ]).default(types_1.EnkaAgent.ENKA).description('请求地址'), data: koishi_1.Schema.union([ koishi_1.Schema.const(types_1.EnkaDataAgent.NYAN).description('NyanZone'), koishi_1.Schema.const(types_1.EnkaDataAgent.GITHUB).description('GitHub'), koishi_1.Schema.const(types_1.EnkaDataAgent.GHPROXY).description('Proxy(GH)'), ]).default(types_1.EnkaDataAgent.NYAN).description('数据地址'), proxy: koishi_1.Schema.union([ koishi_1.Schema.const(false).description('禁止'), koishi_1.Schema.const(true).description('全局设置'), koishi_1.Schema.string().role('link').description('自定义'), ]).description('Puppetter 代理设置,仅用于 Puppeteer, 不会影响其他请求(⚠实验性)'), }); function mapIndexSearch(index, search) { for (const key in index) { if (key.includes(search)) return index[key]; } return false; } function apply(ctx, config) { ctx.i18n.define('zh', zh_json_1.default); ctx.model.extend('user', { genshin_uid: 'string(20)', enka_data: 'json', }); ctx.model.extend('enka_alias', { cid: 'string(20)', alias: 'list', }, { primary: ['cid'], unique: ['cid'], }); const logger = ctx.logger('enka'); let page; let lock; const mapIndex = {}; let map = {}; let characterInfo = {}; async function initlization(forcibly = false) { logger.debug('checking characters data...'); const dataPath = path_1.default.join(ctx.root.baseDir, 'data/enka/idMap.json'); const characterPath = path_1.default.join(ctx.root.baseDir, 'data/enka/characters.json'); const aliasData = await ctx.database.get('enka_alias', {}); const update = async () => { if (forcibly) logger.debug('forcibly update characters data...'); // download UIGF characters data logger.debug('downloading UIGF ID map...'); const data = await ctx.http.get('https://api.uigf.org/dict/genshin/all.json'); for (let locale in data) { locale = locale.toLowerCase(); const characters = data[locale]; if (locale === 'chs') locale = 'zh'; if (locale === 'cht') locale = 'zh-tw'; for (const characterName in characters) { const id = characters[characterName]; if (id < 10000000) continue; if (!map[id]) map[id] = { names: { [locale]: characterName } }; else map[id].names[locale] = characterName; } } // download enka characters data logger.debug('downloading characters data...'); const enka = await ctx.http.get(`${config.data}/characters.json`); await promises_1.default.mkdir('data/enka', { recursive: true }); await promises_1.default.writeFile(characterPath, JSON.stringify(enka)); await promises_1.default.writeFile(dataPath, JSON.stringify(map)); }; if (forcibly) await update(); else { try { await promises_1.default.access(dataPath); await promises_1.default.access(characterPath); // load UIGF ID and Characters data logger.debug('characters data exists, loading...'); const mapBuf = await promises_1.default.readFile(dataPath); const dataBuf = await promises_1.default.readFile(characterPath); map = JSON.parse(mapBuf.toString()); characterInfo = JSON.parse(dataBuf.toString()); } catch (error) { logger.debug('characters data not exists, mapping...'); await update(); } } for (const id in map) { const alias = aliasData.find((i) => i.cid === id)?.alias || []; const characterNames = Object.values(map[id].names).flatMap((nameList) => (Array.isArray(nameList) ? nameList : [nameList])); mapIndex[[...characterNames, ...alias].join(',')] = id; } logger.info('characters data loaded.'); } ctx.on('ready', async () => { page || (page = await ctx.puppeteer.page()); if (config.proxy) await (0, puppeteer_page_proxy_1.default)(page, config.proxy === true ? ctx.root.config.request.proxyAgent : config.proxy); logger.info('initlizing puppeteer.'); await initlization(); logger.debug('all initlized.'); }); ctx.command('enka [search:string]') .userFields(['genshin_uid', 'locales', 'enka_data']) .option('update', '-u') .action(async ({ session, options }, search) => { const locale = (session.user.locale || session.user.locales[0]) ?? 'zh'; const userLang = localeMap_json_1.default[locale] || ['简体中文', '自定义文本']; logger.debug('search:', search); if (!session.user.genshin_uid) return session.text('.bind'); if (search && !mapIndexSearch(mapIndex, search)) return session.text('.non-existent'); search = mapIndexSearch(mapIndex, search); logger.debug('search to id:', search || 'null'); logger.debug('userLang:', userLang); if (Object.keys(session.user.enka_data).length === 0 || options.update) { try { // 添加延迟防止频繁请求 await new Promise((resolve) => { setTimeout(resolve, 1500); }); const response = await ctx.http.get(`${config.agent}/api/uid/${session.user.genshin_uid}`, { headers: { 'User-Agent': 'Mozilla/5.0 (compatible; KoishiBot/1.0; +https://github.com/koishijs/koishi-plugin-enka)', 'Accept': 'application/json', }, }); logger.debug('Enka API response:', JSON.stringify(response, null, 2)); if (!response) { logger.warn('Empty response from Enka API'); return session.text('.error'); } const info = response.playerInfo; logger.debug('Player info:', info ? JSON.stringify(info, null, 2) : 'null'); if (!info) { logger.warn('No player info found in response'); return session.text('.no-data'); } if (!info.showAvatarInfoList) { logger.warn('No showAvatarInfoList found in player info'); return session.text('.no-characters'); } if (info.showAvatarInfoList.length === 0) { logger.warn('showAvatarInfoList is empty'); return session.text('.no-characters'); } session.user.enka_data = { characterList: info.showAvatarInfoList.map((i) => i.avatarId), characterLevels: info.showAvatarInfoList, ...(0, koishi_1.pick)(info, ['nickname', 'level', 'signature', 'worldLevel']), }; } catch (error) { logger.error(`Enka API error: ${error.message}`); logger.error('Error details:', error); if (error.response?.status === 403) { return session.text('.forbidden'); } if (error.response?.status === 404) { return session.text('.no-data'); } if (error.response?.status === 429) { return session.text('.rate-limited'); } if (error.response?.status >= 500) { return session.text('.server-error'); } return session.text('.error'); } } // ... 后续代码不变 ... // now, the 'search' is a character id logger.debug('Sending relax message...'); await session.send(session.text('.relax')); logger.debug('Checking lock...'); if (lock) { logger.debug('Waiting for lock...'); await lock; } logger.debug('Creating new lock...'); let resolve; lock = new Promise((r) => { resolve = r; }); logger.debug('Processing search:', search); if (search) { logger.debug('Search mode - character:', characterInfo[search]); if (!characterInfo[search]) { logger.debug('Character not found in characterInfo'); return session.text('.non-existent'); } // check this search in user's character list logger.debug('Checking character in user list:', Number(search)); logger.debug('User character list:', session.user.enka_data.characterList); if (!session.user.enka_data.characterList.includes(Number(search))) { logger.debug('Character not in user list'); return session.text('.non-existent'); } // get character image logger.debug('Starting character image generation...'); try { logger.debug('Navigating to Enka page...'); await page.goto(`${config.agent}/u/${session.user.genshin_uid}/`, { waitUntil: 'networkidle0', timeout: 60000, }); logger.debug('Page loaded successfully'); const { left, top } = await page.evaluate(async (searchParam, userLangParam) => { Array.from((document.querySelectorAll('.UI.SelectorElement'))) .find((i) => i.innerHTML.trim() === userLangParam)?.click(); for (const i of Array.from((document.querySelectorAll('.Dropdown-list')))) { i.style.display = 'none'; } const tabs = Array.from(document.getElementsByTagName('figure')); const _characters = []; for (const ele of tabs) { if (ele.style.backgroundImage?.includes('AvatarIcon')) { _characters.push(ele.style.backgroundImage?.replace('url("/ui/', '').replace('.png")', '')); } } if (searchParam) { const select = tabs.find((i) => i.style.backgroundImage?.toLowerCase().includes(searchParam.SideIconName.toLowerCase())); const rect = select?.parentElement?.getBoundingClientRect(); for (const i of Array.from((document.querySelectorAll('.Checkbox.Control.sm:not(.checked)')))) { i.click(); } if (!select) return { left: 0, top: 0 }; return { left: rect?.left || 0, top: rect?.top || 0 }; } return { left: 0, top: 0 }; }, characterInfo[search], userLang[0]); logger.debug('Page evaluation result:', { left, top }); if (!left) { logger.debug('No character found on page'); return session.text('.not-found'); } logger.debug('Clicking character position...'); await page.mouse.click(left + 1, top + 1); await Promise.all([ page.waitForNetworkIdle({ idleTime: 100 }), page.evaluate((inText) => { const input = document.querySelector(`[placeholder="${inText || 'Custom text'}"]`); if (input) { input.value = 'Koishi & Enka Network'; input.dispatchEvent(new Event('input')); } return null; }, userLang[1]), ]); logger.debug('Clicking image button...'); await page.click('button[data-icon="image"]'); logger.debug('Waiting for image response...'); const buf = await new Promise((resolvePromise) => { const cb = async (ev) => { logger.debug('Response URL:', ev.request().url()); if (!UUIDRegExp.test(ev.request().url().trim())) return; logger.debug('Found image response, resolving...'); page.off('response', cb); resolvePromise(await ev.buffer()); }; page.on('response', cb); }); logger.debug('Image buffer received, size:', buf.length); return koishi_1.h.image(buf, 'image/png'); } catch (error) { logger.error('Character image generation error:', error); return session.text('.error'); } finally { logger.debug('Resolving lock...'); resolve(); } } else { logger.debug('List mode - showing character list'); const { nickname, level, signature, worldLevel, characterLevels, } = session.user.enka_data; logger.debug('user_data:', session.user.enka_data); let title = `<p>${session.text('.list', [nickname, level, signature, worldLevel])}</p>`; const content = []; let tLength = 1; logger.debug('characterLevels:', characterLevels); if (characterLevels.length > 0) { for (const character of characterLevels) { const namer = map[character.avatarId]; if (character) { const n = namer.names[locale || 'zh']; if (n.length > tLength) tLength = n.length; content.push({ namer: n, level: character.level, }); } } } else { title = session.text('.non-list'); } logger.debug('Resolving lock for list mode...'); resolve(); logger.debug('Returning character list'); return title + content.map((i) => `<p>(${i.level.toString().padStart(2, '0')}) ${i.namer}</p>`).join(''); } }); // 先注册子命令,避免与主命令冲突 ctx.command('enka.uid.show') .alias('enka.uid.info') .userFields(['genshin_uid']) .action(async ({ session }) => { if (!session.user.genshin_uid) { return session.text('.not-bound'); } return session.text('.uid', [session.user.genshin_uid]); }); ctx.command('enka.uid.rm') .alias('enka.uid.remove') .userFields(['genshin_uid', 'enka_data']) .action(async ({ session }) => { if (!session.user.genshin_uid) { return session.text('.not-bound'); } const removedUid = session.user.genshin_uid; session.user.genshin_uid = ''; session.user.enka_data = { nickname: '', level: 0, signature: '', worldLevel: 0, characterList: [], characterLevels: [], }; return session.text('.removed', [removedUid]); }); ctx.command('enka.uid.add <uid:string>') .alias('enka.uid.set') .userFields(['genshin_uid']) .action(async ({ session }, uid) => { if (!/^[1256789][0-9]{3,9}$/gm.test(uid)) return session.text('.fail'); if (uid === session.user.genshin_uid) return session.text('.same'); session.user.genshin_uid = uid; session.send(session.text('.saved', [uid])); }); ctx.command('enka.upgrade') .alias('.up') .action(async ({ session }) => { session.send(session.text('.upgrading')); await initlization(true); return session.text('.upgraded'); }); ctx.command('enka.alias <name:string> <alias:string>') .action(async ({ session }, characterName, alias) => { const cid = mapIndexSearch(mapIndex, characterName); if (!cid) return session.text('.non-existent'); const aliasTable = await ctx.database.get('enka_alias', { cid }); if (aliasTable.length === 0) aliasTable.push({ cid, alias: [alias] }); else { if (aliasTable[0].alias.includes(alias)) return session.text('.exist'); aliasTable[0].alias.push(alias); } await ctx.database.upsert('enka_alias', aliasTable); initlization(); return session.text('.saved', [alias, characterName]); }); }