UNPKG

koishi-plugin-adapter-iirose

Version:
347 lines (299 loc) 7.75 kB
import { Context } from 'koishi'; import { IIROSE_Bot } from '../../bot/bot'; import { LoginObj } from './types'; import { getMd5Password, md5 } from '../password'; import { calculateRetryDelay, waitWithCancel } from './retry'; import { IIROSE_WSsend } from './send'; /** * 测试服务器延迟 */ export async function getLatency( ctx: Context, bot: IIROSE_Bot, url: string, disposed: () => boolean ): Promise<number | 'error'> { return new Promise((resolve) => { if (disposed()) { resolve('error'); return; } let ws: WebSocket | null = null; let timeoutId: (() => void) | null = null; let disposingCheckId: (() => void) | null = null; let resolved = false; const cleanup = () => { if (timeoutId) { timeoutId(); timeoutId = null; } if (disposingCheckId) { disposingCheckId(); disposingCheckId = null; } if (ws && (ws.readyState === 1 || ws.readyState === 0)) { try { ws.close(); } catch (e) { // 忽略关闭错误 } } ws = null; }; const safeResolve = (value: number | 'error') => { if (!resolved) { resolved = true; cleanup(); resolve(value); } }; try { const startTime = Date.now(); const timeout = Math.max(bot.config.timeout, 2000); ws = ctx.http.ws(url); timeoutId = ctx.setTimeout(() => { safeResolve('error'); }, timeout); disposingCheckId = ctx.setInterval(() => { if (disposed()) { safeResolve('error'); } }, 200); ws.addEventListener('open', () => { const endTime = Date.now(); const latency = endTime - startTime; safeResolve(latency); }); ws.addEventListener('error', () => { safeResolve('error'); }); ws.addEventListener('close', () => { if (!resolved) { safeResolve('error'); } }); } catch (error) { safeResolve('error'); } }); } /** * 准备 WebSocket 连接,测试服务器并选择最快的 */ export async function prepareConnection( ctx: Context, bot: IIROSE_Bot, disposed: () => boolean ): Promise<WebSocket> { const iiroseList = ['m1', 'm2', 'm8', 'm9', 'm']; let fastest = 'www'; let maximumSpeed = 100000; let allErrors: boolean; let retryCount = 0; const maxRetryIntervalMinutes = bot.config.maxRetryInterval; do { if (disposed()) { throw new Error('插件正在停用'); } allErrors = true; const speedTests: Promise<{ index: string, speed: number | 'error'; }>[] = []; // 并行测试所有服务器 for (let webIndex of iiroseList) { speedTests.push( getLatency(ctx, bot, `wss://${webIndex}.iirose.com:8778`, disposed) .then(speed => ({ index: webIndex, speed })) .catch(() => ({ index: webIndex, speed: 'error' as const })) ); } try { const results = await Promise.race([ Promise.allSettled(speedTests).then(settledResults => settledResults.map(result => result.status === 'fulfilled' ? result.value : { index: '', speed: 'error' as const } ).filter(r => r.index !== '') ), new Promise<{ index: string, speed: 'error'; }[]>(resolve => ctx.setTimeout(() => resolve(iiroseList.map(index => ({ index, speed: 'error' as const }))), 5000) ) ]); if (disposed()) { throw new Error('插件正在停用'); } // 找到最快的可用服务器 for (const result of results) { if (result.speed !== 'error') { allErrors = false; if (maximumSpeed > result.speed) { fastest = result.index; maximumSpeed = result.speed; } } } if (!allErrors) { break; } } catch (error) { bot.loggerWarn('服务器测试过程中出现错误:', error); } if (allErrors) { const delayMs = calculateRetryDelay(retryCount, maxRetryIntervalMinutes); const delaySec = Math.round(delayMs / 1000); if (!bot.config.silentRetry || bot.config.debugMode) { bot.loggerWarn(`所有服务器都无法连接,将在${delaySec}秒后重试... (重试次数: ${retryCount})`); } const cancelled = await waitWithCancel(ctx, bot, delayMs, disposed); if (cancelled) { throw new Error('插件正在停用'); } retryCount++; if (disposed()) { throw new Error('插件正在停用'); } } } while (allErrors && !disposed()); if (!fastest) { fastest = 'www'; } const targetUrl = `wss://${fastest}.iirose.com:8778`; bot.loggerInfo(`找到可用服务器: ${targetUrl}, 延迟: ${maximumSpeed}ms`); const socket = ctx.http.ws(targetUrl); socket.binaryType = 'arraybuffer'; const dispose = ctx.on('dispose', () => { if (socket && socket.readyState === 1) { socket.close(); } dispose(); }); return socket; } /** * 创建登录对象 */ export function createLoginObj(bot: IIROSE_Bot): LoginObj { const roomIdReg = /\s*\[_([\\s\\S]+)_\]\s*/; const userNameReg = /\s*\[\\*([\\s\\S]+)\\*\]\s*/; const roomIdConfig = bot.config.roomId; const userNameConfig = bot.config.usename; let username = (userNameReg.test(userNameConfig)) ? userNameConfig.match(userNameReg)?.[1] : userNameConfig; let room = (roomIdReg.test(roomIdConfig)) ? roomIdConfig.match(roomIdReg)?.[1] : roomIdConfig; let loginObj: LoginObj; if (bot.config.smStart && bot.config.smPassword === 'ec3a4ac482b483ac02d26e440aa0a948') { loginObj = { r: bot.config.smRoom, n: bot.config.smUsername, i: bot.config.smImage, nc: bot.config.smColor, s: bot.config.smGender, st: bot.config.smst, mo: bot.config.smmo, uid: bot.config.smUid, li: bot.config.smli, mb: bot.config.smmb, mu: bot.config.smmu, la: bot.config.smLocation, vc: bot.config.smvc, fp: `@${md5(bot.config.smUsername)}` }; bot.loggerInfo('已启用蔷薇游客模式'); } else { const hashedPassword = getMd5Password(bot.config.password); if (!hashedPassword) { bot.loggerError('登录失败:密码不能为空'); throw new Error('密码不能为空'); } loginObj = { r: room || bot.config.roomId, n: username || bot.config.usename, p: hashedPassword, st: bot.config.botStatus, mo: bot.config.signature, mb: '', mu: '01', lr: bot.config.oldRoomId, rp: bot.config.roomPassword, fp: `@${md5(username || bot.config.usename)}` }; } if (!loginObj.lr) { delete loginObj.lr; } return loginObj; } /** * 设置连接打开事件 */ export function setupOpenEvent( socket: WebSocket, bot: IIROSE_Bot, loginObj: LoginObj, startHeartbeat: () => void, startEvents: () => (() => boolean)[] ) { socket.addEventListener('open', async () => { bot.loggerInfo('正在登录中...'); try { const loginPack = '*' + JSON.stringify(loginObj); await IIROSE_WSsend(bot, loginPack); const events = startEvents(); if (bot.config.keepAliveEnable) { startHeartbeat(); } return events; } catch (error) { bot.loggerError('登录包发送失败:', error); if (socket.readyState === 1) { socket.close(); } throw error; } }); }