修复生产环境使用网盘有效性检测报错无法找到模块

This commit is contained in:
mtvpls
2026-05-07 16:13:03 +08:00
parent 6445f7c704
commit 949f8a712f
12 changed files with 178 additions and 331 deletions
+23 -37
View File
@@ -9,9 +9,15 @@ import {
setNetdiskCheckInflight,
} from './cache';
import type { NetdiskCheckPlatform, NetdiskCheckResult } from './types';
// eslint-disable-next-line no-eval
const nodeRequire = eval('require') as NodeRequire;
import { check115 } from './vendor/checkers/pan115';
import { checkAliyun } from './vendor/checkers/aliyun';
import { checkBaidu } from './vendor/checkers/baidu';
import { checkCMCC } from './vendor/checkers/cmcc';
import { check123 } from './vendor/checkers/pan123';
import { checkQuark } from './vendor/checkers/quark';
import { checkTianyi } from './vendor/checkers/tianyi';
import { checkUC } from './vendor/checkers/uc';
import { checkXunlei } from './vendor/checkers/xunlei';
const RATE_LIMIT_REASON_PATTERNS = [/频率限制/i, /请求过快/i, /rate.?limit/i, /too many/i, /风控/i];
@@ -21,10 +27,6 @@ type RawCheckerResult = {
isRateLimited?: boolean;
};
type CheckerModule = {
[key: string]: (url: string) => Promise<RawCheckerResult>;
};
const PLATFORM_PATTERNS: Record<NetdiskCheckPlatform, RegExp[]> = {
'115': [/115(?:cdn)?\.com\/s\//i, /anxia\.com\/s\//i],
quark: [/pan\.quark\.cn\/s\//i, /pan\.qoark\.cn\/s\//i],
@@ -37,37 +39,18 @@ const PLATFORM_PATTERNS: Record<NetdiskCheckPlatform, RegExp[]> = {
cmcc: [/yun\.139\.com\/shareweb/i, /caiyun\.139\.com\/m\/i/i],
};
const CHECKER_EXPORTS: Record<NetdiskCheckPlatform, { modulePath: string; exportName: string }> = {
'115': { modulePath: '@/lib/pancheck/vendor/checkers/pan115.js', exportName: 'check115' },
aliyun: { modulePath: '@/lib/pancheck/vendor/checkers/aliyun.js', exportName: 'checkAliyun' },
baidu: { modulePath: '@/lib/pancheck/vendor/checkers/baidu.js', exportName: 'checkBaidu' },
cmcc: { modulePath: '@/lib/pancheck/vendor/checkers/cmcc.js', exportName: 'checkCMCC' },
pan123: { modulePath: '@/lib/pancheck/vendor/checkers/pan123.js', exportName: 'check123' },
quark: { modulePath: '@/lib/pancheck/vendor/checkers/quark.js', exportName: 'checkQuark' },
tianyi: { modulePath: '@/lib/pancheck/vendor/checkers/tianyi.js', exportName: 'checkTianyi' },
uc: { modulePath: '@/lib/pancheck/vendor/checkers/uc.js', exportName: 'checkUC' },
xunlei: { modulePath: '@/lib/pancheck/vendor/checkers/xunlei.js', exportName: 'checkXunlei' },
const CHECKERS: Record<NetdiskCheckPlatform, (url: string) => Promise<RawCheckerResult>> = {
'115': check115,
aliyun: checkAliyun,
baidu: checkBaidu,
cmcc: checkCMCC,
pan123: check123,
quark: checkQuark,
tianyi: checkTianyi,
uc: checkUC,
xunlei: checkXunlei,
};
const checkerFnCache = new Map<NetdiskCheckPlatform, (url: string) => Promise<RawCheckerResult>>();
function resolveModulePath(modulePath: string) {
return modulePath.replace(/^@\//, `${process.cwd()}/src/`);
}
function getChecker(platform: NetdiskCheckPlatform) {
const cached = checkerFnCache.get(platform);
if (cached) return cached;
const config = CHECKER_EXPORTS[platform];
const mod = nodeRequire(resolveModulePath(config.modulePath)) as CheckerModule;
const fn = mod[config.exportName];
if (typeof fn !== 'function') {
throw new Error(`未找到 ${platform} 检测器`);
}
checkerFnCache.set(platform, fn);
return fn;
}
export function normalizeNetdiskCheckUrl(url: string) {
return url.trim().replace(/\s+/g, '').replace(/\/+$/, '');
}
@@ -156,7 +139,10 @@ export async function checkNetdiskLink(platform: NetdiskCheckPlatform, url: stri
const runner = (async () => {
const startedAt = Date.now();
try {
const checker = getChecker(platform);
const checker = CHECKERS[platform];
if (typeof checker !== 'function') {
throw new Error(`未找到 ${platform} 检测器`);
}
const raw = await checker(normalizedUrl);
const finalResult = toFinalResult(platform, url, normalizedUrl, raw, Date.now() - startedAt);
setCachedNetdiskCheckResult(cacheKey, finalResult);
@@ -1,11 +1,8 @@
const { request } = require('./http');
// @ts-nocheck
/**
*
* URL格式: https://www.alipan.com/s/{share_id} 或 https://www.aliyundrive.com/s/{share_id}
* API: POST https://api.aliyundrive.com/adrive/v3/share_link/get_share_by_anonymous
*/
async function checkAliyun(link) {
import { request } from './http';
export async function checkAliyun(link) {
const { shareId, error: parseError } = extractParamsAliPan(link);
if (parseError) {
return { valid: false, reason: '链接格式无效: ' + parseError };
@@ -17,11 +14,11 @@ async function checkAliyun(link) {
method: 'POST',
body: { share_id: shareId },
headers: {
'authorization': '',
authorization: '',
'Content-Type': 'application/json',
'Origin': 'https://www.alipan.com',
'Referer': 'https://www.alipan.com/',
'Priority': 'u=1, i',
Origin: 'https://www.alipan.com',
Referer: 'https://www.alipan.com/',
Priority: 'u=1, i',
'Sec-Ch-Ua': '"Chromium";v="142", "Google Chrome";v="142", "Not_A Brand";v="99"',
'Sec-Ch-Ua-Mobile': '?0',
'Sec-Ch-Ua-Platform': '"Windows"',
@@ -39,7 +36,7 @@ async function checkAliyun(link) {
return { valid: false, reason: `API返回错误状态码: ${statusCode}` };
}
JSON.parse(body); // 验证可解析即可
JSON.parse(body);
return { valid: true, reason: '' };
} catch (err) {
if (err.message === '请求超时') return { valid: false, reason: '请求超时' };
@@ -47,7 +44,7 @@ async function checkAliyun(link) {
}
}
function extractParamsAliPan(urlStr) {
export function extractParamsAliPan(urlStr) {
try {
const u = new URL(urlStr);
const pathParts = u.pathname.replace(/\/+$/, '').split('/').filter(Boolean);
@@ -63,5 +60,3 @@ function extractParamsAliPan(urlStr) {
return { shareId: '', error: e.message };
}
}
module.exports = { checkAliyun, extractParamsAliPan };
@@ -1,11 +1,8 @@
const { request } = require('./http');
// @ts-nocheck
/**
*
* URL格式: https://pan.baidu.com/s/{surl}?pwd={password}
* 两步检测: 有密码时先验证密码获取randskshare/list
*/
async function checkBaidu(link) {
import { request } from './http';
export async function checkBaidu(link) {
const normalizedLink = normalizeBaiduURL(link);
if (!normalizedLink) {
return { valid: false, reason: '未找到有效的百度网盘URL' };
@@ -26,8 +23,6 @@ async function checkBaidu(link) {
try {
let bdclnd = '';
// 如果有提取码,先验证
if (password) {
const verifyURL = `https://pan.baidu.com/share/verify?surl=${encodeURIComponent(shorturl)}&pwd=${encodeURIComponent(password)}`;
const formBody = `pwd=${encodeURIComponent(password)}&vcode=&vcode_str=`;
@@ -36,7 +31,7 @@ async function checkBaidu(link) {
body: formBody,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Referer': normalizedLink,
Referer: normalizedLink,
},
});
@@ -52,20 +47,16 @@ async function checkBaidu(link) {
bdclnd = vData.randsk || '';
}
// 调用 share/list API
const apiURL = `https://pan.baidu.com/share/list?web=5&app_id=250528&desc=1&showempty=0&page=1&num=20&order=time&shorturl=${encodeURIComponent(shorturl)}&root=1&view_mode=1&channel=chunlei&web=1&clienttype=0`;
const reqHeaders = {
'Accept': 'application/json, text/plain, */*',
Accept: 'application/json, text/plain, */*',
'Accept-Language': 'zh,en-GB;q=0.9,en-US;q=0.8,en;q=0.7,zh-CN;q=0.6',
};
if (bdclnd) {
reqHeaders['Cookie'] = `BDCLND=${bdclnd}`;
reqHeaders.Cookie = `BDCLND=${bdclnd}`;
}
const { statusCode, body } = await request(apiURL, {
headers: reqHeaders,
});
const { statusCode, body } = await request(apiURL, { headers: reqHeaders });
if (statusCode !== 200) {
return { valid: false, reason: `API返回错误状态码: ${statusCode}` };
}
@@ -73,7 +64,6 @@ async function checkBaidu(link) {
const result = JSON.parse(body);
const errno = result.errno;
const errMsg = result.errmsg || result.err_msg || '';
if (errno === 0) {
return { valid: true, reason: '' };
}
@@ -87,12 +77,10 @@ async function checkBaidu(link) {
}
}
function normalizeBaiduURL(link) {
export function normalizeBaiduURL(link) {
const cleaned = link.trim();
const startIdx = cleaned.indexOf('https://pan.baidu.com/s/');
if (startIdx === -1) {
return null;
}
if (startIdx === -1) return null;
let endIdx = startIdx;
while (endIdx < cleaned.length) {
const char = cleaned[endIdx];
@@ -103,7 +91,7 @@ function normalizeBaiduURL(link) {
return cleaned.substring(startIdx, endIdx).trim();
}
function extractBaiduShareID(shareURL) {
export function extractBaiduShareID(shareURL) {
try {
const u = new URL(shareURL);
if (u.pathname.startsWith('/s/')) {
@@ -123,11 +111,9 @@ function getFailureReason(errno, errMsg) {
if (errMsg) return `分享链接无效 (errno: ${errno}, err_msg: ${errMsg})`;
switch (errno) {
case -12: return '缺少提取码 (errno: -12)';
case -9: return '提取码错误 (errno: -9)';
case -9: return '提取码错误 (errno: -9)';
case -62: return '请求接口受限 (errno: -62)';
case -8: return '分享文件已过期 (errno: -8)';
default: return `分享链接无效 (errno: ${errno})`;
case -8: return '分享文件已过期 (errno: -8)';
default: return `分享链接无效 (errno: ${errno})`;
}
}
module.exports = { checkBaidu, normalizeBaiduURL, extractBaiduShareID };
@@ -1,44 +1,28 @@
const crypto = require('crypto');
const { request } = require('./http');
// @ts-nocheck
/**
*
* URL格式:
* https://yun.139.com/shareweb/#/w/i/{shareID}
* https://caiyun.139.com/m/i?{shareID}
* API: POST https://share-kd-njs.yun.139.com/yun-share/richlifeApp/devapp/IOutLink/getOutLinkInfoV6
* AES-CBC加密
*/
import crypto from 'crypto';
import { request } from './http';
const CMCC_AES_KEY = 'PVGDwmcvfs1uV3d1';
function aesCBCEncrypt(plaintext, key) {
export function aesCBCEncrypt(plaintext, key) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-128-cbc', Buffer.from(key, 'utf-8'), iv);
// PKCS7 padding
const blockSize = 16;
const padLen = blockSize - (Buffer.byteLength(plaintext, 'utf-8') % blockSize);
const padded = Buffer.concat([
Buffer.from(plaintext, 'utf-8'),
Buffer.alloc(padLen, padLen),
]);
const padded = Buffer.concat([Buffer.from(plaintext, 'utf-8'), Buffer.alloc(padLen, padLen)]);
const encrypted = Buffer.concat([cipher.update(padded), cipher.final()]);
return Buffer.concat([iv, encrypted]).toString('base64');
}
function aesCBCDecrypt(encryptedBase64, key) {
export function aesCBCDecrypt(encryptedBase64, key) {
const rawData = Buffer.from(encryptedBase64, 'base64');
if (rawData.length < 16) throw new Error('加密数据长度不足');
const iv = rawData.subarray(0, 16);
const ciphertext = rawData.subarray(16);
const decipher = crypto.createDecipheriv('aes-128-cbc', Buffer.from(key, 'utf-8'), iv);
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
// 去除PKCS7填充
const padLen = decrypted[decrypted.length - 1];
if (padLen > 0 && padLen <= 16) {
return decrypted.subarray(0, decrypted.length - padLen).toString('utf-8');
@@ -46,7 +30,7 @@ function aesCBCDecrypt(encryptedBase64, key) {
return decrypted.toString('utf-8');
}
async function checkCMCC(link) {
export async function checkCMCC(link) {
const shareID = extractShareID(link);
if (!shareID) {
return { valid: false, reason: '链接格式无效:无法提取分享ID' };
@@ -71,10 +55,7 @@ async function checkCMCC(link) {
},
};
const jsonStr = JSON.stringify(requestData);
const encryptedData = aesCBCEncrypt(jsonStr, CMCC_AES_KEY);
const encryptedJSON = JSON.stringify(encryptedData);
const encryptedJSON = JSON.stringify(aesCBCEncrypt(JSON.stringify(requestData), CMCC_AES_KEY));
const { statusCode, body } = await request(
'https://share-kd-njs.yun.139.com/yun-share/richlifeApp/devapp/IOutLink/getOutLinkInfoV6',
{
@@ -82,7 +63,7 @@ async function checkCMCC(link) {
body: encryptedJSON,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json, text/plain, */*',
Accept: 'application/json, text/plain, */*',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
'hcy-cool-flag': '1',
'x-deviceinfo': '||3|12.27.0|chrome|131.0.0.0|5c7c68368f048245e1ce47f1c0f8f2d0||windows 10|1536X695|zh-CN|||',
@@ -94,19 +75,12 @@ async function checkCMCC(link) {
return { valid: false, reason: `API返回错误状态码: ${statusCode}` };
}
// 解密响应
const decryptedData = aesCBCDecrypt(body.trim(), CMCC_AES_KEY);
const response = JSON.parse(decryptedData);
const resultCode = response.resultCode;
const desc = response.desc;
const data = response.data;
if (resultCode === '0' && data != null) {
const response = JSON.parse(aesCBCDecrypt(body.trim(), CMCC_AES_KEY));
if (response.resultCode === '0' && response.data != null) {
return { valid: true, reason: '' };
}
const failReason = desc || (resultCode ? `错误码: ${resultCode}` : '获取分享信息失败');
const failReason = response.desc || (response.resultCode ? `错误码: ${response.resultCode}` : '获取分享信息失败');
return { valid: false, reason: failReason };
} catch (err) {
if (err.message === '请求超时') return { valid: false, reason: '请求超时' };
@@ -114,9 +88,7 @@ async function checkCMCC(link) {
}
}
function extractShareID(shareURL) {
export function extractShareID(shareURL) {
const match = shareURL.match(/https:\/\/(?:yun\.139\.com\/shareweb\/#\/w\/i\/|caiyun\.139\.com\/m\/i\?)([^&]+)/);
return match ? match[1] : '';
}
module.exports = { checkCMCC, extractShareID, aesCBCEncrypt, aesCBCDecrypt };
@@ -1,24 +1,25 @@
const https = require('https');
const http = require('http');
const { URL } = require('url');
// @ts-nocheck
const DEFAULT_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36';
const DEFAULT_HEADERS = {
'accept': 'application/json;charset=UTF-8',
import http from 'http';
import https from 'https';
export const DEFAULT_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36';
export const DEFAULT_HEADERS = {
accept: 'application/json;charset=UTF-8',
'accept-language': 'en,zh-CN;q=0.9,zh;q=0.8',
'user-agent': DEFAULT_UA,
'cache-control': 'no-cache',
'pragma': 'no-cache',
pragma: 'no-cache',
};
function request(url, options = {}) {
export function request(url, options = {}) {
return new Promise((resolve, reject) => {
const timeout = options.timeout || 15000;
const parsedUrl = new URL(url);
const transport = parsedUrl.protocol === 'https:' ? https : http;
const headers = { ...DEFAULT_HEADERS, ...(options.headers || {}) };
delete headers['Content-Type']; // handled below
delete headers['Content-Type'];
const reqOptions = {
hostname: parsedUrl.hostname,
@@ -56,5 +57,3 @@ function request(url, options = {}) {
req.end();
});
}
module.exports = { request, DEFAULT_UA, DEFAULT_HEADERS };
@@ -1,11 +1,8 @@
const { request } = require('./http');
// @ts-nocheck
/**
* 115
* URL格式: https://115cdn.com/s/{share_code}?password={receive_code}
* API: GET https://115cdn.com/webapi/share/snap
*/
async function check115(link) {
import { request } from './http';
export async function check115(link) {
const { shareCode, receiveCode, error: parseError } = extractParams115(link);
if (parseError || !shareCode || !receiveCode) {
return { valid: false, reason: parseError || (!shareCode ? '缺少分享码' : '缺少提取码') };
@@ -15,7 +12,7 @@ async function check115(link) {
const apiURL = `https://115cdn.com/webapi/share/snap?share_code=${encodeURIComponent(shareCode)}&offset=0&limit=20&receive_code=${encodeURIComponent(receiveCode)}&cid=`;
const { statusCode, body } = await request(apiURL, {
headers: {
'Referer': `https://115cdn.com/s/${shareCode}?password=${receiveCode}&`,
Referer: `https://115cdn.com/s/${shareCode}?password=${receiveCode}&`,
'Sec-Ch-Ua': '"Chromium";v="142", "Google Chrome";v="142", "Not_A Brand";v="99"',
'Sec-Ch-Ua-Mobile': '?0',
'Sec-Ch-Ua-Platform': '"Windows"',
@@ -23,7 +20,7 @@ async function check115(link) {
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
'X-Requested-With': 'XMLHttpRequest',
'Priority': 'u=1, i',
Priority: 'u=1, i',
},
});
@@ -32,10 +29,8 @@ async function check115(link) {
}
const data = JSON.parse(body);
if (data.state === true && data.errno === 0) {
let shareState = data.data?.share_state || 0;
// 兼容部分响应只在 shareinfo 中返回 share_state
if (shareState === 0 && data.data?.shareinfo?.share_state) {
shareState = data.data.shareinfo.share_state;
}
@@ -44,8 +39,7 @@ async function check115(link) {
return { valid: true, reason: '' };
}
const failReason = (data.data?.shareinfo?.forbid_reason || '').trim()
|| `链接状态异常(share_state=${shareState})`;
const failReason = (data.data?.shareinfo?.forbid_reason || '').trim() || `链接状态异常(share_state=${shareState})`;
return { valid: false, reason: failReason };
}
@@ -56,7 +50,7 @@ async function check115(link) {
}
}
function extractParams115(urlStr) {
export function extractParams115(urlStr) {
try {
const u = new URL(urlStr);
const pathParts = u.pathname.replace(/\/+$/, '').split('/');
@@ -73,5 +67,3 @@ function extractParams115(urlStr) {
return { shareCode: '', receiveCode: '', error: e.message };
}
}
module.exports = { check115, extractParams115 };
@@ -1,13 +1,8 @@
const { request } = require('./http');
// @ts-nocheck
/**
* 123
* URL格式: https://www.123pan.com/s/{shareKey}
* API: GET https://www.123pan.com/api/share/info?shareKey={shareKey}
*
* 注意: 此检测器采用保守策略/403/
*/
async function check123(link) {
import { request } from './http';
export async function check123(link) {
const { shareKey, error: parseError } = extractShareKey123(link);
if (parseError) {
return { valid: false, reason: '链接格式无效: ' + parseError };
@@ -21,35 +16,27 @@ async function check123(link) {
},
});
// 403视为有效(访问限制,不是链接失效)
if (statusCode === 403) {
return { valid: true, reason: '' };
}
if (statusCode !== 200) {
return { valid: true, reason: '' }; // 非预期状态码也视为有效,避免误判
}
if (statusCode === 403) return { valid: true, reason: '' };
if (statusCode !== 200) return { valid: true, reason: '' };
let data;
try {
data = JSON.parse(body);
} catch (_) {
return { valid: true, reason: '' }; // JSON解析错误视为有效
return { valid: true, reason: '' };
}
// code==0 或 HasPwd==true 均视为有效
if (data.code === 0 || data.data?.HasPwd === true) {
return { valid: true, reason: '' };
}
return { valid: false, reason: '链接已失效' };
} catch (err) {
// 超时和请求错误均视为有效,避免误判
} catch (_) {
return { valid: true, reason: '' };
}
}
function extractShareKey123(urlStr) {
export function extractShareKey123(urlStr) {
const patterns = [
/https?:\/\/(?:www\.)?(?:123684|123685|123912|123pan|123592|123865)\.com\/s\/([a-zA-Z0-9-]+)/,
/https?:\/\/(?:www\.)?123pan\.cn\/s\/([a-zA-Z0-9-]+)/,
@@ -62,7 +49,6 @@ function extractShareKey123(urlStr) {
}
}
// Fallback: 从URL路径中提取
try {
const u = new URL(urlStr);
const pathParts = u.pathname.replace(/\/+$/, '').split('/').filter(Boolean);
@@ -73,5 +59,3 @@ function extractShareKey123(urlStr) {
return { shareKey: '', error: '无法从URL中提取shareKey' };
}
module.exports = { check123, extractShareKey123 };
@@ -1,18 +1,14 @@
const { request } = require('./http');
// @ts-nocheck
/**
*
* URL格式: https://pan.quark.cn/s/{pwd_id}?pwd={passcode}
* 两步检测: 先获取stoken
*/
async function checkQuark(link) {
import { request } from './http';
export async function checkQuark(link) {
const { resId, pwd, error: parseError } = extractParamsQuark(link);
if (parseError) {
return { valid: false, reason: '链接格式无效: ' + parseError };
}
try {
// Step 1: 获取 stoken
const tokenURL = 'https://drive-h.quark.cn/1/clouddrive/share/sharepage/token';
const { statusCode: status1, body: body1 } = await request(tokenURL, {
method: 'POST',
@@ -23,8 +19,8 @@ async function checkQuark(link) {
},
headers: {
'Content-Type': 'application/json',
'Origin': 'https://pan.quark.cn',
'Referer': 'https://pan.quark.cn/',
Origin: 'https://pan.quark.cn',
Referer: 'https://pan.quark.cn/',
},
});
@@ -40,16 +36,15 @@ async function checkQuark(link) {
return { valid: false, reason: '分享链接无效:未获取到访问令牌' };
}
// Step 2: 获取文件列表
const detailURL = `https://drive-pc.quark.cn/1/clouddrive/share/sharepage/detail?pwd_id=${encodeURIComponent(resId)}&stoken=${encodeURIComponent(tokenResp.data.stoken)}&ver=2&pr=ucpro`;
const { statusCode: status2, body: body2 } = await request(detailURL, {
headers: {
'Accept': 'application/json, text/plain, */*',
Accept: 'application/json, text/plain, */*',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Cache-Control': 'no-cache',
'Origin': 'https://pan.quark.cn',
'Referer': 'https://pan.quark.cn/',
'Pragma': 'no-cache',
Origin: 'https://pan.quark.cn',
Referer: 'https://pan.quark.cn/',
Pragma: 'no-cache',
},
});
@@ -69,7 +64,7 @@ async function checkQuark(link) {
}
}
function extractParamsQuark(rawURL) {
export function extractParamsQuark(rawURL) {
const urlRegex = /^https:\/\/(?:pan\.quark\.cn|pan\.qoark\.cn)\/s\/[a-zA-Z0-9]+(?:\?[^#]*)?(?:#.*)?$/;
if (!urlRegex.test(rawURL)) {
return { resId: '', pwd: '', error: '无效的URL格式' };
@@ -93,5 +88,3 @@ function extractParamsQuark(rawURL) {
return { resId: '', pwd: '', error: e.message };
}
}
module.exports = { checkQuark, extractParamsQuark };
@@ -1,14 +1,8 @@
const { request } = require('./http');
// @ts-nocheck
/**
*
* URL格式:
* https://cloud.189.cn/web/share?code=xxx
* https://cloud.189.cn/t/xxx
* https://h5.cloud.189.cn/share.html#/t/xxx
* API: GET https://cloud.189.cn/api/open/share/getShareInfoByCodeV2.action
*/
async function checkTianyi(link) {
import { request } from './http';
export async function checkTianyi(link) {
const { codeValue, accessCode, refererValue, error: parseError } = extractCodeFromURL(link);
if (parseError) {
return { valid: false, reason: '链接格式无效: ' + parseError };
@@ -16,7 +10,6 @@ async function checkTianyi(link) {
try {
const noCache = Math.random();
// 如果有访问码,需要将访问码包含在shareCode参数中
let shareCodeParam = codeValue;
if (accessCode) {
shareCodeParam = `${codeValue}(访问码:${accessCode}`;
@@ -25,8 +18,8 @@ async function checkTianyi(link) {
const apiURL = `https://cloud.189.cn/api/open/share/getShareInfoByCodeV2.action?noCache=${noCache}&shareCode=${encodeURIComponent(shareCodeParam)}`;
const { statusCode, body } = await request(apiURL, {
headers: {
'Priority': 'u=1, i',
'Referer': refererValue,
Priority: 'u=1, i',
Referer: refererValue,
'Sec-Ch-Ua': '"Chromium";v="142", "Google Chrome";v="142", "Not_A Brand";v="99"',
'Sec-Ch-Ua-Mobile': '?0',
'Sec-Ch-Ua-Platform': '"Windows"',
@@ -54,21 +47,17 @@ async function checkTianyi(link) {
}
}
function extractCodeFromURL(urlStr) {
export function extractCodeFromURL(urlStr) {
try {
const u = new URL(urlStr);
let codeValue = '';
let accessCode = '';
// 1. 从查询参数获取code
codeValue = u.searchParams.get('code') || '';
// 2. 从路径获取 /t/xxx
if (!codeValue && u.pathname.startsWith('/t/')) {
codeValue = u.pathname.replace('/t/', '').split('/')[0];
}
// 3. 从hash获取 #/t/xxx
if (!codeValue && u.hash) {
const fragment = u.hash.replace(/^#/, '');
if (fragment.startsWith('/t/')) {
@@ -82,9 +71,7 @@ function extractCodeFromURL(urlStr) {
return { codeValue: '', accessCode: '', refererValue: '', error: '输入URL中未找到code参数' };
}
// 提取访问码(访问码:xxx
const accessCodePattern = /[(]访问码[:]\s*([a-zA-Z0-9]+)[)]/;
const match = urlStr.match(accessCodePattern);
const match = urlStr.match(/[(]访问码[:]\s*([a-zA-Z0-9]+)[)]/);
if (match && match[1]) {
accessCode = match[1];
}
@@ -94,5 +81,3 @@ function extractCodeFromURL(urlStr) {
return { codeValue: '', accessCode: '', refererValue: '', error: e.message };
}
}
module.exports = { checkTianyi, extractCodeFromURL };
-58
View File
@@ -1,58 +0,0 @@
const { request, DEFAULT_UA } = require('./http');
/**
* UC网盘链接检测
* URL格式: https://drive.uc.cn/s/{shareID}
* 检测方法: 页面爬取,通过关键词判断有效性
*/
async function checkUC(link) {
const { shareID, error: parseError } = extractShareIDFromURL(link);
if (parseError) {
return { valid: false, reason: '链接格式无效: ' + parseError };
}
try {
const url = `https://drive.uc.cn/s/${shareID}`;
const { statusCode, body } = await request(url, {
headers: {
'User-Agent': 'Mozilla/5.0 (Linux; Android 10; SM-G975F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.101 Mobile Safari/537.36',
},
});
if (statusCode !== 200) {
return { valid: false, reason: `HTTP状态码: ${statusCode}` };
}
const pageText = body.toLowerCase();
const errorKeywords = ['失效', '不存在', '违规', '删除', '已过期', '被取消'];
for (const keyword of errorKeywords) {
if (pageText.includes(keyword)) {
return { valid: false, reason: '链接已失效' };
}
}
const validKeywords = ['文件', '分享'];
for (const keyword of validKeywords) {
if (pageText.includes(keyword)) {
return { valid: true, reason: '' };
}
}
return { valid: false, reason: '无法判断链接有效性' };
} catch (err) {
if (err.message === '请求超时') return { valid: true, reason: '' }; // 超时视为有效,避免误判
return { valid: true, reason: '' }; // 连接错误也视为有效,避免误判
}
}
function extractShareIDFromURL(urlStr) {
const pattern = /https?:\/\/drive\.uc\.cn\/s\/([a-zA-Z0-9]+)/;
const match = urlStr.match(pattern);
if (match && match[1]) {
return { shareID: match[1], error: null };
}
return { shareID: '', error: '无法从URL中提取share_id' };
}
module.exports = { checkUC, extractShareIDFromURL };
+44
View File
@@ -0,0 +1,44 @@
// @ts-nocheck
import { request } from './http';
export async function checkUC(link) {
const { shareID, error: parseError } = extractShareIDFromURL(link);
if (parseError) {
return { valid: false, reason: '链接格式无效: ' + parseError };
}
try {
const url = `https://drive.uc.cn/s/${shareID}`;
const { statusCode, body } = await request(url, {
headers: {
'User-Agent': 'Mozilla/5.0 (Linux; Android 10; SM-G975F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.101 Mobile Safari/537.36',
},
});
if (statusCode !== 200) {
return { valid: false, reason: `HTTP状态码: ${statusCode}` };
}
const pageText = body.toLowerCase();
for (const keyword of ['失效', '不存在', '违规', '删除', '已过期', '被取消']) {
if (pageText.includes(keyword)) return { valid: false, reason: '链接已失效' };
}
for (const keyword of ['文件', '分享']) {
if (pageText.includes(keyword)) return { valid: true, reason: '' };
}
return { valid: false, reason: '无法判断链接有效性' };
} catch (err) {
if (err.message === '请求超时') return { valid: true, reason: '' };
return { valid: true, reason: '' };
}
}
export function extractShareIDFromURL(urlStr) {
const match = urlStr.match(/https?:\/\/drive\.uc\.cn\/s\/([a-zA-Z0-9]+)/);
if (match && match[1]) {
return { shareID: match[1], error: null };
}
return { shareID: '', error: '无法从URL中提取share_id' };
}
@@ -1,12 +1,8 @@
const crypto = require('crypto');
const zlib = require('zlib');
const { request } = require('./http');
// @ts-nocheck
/**
*
* URL格式: https://pan.xunlei.com/s/{share_id}?pwd={pass_code}
* 两步检测: 先获取captcha tokenshare API
*/
import crypto from 'crypto';
import zlib from 'zlib';
import { request } from './http';
const XUNLEI_DEVICE_ID = '5505bd0cab8c9469b98e5891d9fb3e0d';
const XUNLEI_CLIENT_ID = 'ZUBzD9J_XPXfn7f7';
@@ -26,18 +22,16 @@ const CAPTCHA_ALGORITHMS = [
'ThTWPG5eC0UBqlbQ+04nZAptqGCdpv9o55A',
];
function getCaptchaSign(clientID, clientVersion, packageName, deviceID) {
export function getCaptchaSign(clientID, clientVersion, packageName, deviceID) {
const timestamp = Date.now().toString();
let str = `${clientID}${clientVersion}${packageName}${deviceID}${timestamp}`;
for (const algorithm of CAPTCHA_ALGORITHMS) {
str = crypto.createHash('md5').update(str + algorithm).digest('hex');
}
return { timestamp, sign: `1.${str}` };
}
async function getCaptchaToken(action, metas = {}) {
export async function getCaptchaToken(action, metas = {}) {
const { timestamp, sign: captchaSign } = getCaptchaSign(
XUNLEI_CLIENT_ID, XUNLEI_CLIENT_VERSION, XUNLEI_PACKAGE_NAME, XUNLEI_DEVICE_ID
);
@@ -47,22 +41,20 @@ async function getCaptchaToken(action, metas = {}) {
metas.client_version = XUNLEI_CLIENT_VERSION;
metas.package_name = XUNLEI_PACKAGE_NAME;
const requestBody = {
action,
captcha_token: '',
client_id: XUNLEI_CLIENT_ID,
device_id: XUNLEI_DEVICE_ID,
meta: metas,
redirect_uri: 'xlaccsdk01://xunlei.com/callback?state=harbor',
};
const { statusCode, body, headers } = await request(
'https://xluser-ssl.xunlei.com/v1/shield/captcha/init',
{
method: 'POST',
body: requestBody,
body: {
action,
captcha_token: '',
client_id: XUNLEI_CLIENT_ID,
device_id: XUNLEI_DEVICE_ID,
meta: metas,
redirect_uri: 'xlaccsdk01://xunlei.com/callback?state=harbor',
},
headers: {
'Accept': 'application/json;charset=UTF-8',
Accept: 'application/json;charset=UTF-8',
'Content-Type': 'application/json',
'User-Agent': XUNLEI_UA,
'X-Device-Id': XUNLEI_DEVICE_ID,
@@ -77,7 +69,6 @@ async function getCaptchaToken(action, metas = {}) {
}
let respBody = body;
// 解压 gzip/deflate
const encoding = (headers['content-encoding'] || '').toLowerCase();
if (encoding === 'gzip') {
respBody = zlib.gunzipSync(Buffer.from(body, 'binary')).toString('utf-8');
@@ -86,16 +77,12 @@ async function getCaptchaToken(action, metas = {}) {
}
const data = JSON.parse(respBody);
if (data.url) {
throw new Error(`需要验证: ${data.url}`);
}
if (!data.captcha_token) {
throw new Error('未获取到验证码token');
}
if (data.url) throw new Error(`需要验证: ${data.url}`);
if (!data.captcha_token) throw new Error('未获取到验证码token');
return data.captcha_token;
}
async function checkXunlei(link) {
export async function checkXunlei(link) {
const shareID = extractShareID(link);
if (!shareID) {
return { valid: false, reason: '链接格式无效:无法提取share_id' };
@@ -108,7 +95,6 @@ async function checkXunlei(link) {
} catch (_) {}
try {
// Step 1: 获取 captcha token
let captchaToken = '';
try {
captchaToken = await getCaptchaToken('get:/drive/v1/share', {
@@ -119,17 +105,14 @@ async function checkXunlei(link) {
client_version: '1.92.10',
user_id: '0',
});
} catch (_) {
// token获取失败时继续,不带token请求
}
} catch (_) {}
// Step 2: 调用share API
const apiURL = `https://api-pan.xunlei.com/drive/v1/share?share_id=${encodeURIComponent(shareID)}&pass_code=${encodeURIComponent(passCode)}&limit=100&pass_code_token=&page_token=&thumbnail_size=SIZE_SMALL`;
const reqHeaders = {
'Accept': '*/*',
Accept: '*/*',
'Content-Type': 'application/json',
'Origin': 'https://pan.xunlei.com',
'Referer': 'https://pan.xunlei.com/',
Origin: 'https://pan.xunlei.com',
Referer: 'https://pan.xunlei.com/',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36',
'Accept-Encoding': 'gzip, deflate',
'X-Client-Id': XUNLEI_CLIENT_ID,
@@ -140,9 +123,7 @@ async function checkXunlei(link) {
}
const { statusCode, body, headers } = await request(apiURL, { headers: reqHeaders });
let respBody = body;
// 解压
const encoding = (headers['content-encoding'] || '').toLowerCase();
if (encoding === 'gzip') {
respBody = zlib.gunzipSync(Buffer.from(body, 'binary')).toString('utf-8');
@@ -151,14 +132,12 @@ async function checkXunlei(link) {
}
if (statusCode !== 200) {
let isRateLimited = false;
try {
const errData = JSON.parse(respBody);
if (errData.error_code === 9) isRateLimited = true;
return {
valid: false,
reason: `HTTP状态码: ${statusCode}, 响应: ${respBody}`,
isRateLimited,
isRateLimited: errData.error_code === 9,
};
} catch (_) {
return { valid: false, reason: `HTTP状态码: ${statusCode}` };
@@ -166,26 +145,16 @@ async function checkXunlei(link) {
}
const apiResp = JSON.parse(respBody);
if (apiResp.share_status === 'OK') {
return { valid: true, reason: '' };
}
if (apiResp.error) {
return { valid: false, reason: apiResp.error };
}
const statusText = apiResp.share_status_text || `分享状态: ${apiResp.share_status}`;
return { valid: false, reason: statusText };
if (apiResp.share_status === 'OK') return { valid: true, reason: '' };
if (apiResp.error) return { valid: false, reason: apiResp.error };
return { valid: false, reason: apiResp.share_status_text || `分享状态: ${apiResp.share_status}` };
} catch (err) {
if (err.message === '请求超时') return { valid: true, reason: '' }; // 超时视为有效,避免误判
if (err.message === '请求超时') return { valid: true, reason: '' };
return { valid: false, reason: `检测失败: ${err.message}` };
}
}
function extractShareID(shareURL) {
export function extractShareID(shareURL) {
const match = shareURL.match(/pan\.xunlei\.com\/s\/([^?/#]+)/);
return match ? match[1] : '';
}
module.exports = { checkXunlei, extractShareID, getCaptchaSign, getCaptchaToken };