feat: PeopleLib 开放文献客户端,集成 Z-Library 与 LibGen 等多源检索
Electron 桌面客户端,聚合多个开放获取文献源的搜索、详情与下载。 新增数据源: - Z-Library:邮箱登录(凭据本地存储),会话失效自动重登 - LibGen:适配新版 libgen.ac 前端(旧版 search.php 镜像已全部下线) - Memory of the World、Sci-Hub、Anna's Archive 基础设施: - mirror.js:镜像故障转移,支持串行优先与并发竞速两种策略, 失效镜像 5 分钟冷却后自动重试,避免站点恢复后被永久跳过 - http.js:统一 15 秒请求超时,防止单个卡死镜像拖垮整次搜索 - settings.js:全局代理配置持久化,经 Electron net.fetch 生效于所有请求 Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
commit
1a1288ce18
@@ -0,0 +1,160 @@
|
||||
// Sci-Hub 数据源:按 DOI 精确获取论文 PDF
|
||||
// 注意:Sci-Hub 各镜像会不定期启用人机验证(ALTCHA),此时无法通过纯 HTTP 抓取,
|
||||
// 模块会明确抛错并提示用户在浏览器中打开。
|
||||
|
||||
const { fetchText, clampPage, decodeEntities } = require('./http');
|
||||
const { tryMirrors } = require('./mirror');
|
||||
|
||||
const MIRRORS = [
|
||||
'https://sci-hub.se',
|
||||
'https://sci-hub.st',
|
||||
'https://sci-hub.ru'
|
||||
];
|
||||
|
||||
const DOI_RE = /^10\.\d{4,9}\/\S+$/;
|
||||
|
||||
function normalizeDoi(input) {
|
||||
let s = String(input || '').trim();
|
||||
s = s.replace(/^doi:\s*/i, '');
|
||||
s = s.replace(/^https?:\/\/(?:dx\.)?doi\.org\//i, '');
|
||||
return s;
|
||||
}
|
||||
|
||||
function absUrl(base, href) {
|
||||
if (!href) return '';
|
||||
if (/^https?:\/\//.test(href)) return href;
|
||||
if (href.startsWith('//')) return 'https:' + href;
|
||||
if (href.startsWith('/')) return base + href;
|
||||
return base + '/' + href;
|
||||
}
|
||||
|
||||
function isChallenge(html) {
|
||||
return /altcha|你是机器人|are you a robot|captcha/i.test(html);
|
||||
}
|
||||
|
||||
function extractTitle(html, doi) {
|
||||
// 优先用引文区块(含完整论文标题)
|
||||
const cite = html.match(/id\s*=\s*["']citation["'][^>]*>([\s\S]{0,600}?)<\/(?:div|i|p)>/i);
|
||||
if (cite) {
|
||||
const t = decodeEntities(cite[1].replace(/<[^>]*>/g, ' ')).replace(/\s+/g, ' ').trim();
|
||||
if (t) return t;
|
||||
}
|
||||
const titleM = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
|
||||
if (titleM) {
|
||||
let t = decodeEntities(titleM[1]).replace(/\s+/g, ' ').trim();
|
||||
t = t.replace(/^Sci-Hub\s*[::]\s*/i, '').replace(/\s*[-–|]\s*Sci-Hub.*$/i, '').trim();
|
||||
if (t) return t;
|
||||
}
|
||||
return doi;
|
||||
}
|
||||
|
||||
function extractPdf(html, base) {
|
||||
const patterns = [
|
||||
/<iframe[^>]+src\s*=\s*["']([^"']+)["']/i,
|
||||
/<embed[^>]+src\s*=\s*["']([^"']+)["']/i,
|
||||
/location\.href\s*=\s*['"]([^'"]+)['"]/i,
|
||||
/<a[^>]+href\s*=\s*["']([^"']*\.pdf[^"']*)["']/i
|
||||
];
|
||||
for (const re of patterns) {
|
||||
const m = html.match(re);
|
||||
if (m && m[1] && /\.pdf|\/downloads?\//i.test(m[1])) {
|
||||
return absUrl(base, m[1].replace(/#.*$/, ''));
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
async function fetchSciHub(base, doi) {
|
||||
const url = `${base}/${doi}`;
|
||||
const html = await fetchText(url);
|
||||
if (isChallenge(html)) {
|
||||
throw new Error(`${base} 启用了人机验证`);
|
||||
}
|
||||
const pdfUrl = extractPdf(html, base);
|
||||
const title = extractTitle(html, doi);
|
||||
const notFound = /article not found|не найдена|抱歉/i.test(html);
|
||||
if (!pdfUrl && notFound) throw new Error('该 DOI 在 Sci-Hub 中不存在');
|
||||
return { pdfUrl, title, url, base };
|
||||
}
|
||||
|
||||
async function resolve(doi) {
|
||||
try {
|
||||
return await tryMirrors('scihub', MIRRORS, (m) => fetchSciHub(m, doi));
|
||||
} catch (e) {
|
||||
if (/人机验证/.test(e.message)) {
|
||||
throw new Error('Sci-Hub 当前要求人机验证,请在浏览器中打开该 DOI 页面');
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
id: 'scihub',
|
||||
name: 'Sci-Hub(按 DOI)',
|
||||
supportsSearch: true,
|
||||
|
||||
async list() {
|
||||
return { items: [], maxPage: 1, page: 1 };
|
||||
},
|
||||
|
||||
async search(keyword, page) {
|
||||
page = clampPage(page);
|
||||
const doi = normalizeDoi(keyword);
|
||||
if (!doi) return { items: [], maxPage: 1, page: 1 };
|
||||
if (!DOI_RE.test(doi)) {
|
||||
throw new Error('Sci-Hub 仅支持 DOI 查询,例如 10.1038/nature12373');
|
||||
}
|
||||
const r = await resolve(doi);
|
||||
return {
|
||||
items: [{
|
||||
postId: doi,
|
||||
title: r.title,
|
||||
cover: '',
|
||||
date: '',
|
||||
url: r.url,
|
||||
subtitle: doi
|
||||
}],
|
||||
maxPage: 1,
|
||||
page: 1
|
||||
};
|
||||
},
|
||||
|
||||
async detail(postId) {
|
||||
const doi = normalizeDoi(postId);
|
||||
const r = await resolve(doi);
|
||||
return {
|
||||
postId: doi,
|
||||
title: r.title,
|
||||
cover: '',
|
||||
authors: [],
|
||||
date: '',
|
||||
tags: [`DOI:${doi}`],
|
||||
brief: '',
|
||||
url: r.url,
|
||||
links: [
|
||||
{ name: 'Sci-Hub 页', url: r.url },
|
||||
{ name: 'DOI 原文', url: `https://doi.org/${doi}` }
|
||||
]
|
||||
};
|
||||
},
|
||||
|
||||
async download(postId) {
|
||||
const doi = normalizeDoi(postId);
|
||||
const r = await resolve(doi);
|
||||
const files = [];
|
||||
if (r.pdfUrl) {
|
||||
files.push({
|
||||
name: `${doi.replace(/[\\/:*?"<>|]/g, '_')}.pdf`,
|
||||
link: r.pdfUrl,
|
||||
format: 'PDF'
|
||||
});
|
||||
}
|
||||
return {
|
||||
files,
|
||||
links: [
|
||||
{ name: 'Sci-Hub 页', url: r.url },
|
||||
{ name: 'DOI 原文', url: `https://doi.org/${doi}` }
|
||||
]
|
||||
};
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user