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:
lofyer
2026-07-25 14:51:10 +08:00
co-authored by factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
commit 1a1288ce18
33 changed files with 4640 additions and 0 deletions
+160
View File
@@ -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}` }
]
};
}
};