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
+61
View File
@@ -0,0 +1,61 @@
window.$ = (id) => document.getElementById(id);
window.escapeHtml = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
}[c]));
window.coverStyle = (cover) => {
if (!cover) return '';
const url = /^(https?:|data:)/.test(cover) ? cover : 'file:///' + String(cover).replace(/\\/g, '/');
return `background-image:url('${url.replace(/'/g, "\\'")}')`;
};
window.copyText = async (btn, text) => {
await window.api.copy(text);
const orig = btn.textContent;
btn.textContent = '已复制 ✓';
btn.classList.add('copied');
setTimeout(() => { btn.textContent = orig; btn.classList.remove('copied'); }, 1500);
};
window.formatDate = (ts) => {
if (!ts) return '';
const d = new Date(ts);
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
};
window.getEnabledSources = () => {
let ids = null;
try {
const raw = localStorage.getItem('enabledSources');
if (raw) ids = JSON.parse(raw);
} catch (e) { /* ignore */ }
return ids;
};
window.setEnabledSources = (ids) => {
localStorage.setItem('enabledSources', JSON.stringify(ids));
};
// 通用弹窗: 返回 Promise<{ok, values}|null>
window.openModal = (title, bodyHtml, onOk) => {
const modal = $('modal');
$('modalTitle').textContent = title;
$('modalBody').innerHTML = bodyHtml;
modal.classList.remove('hidden');
return new Promise((resolve) => {
const close = (result) => {
modal.classList.add('hidden');
$('modalOk').onclick = null;
$('modalCancel').onclick = null;
resolve(result);
};
$('modalCancel').onclick = () => close(null);
$('modalOk').onclick = async () => {
const r = onOk ? await onOk() : true;
if (r !== false) close(r);
};
});
};
window.confirmModal = (title, text) => window.openModal(title, `<p>${escapeHtml(text)}</p>`);