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
+111
View File
@@ -0,0 +1,111 @@
// 临时测试脚本:完全模拟 main.js 的初始化流程,跑全部数据源搜索
const { app, session } = require('electron');
const path = require('path');
app.setName('PeopleLib');
app.commandLine.appendSwitch('ignore-certificate-errors');
const userDataDir = path.join(app.getPath('appData'), 'PeopleLib');
app.setPath('userData', userDataDir);
const sources = require('./src/sources');
const zlibAuth = require('./src/sources/zlib-auth');
const settings = require('./src/settings');
const { setProxy, getProxy } = require('./src/sources/http');
zlibAuth.init(userDataDir);
settings.init(userDataDir);
// 允许通过命令行覆盖代理:electron test-search.js --proxy http://localhost:7897
const argv = process.argv.slice(2);
const pIdx = argv.indexOf('--proxy');
const proxyOverride = pIdx >= 0 ? argv[pIdx + 1] : null;
setProxy(proxyOverride !== null ? proxyOverride : settings.get('proxy', ''));
function mask(s) {
s = String(s || '');
return s.length > 8 ? s.slice(0, 4) + '***' + s.slice(-2) : '***';
}
const CASES = [
{ id: 'motw', kw: 'marx' },
{ id: 'scihub', kw: '10.1038/nature12373' },
{ id: 'libgen', kw: 'godel escher bach' },
{ id: 'zlib', kw: 'godel' },
{ id: 'gutenberg', kw: 'alice' },
{ id: 'arxiv', kw: 'transformer' }
];
async function runCase(c) {
const label = `[${c.id}]`;
const t0 = Date.now();
try {
const src = sources.getSource(c.id);
const r = await src.search(c.kw, 1);
const n = (r.items || []).length;
console.log(`${label} search("${c.kw}") -> ${n} items, maxPage=${r.maxPage} (${Date.now() - t0}ms)`);
if (!n) {
console.log(`${label} !! EMPTY RESULT`);
return { id: c.id, ok: false, reason: 'empty' };
}
for (const it of r.items.slice(0, 2)) {
console.log(`${label} - ${String(it.title).slice(0, 70)} | ${String(it.subtitle || '').slice(0, 40)}`);
}
// 顺带验证 detail + download 链路
const first = r.items[0];
try {
const d = await src.detail(first.postId);
console.log(`${label} detail OK: ${String(d.title).slice(0, 60)}`);
} catch (e) {
console.log(`${label} detail FAIL: ${e.message}`);
}
try {
const dl = await src.download(first.postId);
const files = (dl.files || []).length;
const links = (dl.links || []).length;
console.log(`${label} download OK: ${files} files, ${links} links`);
if (files) console.log(`${label} file: ${dl.files[0].name} -> ${String(dl.files[0].link).slice(0, 80)}`);
} catch (e) {
console.log(`${label} download FAIL: ${e.message}`);
}
return { id: c.id, ok: true, n };
} catch (e) {
console.log(`${label} SEARCH FAIL (${Date.now() - t0}ms): ${e.message}`);
return { id: c.id, ok: false, reason: e.message };
}
}
app.whenReady().then(async () => {
const p = getProxy();
console.log('=== 环境 ===');
console.log('userData:', userDataDir);
console.log('proxy(app):', p || '(空 -> 使用系统代理)');
if (p) {
await session.defaultSession.setProxy({ proxyRules: p }).catch(() => {});
}
app.on('certificate-error', (event, wc, url, err, cert, cb) => {
event.preventDefault();
cb(true);
});
const s = zlibAuth.getSession();
const creds = zlibAuth.read();
console.log('zlib creds:', creds && creds.email ? creds.email : '(无)');
console.log('zlib session:', s ? `userId=${s.userId} userKey=${mask(s.userKey)} mirror='${s.mirror}'` : '(无)');
console.log('');
const results = [];
for (const c of CASES) {
results.push(await runCase(c));
console.log('');
}
console.log('=== 汇总 ===');
for (const r of results) {
console.log(`${r.ok ? 'PASS' : 'FAIL'} ${r.id}${r.ok ? ` (${r.n})` : ` - ${r.reason}`}`);
}
const after = zlibAuth.getSession();
console.log('');
console.log('zlib session after:', after ? `mirror='${after.mirror}'` : '(无)');
app.exit(0);
});