// ==UserScript==
// @name 自动登录CSDN
// @namespace https://csdn-download.local
// @version 1.5.0
// @description CSDN下载页辅助:移除登录弹窗/遮罩、提取真实文件地址;未登录时后台API静默登录(API变动自动发现新地址),失败再跳登录页自动填账密
// @author 自写脚本
// @match *://download.csdn.net/*
// @match *://blog.csdn.net/*/article/details/*
// @match *://passport.csdn.net/*
// @run-at document-start
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @grant unsafeWindow
// @connect passport.csdn.net
// @connect csdnimg.cn
// @license MIT
// ==/UserScript==
(function () {
'use strict';
var VERSION = '1.5.0';
/* ============================================================
* 账号配置:不内置账密,需通过脚本菜单「设置账号密码」自行填写
* ============================================================ */
function getAccount() {
var u = '', p = '';
try {
if (typeof GM_getValue === 'function') {
u = GM_getValue('csdn_user', '');
p = GM_getValue('csdn_pwd', '');
}
} catch (e) {}
return { user: u, pwd: p };
}
function hasAccount() {
var acc = getAccount();
return !!(acc.user && acc.pwd);
}
var HOST = location.hostname;
/* ============================================================
* 通用工具
* ============================================================ */
// 原生赋值(兼容 React/Vue 受控输入框)
function setNativeValue(el, value) {
try {
var proto = el.__proto__;
var desc = Object.getOwnPropertyDescriptor(proto, 'value');
if (desc && desc.set) {
desc.set.call(el, value);
} else {
el.value = value;
}
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
} catch (e) {
el.value = value;
}
}
function isLoggedIn() {
// CSDN 登录后的特征 Cookie
var c = document.cookie || '';
return c.indexOf('UserToken=') >= 0 || c.indexOf('UserName=') >= 0;
}
function showTip(text, color) {
var old = document.getElementById('csdn-dl-tip');
if (old) old.parentNode.removeChild(old);
var tip = document.createElement('div');
tip.id = 'csdn-dl-tip';
tip.style.cssText = 'position:fixed;top:20px;left:50%;transform:translateX(-50%);z-index:9999999;' +
'background:' + (color || 'rgba(20,20,30,0.95)') + ';color:#fff;padding:12px 22px;border-radius:8px;' +
'font:14px/1.5 sans-serif;box-shadow:0 4px 16px rgba(0,0,0,0.4);';
tip.textContent = text;
(document.body || document.documentElement).appendChild(tip);
return tip;
}
/* ============================================================
* A. 登录页逻辑:自动填账密 + 自动点登录
* ============================================================ */
function runLoginPage() {
console.log('[CSDN-DL] 登录页,准备自动登录');
if (!hasAccount()) {
setTimeout(showAccountTip, 500);
return;
}
var filled = false;
var clicked = false;
function tryFillAndLogin() {
// 找账号框:可见的非密码文本框
var userInput = null;
var inputs = document.querySelectorAll('input');
for (var i = 0; i < inputs.length; i++) {
var inp = inputs[i];
var t = (inp.type || '').toLowerCase();
if (t === 'password') continue;
if (t !== 'text' && t !== 'tel' && t !== 'email' && t !== '') continue;
var ph = (inp.placeholder || '') + (inp.name || '') + (inp.id || '');
if (/账号|手机号|邮箱|用户名|user|account|phone|email|login/i.test(ph) || !userInput) {
userInput = inp;
if (/账号|手机号|邮箱|用户名|user|account/i.test(ph)) break;
}
}
// 找密码框
var pwdInput = document.querySelector('input[type="password"]');
if (userInput && pwdInput && !filled) {
filled = true;
var acc = getAccount();
setNativeValue(userInput, acc.user);
setNativeValue(pwdInput, acc.pwd);
console.log('[CSDN-DL] 已填写账密');
showTip('已自动填写账密,正在登录…');
setTimeout(clickLogin, 600);
}
}
function clickLogin() {
if (clicked) return;
// 找登录按钮:button 或含"登录"文字的可点元素
var candidates = document.querySelectorAll('button, [class*="log"], [class*="btn"], a');
for (var i = 0; i < candidates.length; i++) {
var el = candidates[i];
var txt = (el.textContent || '').trim();
if (txt === '登录' || txt === '登 录' || /^登\s*录$/.test(txt)) {
// 排除"登录/注册"导航链接(通常在登录页顶部)
if (el.closest && el.closest('.toolbar, nav, header')) continue;
clicked = true;
el.click();
console.log('[CSDN-DL] 已点击登录按钮');
watchLoginResult();
return;
}
}
}
function watchLoginResult() {
var n = 0;
var timer = setInterval(function () {
n++;
// 离开登录页 = 登录成功
if (location.hostname !== 'passport.csdn.net') {
clearInterval(timer);
return;
}
// 检测验证码/滑块出现
var captcha = document.querySelector('[class*="captcha"], [class*="verify"], [class*="slide"], [id*="captcha"], [class*="geetest"], [class*="nc_"]');
if (captcha) {
clearInterval(timer);
showTip('出现验证码/滑块,请手动完成验证', 'rgba(200,80,20,0.95)');
return;
}
if (n > 10) { // 10 秒后仍在登录页
clearInterval(timer);
showTip('自动登录未完成,请检查是否需要验证码或手动登录', 'rgba(200,80,20,0.95)');
}
}, 1000);
}
// 等 DOM 出现输入框
var mo = new MutationObserver(function () {
tryFillAndLogin();
});
function startObs() {
if (document.documentElement) {
mo.observe(document.documentElement, { childList: true, subtree: true });
}
tryFillAndLogin();
setTimeout(tryFillAndLogin, 1000);
setTimeout(tryFillAndLogin, 2500);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', startObs);
} else {
startObs();
}
}
/* ============================================================
* B. 内容页逻辑:登录态检测 + 去弹窗 + 提取地址
* ============================================================ */
var LOGIN_SELECTORS = [
'.passport-login-container',
'.passport-login-pop-container',
'[class*="login-modal"]',
'[class*="login-dialog"]',
'[class*="login-mask"]',
'[id*="passport-login"]'
];
function injectCSS() {
var css = LOGIN_SELECTORS.join(',') + '{display:none!important;visibility:hidden!important;}';
css += 'body{overflow:auto!important;}';
try {
if (typeof GM_addStyle === 'function') { GM_addStyle(css); return; }
} catch (e) {}
function doInject() {
var parent = document.head || document.documentElement;
if (!parent) { setTimeout(doInject, 10); return; }
var style = document.createElement('style');
style.textContent = css;
style.setAttribute('data-script', 'csdn-dl');
parent.appendChild(style);
}
doInject();
}
function removeLoginPopups() {
for (var i = 0; i < LOGIN_SELECTORS.length; i++) {
var els = document.querySelectorAll(LOGIN_SELECTORS[i]);
for (var j = 0; j < els.length; j++) {
try { els[j].parentNode && els[j].parentNode.removeChild(els[j]); } catch (e) {}
}
}
try {
document.documentElement.style.setProperty('overflow', 'auto', 'important');
if (document.body) document.body.style.setProperty('overflow', 'auto', 'important');
} catch (e) {}
}
function installRedirectBlocker() {
var _open = window.open;
window.open = function (url) {
if (url && String(url).indexOf('passport.csdn.net') >= 0) {
console.log('[CSDN-DL] 拦截登录页弹窗:', url);
return null;
}
return _open.apply(this, arguments);
};
}
function extractDownloadUrl() {
try {
var state = window.__INITIAL_STATE__ || window.__NUXT__ || {};
var found = [];
JSON.stringify(state, function (k, v) {
if (typeof v === 'string' &&
(v.indexOf('.zip') >= 0 || v.indexOf('.rar') >= 0 ||
v.indexOf('.7z') >= 0 || v.indexOf('.pdf') >= 0 ||
v.indexOf('.tar') >= 0 || v.indexOf('.gz') >= 0) &&
(v.indexOf('http') === 0)) {
found.push(v);
}
return v;
});
var scripts = document.querySelectorAll('script');
for (var i = 0; i < scripts.length; i++) {
var text = scripts[i].textContent || '';
var m = text.match(/"((?:https?:)?\/\/[^"]*?\.(?:zip|rar|7z|pdf|tar|gz)[^"]*)"/i);
if (m && m[1]) found.push(m[1]);
}
return found.length ? found[0] : null;
} catch (e) {
return null;
}
}
// 设置面板(通过脚本猫菜单"设置账号密码"打开)
function showSettingsPanel() {
var old = document.getElementById('csdn-dl-settings');
if (old) old.parentNode.removeChild(old);
var acc = getAccount();
var mask = document.createElement('div');
mask.id = 'csdn-dl-settings';
mask.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;z-index:99999999;' +
'background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;';
mask.innerHTML =
'
' +
'
CSDN免登录下载 设置 v' + VERSION + '
' +
'
账号
' +
'
' +
'
密码
' +
'
' +
'
' +
'取消' +
'保存' +
'
';
(document.body || document.documentElement).appendChild(mask);
document.getElementById('csdn-set-user').value = acc.user;
document.getElementById('csdn-set-pwd').value = acc.pwd;
document.getElementById('csdn-set-save').onclick = function () {
var u = document.getElementById('csdn-set-user').value.trim();
var p = document.getElementById('csdn-set-pwd').value;
try { GM_setValue('csdn_user', u); GM_setValue('csdn_pwd', p); } catch (e) {}
mask.parentNode.removeChild(mask);
removeAccountTip();
showTip('账号已保存', 'rgba(30,150,60,0.95)');
// 保存后若仍未登录,立即尝试登录;若在登录页则刷新触发自动填表
if (location.hostname === 'passport.csdn.net') {
setTimeout(function () { location.reload(); }, 600);
} else if (!isLoggedIn()) {
setTimeout(ensureLogin, 600);
}
};
document.getElementById('csdn-set-cancel').onclick = function () {
mask.parentNode.removeChild(mask);
};
mask.addEventListener('click', function (e) {
if (e.target === mask) mask.parentNode.removeChild(mask);
});
}
// 注册脚本猫菜单
try {
if (typeof GM_registerMenuCommand === 'function') {
GM_registerMenuCommand('设置账号密码', showSettingsPanel);
}
} catch (e) {}
// 未填账密时:网页右上角常驻提示,点击打开设置面板
function showAccountTip() {
if (document.getElementById('csdn-dl-noacc')) return;
var tip = document.createElement('div');
tip.id = 'csdn-dl-noacc';
tip.style.cssText = 'position:fixed;top:16px;right:16px;z-index:9999999;' +
'background:rgba(200,120,20,0.95);color:#fff;padding:10px 16px;border-radius:8px;' +
'font:13px/1.5 sans-serif;cursor:pointer;box-shadow:0 4px 12px rgba(0,0,0,0.3);';
tip.textContent = '未设置 CSDN 账号密码,点击此处填写';
tip.onclick = function () {
tip.parentNode && tip.parentNode.removeChild(tip);
showSettingsPanel();
};
(document.body || document.documentElement).appendChild(tip);
}
function removeAccountTip() {
var tip = document.getElementById('csdn-dl-noacc');
if (tip) tip.parentNode.removeChild(tip);
}
function enhanceDownloadButtons() {
var btns = document.querySelectorAll('a[href*="download"], button, .download-btn, [class*="download"]');
for (var i = 0; i < btns.length; i++) {
(function (btn) {
if (btn.__csdnDlHooked) return;
btn.__csdnDlHooked = true;
btn.addEventListener('click', function () {
setTimeout(removeLoginPopups, 300);
setTimeout(removeLoginPopups, 1000);
}, true);
})(btns[i]);
}
}
// 后台 API 静默登录(逆向自官方登录页:明文密码 + 空风控 token)
// 内置候选 API 列表(CSDN 改路径时自动逐个尝试)
var API_CANDIDATES = [
'https://passport.csdn.net/v1/register/pc/login/doLogin',
'https://passport.csdn.net/register/pc/login/doLogin'
];
// 向指定 API 发登录请求。回调 onResult(ok, msg, hardFail)
// ok=true 登录成功;hardFail=false 表示可尝试下一个候选(404/网络错等)
function postLogin(apiUrl, onResult) {
var acc = getAccount();
GM_xmlhttpRequest({
method: 'POST',
url: apiUrl,
headers: {
'Content-Type': 'application/json;charset=utf-8',
'X-Requested-With': 'XMLHttpRequest'
},
data: JSON.stringify({
userIdentification: acc.user,
pwdOrVerifyCode: acc.pwd,
loginType: '1',
webUmidToken: '',
uaToken: '',
agreedPrivacyPolicy: 1
}),
timeout: 12000,
onload: function (res) {
console.log('[CSDN-DL] 后台登录响应', apiUrl, 'HTTP', res.status, res.responseText);
if (res.status === 404 || res.status === 521 || res.status === 502 || res.status === 503) {
onResult(false, 'HTTP ' + res.status, false); // API 可能已变动,可换下一个
return;
}
var data = null;
try { data = JSON.parse(res.responseText); } catch (e) {}
if (data && data.status) {
onResult(true, '', true);
} else {
// 业务失败(密码错/要验证码):换 URL 无意义,硬失败
var msg = (data && (data.message || data.msg)) || ('HTTP ' + res.status + ' 非JSON响应');
onResult(false, msg, true);
}
},
onerror: function (e) {
console.log('[CSDN-DL] 请求错误', apiUrl, e);
onResult(false, 'network error', false);
},
ontimeout: function () { onResult(false, 'timeout', false); }
});
}
// 动态发现最新登录 API:抓登录页 HTML → 找 loginv3.*.js → 从 JS 提取 pcDoLogin 路径
function discoverApiUrl(callback) {
console.log('[CSDN-DL] 开始动态发现登录 API…');
GM_xmlhttpRequest({
method: 'GET',
url: 'https://passport.csdn.net/login',
timeout: 12000,
onload: function (res) {
var m = res.responseText.match(/src="(https?:\/\/[^"]*loginv3[^"]*\.js)"/) ||
res.responseText.match(/src="(https?:\/\/[^"]*login[^"]*\.js)"/);
if (!m) { console.log('[CSDN-DL] 未发现登录 JS'); callback(null); return; }
console.log('[CSDN-DL] 登录 JS:', m[1]);
GM_xmlhttpRequest({
method: 'GET',
url: m[1],
timeout: 15000,
onload: function (res2) {
var m2 = res2.responseText.match(/pcDoLogin\s*:\s*"([^"]+)"/);
if (!m2) { console.log('[CSDN-DL] JS 中未找到 pcDoLogin'); callback(null); return; }
var path = m2[1];
var url = path.indexOf('http') === 0 ? path : 'https://passport.csdn.net' + path;
console.log('[CSDN-DL] 发现最新 API:', url);
callback(url);
},
onerror: function () { callback(null); },
ontimeout: function () { callback(null); }
});
},
onerror: function () { callback(null); },
ontimeout: function () { callback(null); }
});
}
function tryApiLogin(onFail) {
if (typeof GM_xmlhttpRequest !== 'function') {
onFail('GM_xmlhttpRequest 不可用');
return;
}
showTip('未登录,正在后台静默登录…');
// 构建尝试队列:localStorage 缓存的上次成功地址 → 内置候选
var queue = [];
var cached = null;
try { cached = localStorage.getItem('csdn_dl_api_url'); } catch (e) {}
if (cached) queue.push(cached);
for (var i = 0; i < API_CANDIDATES.length; i++) {
if (queue.indexOf(API_CANDIDATES[i]) < 0) queue.push(API_CANDIDATES[i]);
}
function onSuccess(apiUrl) {
try { localStorage.setItem('csdn_dl_api_url', apiUrl); } catch (e) {}
showTip('后台登录成功,刷新页面…', 'rgba(30,150,60,0.95)');
setTimeout(function () { location.reload(); }, 800);
}
function tryNext() {
if (!queue.length) {
// 候选全失败 → 动态发现新 API
discoverApiUrl(function (newUrl) {
if (!newUrl) { onFail('API 候选全部失败且动态发现无果'); return; }
if (API_CANDIDATES.indexOf(newUrl) >= 0 || newUrl === cached) {
onFail('动态发现的 API 与已失败地址相同'); return;
}
postLogin(newUrl, function (ok, msg) {
if (ok) onSuccess(newUrl);
else onFail('新发现的 API 也失败: ' + msg);
});
});
return;
}
var url = queue.shift();
postLogin(url, function (ok, msg, hardFail) {
if (ok) { onSuccess(url); return; }
if (hardFail) { onFail(msg); return; }
console.log('[CSDN-DL] 候选失败,尝试下一个:', url, msg);
tryNext();
});
}
tryNext();
}
// 未登录 → 先尝试后台 API 登录,失败再跳登录页(防死循环:60 秒内只试一次)
function ensureLogin() {
if (isLoggedIn()) { removeAccountTip(); return; }
if (!hasAccount()) { showAccountTip(); return; }
var key = 'csdn_dl_last_redirect';
var last = 0;
try { last = parseInt(sessionStorage.getItem(key) || '0', 10); } catch (e) {}
var now = Date.now();
if (now - last < 60000) {
console.log('[CSDN-DL] 未登录,但 60 秒内已尝试过(防循环)');
return;
}
try { sessionStorage.setItem(key, String(now)); } catch (e) {}
tryApiLogin(function (reason) {
console.log('[CSDN-DL] 后台登录不可用(' + reason + '),回退跳登录页');
showTip('后台登录失败,跳转登录页自动登录…');
setTimeout(function () {
location.href = 'https://passport.csdn.net/login?from=' + encodeURIComponent(location.href);
}, 800);
});
}
function initContentPage() {
console.log('[CSDN-DL] 已加载 v' + VERSION);
injectCSS();
installRedirectBlocker();
removeLoginPopups();
enhanceDownloadButtons();
setTimeout(ensureLogin, 1500);
}
/* ============================================================
* 主路由
* ============================================================ */
if (HOST === 'passport.csdn.net') {
runLoginPage();
} else {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initContentPage);
} else {
initContentPage();
}
if ('MutationObserver' in window) {
var observer = new MutationObserver(function (mutations) {
for (var i = 0; i < mutations.length; i++) {
if (mutations[i].addedNodes.length) {
removeLoginPopups();
enhanceDownloadButtons();
break;
}
}
});
function startMainObs() {
if (document.documentElement) {
observer.observe(document.documentElement, { childList: true, subtree: true });
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', startMainObs);
} else {
startMainObs();
}
}
}
})();