(url: string)
| 137 | |
| 138 | // 尝试解析节点信息 (节点数, 类型, 地区分布) |
| 139 | async function getNodeInfo(url: string): Promise<{ node_count: number | string, type_count: Record<string, number>, regions: Record<string, number> } | null> { |
| 140 | try { |
| 141 | const res = await axios.get(url, { timeout: 10000, responseType: 'text' }); |
| 142 | |
| 143 | // 1. 尝试解析 YAML (Clash/Surge) |
| 144 | try { |
| 145 | const config = yaml.load(res.data); |
| 146 | if (config && (config as any).proxies) { |
| 147 | const proxies = (config as any).proxies; |
| 148 | const typeCount: Record<string, number> = {}; |
| 149 | const regions: Record<string, number> = {}; |
| 150 | let totalNodes = proxies.length; |
| 151 | let identified = 0; |
| 152 | for (const proxy of proxies) { |
| 153 | const type = proxy.type?.toLowerCase(); |
| 154 | typeCount[type] = (typeCount[type] || 0) + 1; |
| 155 | const nameLow = proxy.name?.toLowerCase() || ''; |
| 156 | for (const [region, keys] of REGION_RULES) { |
| 157 | if (keys.some(k => nameLow.includes(k.toLowerCase()))) { |
| 158 | regions[region] = (regions[region] || 0) + 1; |
| 159 | identified++; |
| 160 | break; |
| 161 | } |
| 162 | } |
| 163 | } |
| 164 | if (totalNodes - identified > 0) regions['其他'] = totalNodes - identified; |
| 165 | return { |
| 166 | node_count: totalNodes, |
| 167 | type_count: Object.fromEntries(Object.entries(typeCount).filter(([, v]) => v > 0)), |
| 168 | regions: Object.fromEntries(Object.entries(regions).filter(([, v]) => v > 0)) |
| 169 | }; |
| 170 | } |
| 171 | } catch { /* 忽略 YAML 解析错误 */ } |
| 172 | |
| 173 | // 2. 尝试解析 Base64 (V2Ray/Shadowsocks 原始链接) |
| 174 | try { |
| 175 | const decoded = Buffer.from(res.data, 'base64').toString(); |
| 176 | const typeCount: Record<string, number> = {}; |
| 177 | const regions: Record<string, number> = {}; |
| 178 | let nodeCount = 0; |
| 179 | let identified = 0; |
| 180 | const protocols = ['vmess://', 'trojan://', 'ss://', 'ssr://', 'vless://', 'hy2://', 'hysteria://', 'hy://', 'tuic://', 'wireguard://', 'socks5://', 'http://', 'https://', 'shadowtls://', 'naive://']; |
| 181 | |
| 182 | decoded.split('\n').forEach((line: string) => { |
| 183 | if (!line.trim()) return; |
| 184 | for (const pattern of protocols) { |
| 185 | if (line.startsWith(pattern)) { |
| 186 | let t = pattern.replace('://', ''); |
| 187 | typeCount[t] = (typeCount[t] || 0) + 1; |
| 188 | nodeCount++; |
| 189 | let lLow = line.toLowerCase(); |
| 190 | for (const [region, keys] of REGION_RULES) { |
| 191 | if (keys.some(k => lLow.includes(k.toLowerCase()))) { |
| 192 | regions[region] = (regions[region] || 0) + 1; |
| 193 | identified++; |
| 194 | break; |
| 195 | } |
| 196 | } |
no test coverage detected