/** * 当前节点网络信息与网站延迟仪表板。 */ const NETWORK_SOURCES = [ { key: 'domestic', title: '国内出口', source: 'speedtest.cn' }, { key: 'overseas', title: '国外出口', source: 'cmliussss API' }, { key: 'cloudflare', title: 'Cloudflare', source: '090227 API' }, { key: 'twitter', title: 'X.com', source: 'Cloudflare trace' } ] const SITES = [ ['字节跳动', '国内', 'https://lf3-zlink-tos.ugurl.cn/obj/zebra-public/resource_lmmizj_1632398893.png'], ['Bilibili', '国内', 'https://i0.hdslb.com/bfs/face/member/noface.jpg@24w_24h_1c'], ['微信', '国内', 'https://res.wx.qq.com/a/wx_fed/assets/res/NTI4MWU5.ico'], ['淘宝', '国内', 'https://img.alicdn.com/imgextra/i2/O1CN01qnQCrN1VkzAWiU4Hs_!!6000000002692-2-tps-33-33.png'], ['GitHub', '国际', 'https://github.github.io/janky/images/bg_hr.png'], ['jsDelivr', '国际', 'https://cdn.jsdelivr.net/npm/latency-test@1.0.1/smallest-possible-white.gif'], ['Cloudflare', '国际', 'https://www.cloudflare.com/favicon.ico'], ['YouTube', '国际', 'https://www.youtube.com/favicon.ico'] ].map(([name, region, url]) => ({ name, region, url })) /** 构造带认证和 IPv6 兼容的本地代理 URL。 */ const proxyUrlOf = (endpoint) => { if (!endpoint?.host || !endpoint?.port) throw new Error('没有可用的本地代理入站') const host = String(endpoint.host).includes(':') && !String(endpoint.host).startsWith('[') ? `[${endpoint.host}]` : endpoint.host const auth = endpoint.username ? `${encodeURIComponent(endpoint.username)}:${encodeURIComponent(endpoint.password || '')}@` : '' return `${endpoint.schema || endpoint.proxyType || 'http'}://${auth}${host}:${endpoint.port}` } /** 把第三方返回的 trace 文本解析成键值对象。 */ const parseTrace = (body) => Object.fromEntries( String(body || '') .split(/\r?\n/) .map((line) => line.split('=').map((part) => part.trim())) .filter(([key, value]) => key && value) ) /** 追加时间戳,避免出口信息和延迟请求被缓存。 */ const cacheBust = (url) => `${url}${url.includes('?') ? '&' : '?'}t=${Date.now()}` /** 将异常转换成短中文错误信息。 */ const errorText = (error) => String(error?.message || error || '请求失败') .replace(/^Error:\s*/, '') .slice(0, 120) /** 按延迟返回界面颜色等级。 */ const latencyClass = (value) => value < 0 ? 'text-red-500' : value <= 80 ? 'text-green-500' : value <= 180 ? 'text-cyan-500' : value <= 350 ? 'text-orange-500' : 'text-red-500' /** 创建一次检测请求上下文。 */ const createRun = (proxy) => ({ proxy, generation: Date.now() + Math.random(), cancelled: false, ids: new Set() }) /** 取消检测上下文中的全部请求。 */ const cancelRun = (run) => { if (!run) return run.cancelled = true run.ids.forEach((id) => { try { Plugins.HttpCancel(id) } catch {} }) run.ids.clear() } /** 通过 GFS 代理请求并返回响应与耗时。 */ const request = async (run, url, autoTransformBody = true) => { if (run.cancelled) throw new Error('检测已取消') const id = `network-info-${Plugins.sampleID()}` run.ids.add(id) const started = performance.now() try { const response = await Plugins.Requests({ method: 'GET', url: cacheBust(url), autoTransformBody, options: { Proxy: run.proxy, Timeout: 10, CancelId: id } }) if (response.status < 200 || response.status >= 400) throw new Error(`HTTP ${response.status}`) return { ...response, elapsed: performance.now() - started } } finally { run.ids.delete(id) } } /** 获取国内出口信息,按三个公开接口依次回退。 */ const domesticInfo = async (run) => { const sources = [ [ 'speedtest.cn', 'https://api-v3.speedtest.cn/ip', (body) => { if (body?.code !== 0 || !body.data) throw new Error('返回格式异常') return { ip: body.data.ip, place: `${body.data.country || ''} ${body.data.city || ''}` } } ], ['ipipv.com', 'https://myip.ipipv.com/', (body) => ({ ip: body.Ip, place: `${body.Country || ''} ${body.City || ''}` })], [ 'ipip.net', 'https://myip.ipip.net/json', (body) => { if (body?.ret !== 'ok' || !body.data) throw new Error('返回格式异常') return { ip: body.data.ip, place: `${body.data.location?.[0] || ''} ${body.data.location?.[2] || ''}` } } ] ] let last for (const [source, url, parse] of sources) { if (run.cancelled) throw new Error('检测已取消') try { const response = await request(run, url) const result = parse(response.body) return { ...result, source, elapsed: response.elapsed } } catch (error) { last = error } } throw last || new Error('国内出口检测失败') } /** 获取国外出口信息,按三个公开接口依次回退,全部失败时抛出最后一个错误。 */ const overseasInfo = async (run) => { const sources = [ [ 'cmliussss API', 'https://api.cmliussss.net/api/ipinfo', (body) => { if (!body?.ip) throw new Error('返回格式异常') return { ip: body.ip, place: `${body.country_code || ''} AS${body.asn || ''} ${body.as_name || ''}` } } ], [ 'ipinfo.io', 'https://ipinfo.io/json', (body) => { if (!body?.ip) throw new Error('返回格式异常') return { ip: body.ip, place: `${body.country || ''} ${body.org || ''}` } } ], [ 'ipapi.co', 'https://ipapi.co/json/', (body) => { if (!body?.ip) throw new Error('返回格式异常') return { ip: body.ip, place: `${body.country_code || body.country_name || ''} ${body.org || (body.asn ? `AS${body.asn}` : '')}` } } ] ] let last for (const [source, url, parse] of sources) { if (run.cancelled) throw new Error('检测已取消') try { const response = await request(run, url) const result = parse(response.body) return { ...result, source, elapsed: response.elapsed } } catch (error) { last = error } } throw last || new Error('国外出口检测失败') } /** 获取四类出口信息。 */ const networkInfo = async (run) => { const jobs = [ domesticInfo(run), overseasInfo(run), request(run, 'https://cf.090227.xyz/ip.json').then((r) => ({ ip: r.body.ip, place: `${r.body.country || ''} ${r.body.org || ''}`, source: 'cf.090227.xyz', elapsed: r.elapsed })), request(run, 'https://help.x.com/cdn-cgi/trace', false).then((r) => { const data = parseTrace(r.body) return { ip: data.ip, place: `${data.loc || ''} ${data.colo || ''}`, source: 'X.com trace', elapsed: r.elapsed } }) ] return Promise.allSettled(jobs) } /** 测量一个网站的请求耗时。 */ const siteLatency = async (run, site) => { const response = await request(run, site.url) return { ...site, latency: Math.round(response.elapsed), status: '成功', error: '' } } /** 打开网络信息仪表板。 */ const onRun = async () => { const api = Plugins.useKernelApiStore() if (!api.running) return Plugins.message.error('内核未运行,请先启动内核') let activeRun = null const modal = Plugins.modal({ title: Plugin.name || '当前网络信息', width: '88', height: '88', submit: false, cancelText: '关闭', onCancel: () => { cancelRun(activeRun) return true } }) const content = { template: `