Compare commits
2
Commits
7f0eea7464
...
26c7aaff66
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26c7aaff66 | ||
|
|
488de94289 |
@@ -556,6 +556,8 @@ sources.getSource('zlib').setLoginTransport(browserZlibLogin);
|
|||||||
ipcMain.handle('zlib:hasCreds', () => wrap(() => zlibAuth.hasCreds()));
|
ipcMain.handle('zlib:hasCreds', () => wrap(() => zlibAuth.hasCreds()));
|
||||||
ipcMain.handle('zlib:login', (_e, email, password) => wrap(() => sources.getSource('zlib').login(email, password)));
|
ipcMain.handle('zlib:login', (_e, email, password) => wrap(() => sources.getSource('zlib').login(email, password)));
|
||||||
ipcMain.handle('zlib:logout', () => wrap(() => sources.getSource('zlib').logout()));
|
ipcMain.handle('zlib:logout', () => wrap(() => sources.getSource('zlib').logout()));
|
||||||
|
ipcMain.handle('zlib:getMirrors', () => wrap(() => sources.getSource('zlib').getMirrorConfig()));
|
||||||
|
ipcMain.handle('zlib:setMirrors', (_e, list) => wrap(() => sources.getSource('zlib').setMirrors(list)));
|
||||||
|
|
||||||
// Semantic Scholar API Key
|
// Semantic Scholar API Key
|
||||||
ipcMain.handle('semanticScholar:keyStatus', () => wrap(() => semanticKey.status()));
|
ipcMain.handle('semanticScholar:keyStatus', () => wrap(() => semanticKey.status()));
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "peoplelib",
|
"name": "peoplelib",
|
||||||
"version": "2.1.4",
|
"version": "2.1.6",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "peoplelib",
|
"name": "peoplelib",
|
||||||
"version": "2.1.4",
|
"version": "2.1.6",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"foliate-js": "1.0.1",
|
"foliate-js": "1.0.1",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "peoplelib",
|
"name": "peoplelib",
|
||||||
"version": "2.1.4",
|
"version": "2.1.6",
|
||||||
"description": "多源开放文献、电子书与本地书库客户端",
|
"description": "多源开放文献、电子书与本地书库客户端",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"author": "peoplelib",
|
"author": "peoplelib",
|
||||||
|
|||||||
+3
-1
@@ -129,7 +129,9 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
zlib: {
|
zlib: {
|
||||||
hasCreds: () => ipcRenderer.invoke('zlib:hasCreds'),
|
hasCreds: () => ipcRenderer.invoke('zlib:hasCreds'),
|
||||||
login: (email, password) => ipcRenderer.invoke('zlib:login', email, password),
|
login: (email, password) => ipcRenderer.invoke('zlib:login', email, password),
|
||||||
logout: () => ipcRenderer.invoke('zlib:logout')
|
logout: () => ipcRenderer.invoke('zlib:logout'),
|
||||||
|
getMirrors: () => ipcRenderer.invoke('zlib:getMirrors'),
|
||||||
|
setMirrors: (list) => ipcRenderer.invoke('zlib:setMirrors', list)
|
||||||
},
|
},
|
||||||
semanticScholar: {
|
semanticScholar: {
|
||||||
keyStatus: () => ipcRenderer.invoke('semanticScholar:keyStatus'),
|
keyStatus: () => ipcRenderer.invoke('semanticScholar:keyStatus'),
|
||||||
|
|||||||
@@ -135,6 +135,36 @@ test('customMirrors 往返不丢失', () => {
|
|||||||
assert.deepStrictEqual(auth.read().customMirrors, ['https://m1', 'https://m2']);
|
assert.deepStrictEqual(auth.read().customMirrors, ['https://m1', 'https://m2']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('setCustomMirrors 不重写密文,凭据与会话都保留', () => {
|
||||||
|
const d = tmp();
|
||||||
|
const auth = freshAuth();
|
||||||
|
auth.init(d, fakeStorage());
|
||||||
|
auth.write({ email: 'a@b.c', password: 'Keep', userId: '1', userKey: 'k', mirror: 'https://m1' });
|
||||||
|
|
||||||
|
auth.setCustomMirrors(['https://m1', 'https://m2']);
|
||||||
|
assert.deepStrictEqual(auth.getCustomMirrors(), ['https://m1', 'https://m2']);
|
||||||
|
assert.strictEqual(auth.read().password, 'Keep', '改镜像不该动凭据');
|
||||||
|
assert.deepStrictEqual(auth.getSession(), { userId: '1', userKey: 'k', mirror: 'https://m1' });
|
||||||
|
|
||||||
|
auth.setCustomMirrors([]);
|
||||||
|
assert.deepStrictEqual(auth.getCustomMirrors(), []);
|
||||||
|
assert.strictEqual(auth.hasCreds(), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('退出登录清掉凭据但保留自定义镜像', () => {
|
||||||
|
const d = tmp();
|
||||||
|
const auth = freshAuth();
|
||||||
|
auth.init(d, fakeStorage());
|
||||||
|
auth.write({ email: 'a@b.c', password: 'p', userId: '1', userKey: 'k' });
|
||||||
|
auth.setCustomMirrors(['https://mine.example']);
|
||||||
|
|
||||||
|
auth.clear();
|
||||||
|
assert.strictEqual(auth.hasCreds(), false, '凭据未清除');
|
||||||
|
assert.strictEqual(auth.getSession(), null, '会话未清除');
|
||||||
|
// 默认域名可能已全部失效,退出登录顺带清掉镜像会让用户无从登录
|
||||||
|
assert.deepStrictEqual(auth.getCustomMirrors(), ['https://mine.example']);
|
||||||
|
});
|
||||||
|
|
||||||
test('损坏的密文不影响会话字段读取', () => {
|
test('损坏的密文不影响会话字段读取', () => {
|
||||||
const d = tmp();
|
const d = tmp();
|
||||||
const auth = freshAuth();
|
const auth = freshAuth();
|
||||||
|
|||||||
@@ -95,6 +95,29 @@ app.whenReady().then(async () => {
|
|||||||
proxyRoute);
|
proxyRoute);
|
||||||
await win.webContents.executeJavaScript(`window.api.proxy.set('')`);
|
await win.webContents.executeJavaScript(`window.api.proxy.set('')`);
|
||||||
|
|
||||||
|
await win.webContents.executeJavaScript(`document.getElementById('zlibMirrorBtn').click()`);
|
||||||
|
await waitUntil(() => win.webContents.executeJavaScript(
|
||||||
|
`!!document.getElementById('zlibMirrorList')`
|
||||||
|
));
|
||||||
|
await win.webContents.executeJavaScript(`(() => {
|
||||||
|
document.getElementById('zlibMirrorList').value =
|
||||||
|
'https://mirror.example/path\\nhttps://second.example/';
|
||||||
|
document.getElementById('modalOk').click();
|
||||||
|
})()`);
|
||||||
|
const mirrorConfig = await waitUntil(() => win.webContents.executeJavaScript(`(async () => {
|
||||||
|
const result = await window.api.zlib.getMirrors();
|
||||||
|
const status = document.getElementById('zlibMirrorStatus').textContent;
|
||||||
|
return result.ok && result.data.custom.length === 2 && /已自定义 2 个镜像/.test(status)
|
||||||
|
? { custom: result.data.custom, status }
|
||||||
|
: null;
|
||||||
|
})()`));
|
||||||
|
check('Z-Library 账户设置可保存并优先使用自定义镜像',
|
||||||
|
JSON.stringify(mirrorConfig.custom) === JSON.stringify([
|
||||||
|
'https://mirror.example',
|
||||||
|
'https://second.example'
|
||||||
|
]),
|
||||||
|
JSON.stringify(mirrorConfig));
|
||||||
|
|
||||||
const sourceRows = await waitUntil(() => win.webContents.executeJavaScript(`(() => {
|
const sourceRows = await waitUntil(() => win.webContents.executeJavaScript(`(() => {
|
||||||
const rows = Array.from(document.querySelectorAll('#sourceList input[data-id]')).map((input) => ({
|
const rows = Array.from(document.querySelectorAll('#sourceList input[data-id]')).map((input) => ({
|
||||||
id: input.dataset.id,
|
id: input.dataset.id,
|
||||||
|
|||||||
@@ -560,6 +560,117 @@ test('zlib: 完全无效的 id 仍然拒绝', async () => {
|
|||||||
await assert.rejects(zlib.detail('not-an-id'), /无效的 Z-Library ID/);
|
await assert.rejects(zlib.detail('not-an-id'), /无效的 Z-Library ID/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('zlib: 自定义镜像规范化、去重并优先于内置列表', () => {
|
||||||
|
const zlib = h.freshRequire('sources/zlib.js');
|
||||||
|
const auth = require('../sources/zlib-auth');
|
||||||
|
const orig = {
|
||||||
|
getCustomMirrors: auth.getCustomMirrors,
|
||||||
|
setCustomMirrors: auth.setCustomMirrors,
|
||||||
|
getSession: auth.getSession,
|
||||||
|
clearSession: auth.clearSession
|
||||||
|
};
|
||||||
|
let custom = [];
|
||||||
|
auth.getCustomMirrors = () => custom.slice();
|
||||||
|
auth.setCustomMirrors = (list) => { custom = list.slice(); };
|
||||||
|
auth.getSession = () => null;
|
||||||
|
auth.clearSession = () => {};
|
||||||
|
try {
|
||||||
|
const result = zlib.setMirrors([
|
||||||
|
'https://mirror.example/path?ignored=1',
|
||||||
|
'https://mirror.example/',
|
||||||
|
'https://second.example'
|
||||||
|
]);
|
||||||
|
assert.deepStrictEqual(result.custom, [
|
||||||
|
'https://mirror.example',
|
||||||
|
'https://second.example'
|
||||||
|
]);
|
||||||
|
assert.deepStrictEqual(zlib.getMirrorConfig().custom, result.custom);
|
||||||
|
assert.ok(result.defaults.length >= 1, '内置镜像列表丢失');
|
||||||
|
} finally {
|
||||||
|
Object.assign(auth, orig);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('zlib: 自定义镜像拒绝非 HTTPS、凭据与超量输入', () => {
|
||||||
|
const zlib = h.freshRequire('sources/zlib.js');
|
||||||
|
assert.throws(() => zlib.setMirrors(['http://mirror.example']), /必须使用 HTTPS/);
|
||||||
|
assert.throws(() => zlib.setMirrors(['https://user:pass@mirror.example']), /不能包含账号信息/);
|
||||||
|
assert.throws(
|
||||||
|
() => zlib.setMirrors(Array.from({ length: 21 }, (_, i) => `https://m${i}.example`)),
|
||||||
|
/最多只能保存 20 个镜像/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('zlib: 移除当前会话的非内置镜像会清除旧会话', () => {
|
||||||
|
const zlib = h.freshRequire('sources/zlib.js');
|
||||||
|
const auth = require('../sources/zlib-auth');
|
||||||
|
const orig = {
|
||||||
|
getCustomMirrors: auth.getCustomMirrors,
|
||||||
|
setCustomMirrors: auth.setCustomMirrors,
|
||||||
|
getSession: auth.getSession,
|
||||||
|
clearSession: auth.clearSession
|
||||||
|
};
|
||||||
|
let custom = ['https://old.example'];
|
||||||
|
let cleared = 0;
|
||||||
|
auth.getCustomMirrors = () => custom.slice();
|
||||||
|
auth.setCustomMirrors = (list) => { custom = list.slice(); };
|
||||||
|
auth.getSession = () => ({ userId: '1', userKey: 'k', mirror: 'https://old.example' });
|
||||||
|
auth.clearSession = () => { cleared += 1; };
|
||||||
|
try {
|
||||||
|
zlib.setMirrors(['https://new.example']);
|
||||||
|
assert.strictEqual(cleared, 1, '被移除镜像上的会话仍被保留');
|
||||||
|
} finally {
|
||||||
|
Object.assign(auth, orig);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('zlib: 自定义镜像优先于旧会话记录的内置镜像', async () => {
|
||||||
|
const zlib = h.freshRequire('sources/zlib.js');
|
||||||
|
const auth = require('../sources/zlib-auth');
|
||||||
|
const orig = {
|
||||||
|
getCustomMirrors: auth.getCustomMirrors,
|
||||||
|
getSession: auth.getSession,
|
||||||
|
setSession: auth.setSession
|
||||||
|
};
|
||||||
|
auth.getCustomMirrors = () => ['https://custom.example'];
|
||||||
|
auth.getSession = () => ({ userId: '1', userKey: 'k', mirror: 'https://1lib.sk' });
|
||||||
|
auth.setSession = () => {};
|
||||||
|
try {
|
||||||
|
h.resetCalls();
|
||||||
|
h.setHandler((url) => {
|
||||||
|
assert.ok(url.startsWith('https://custom.example/'), `先请求了旧会话镜像:${url}`);
|
||||||
|
return h.makeResponse({ body: { success: 1, books: [] } });
|
||||||
|
});
|
||||||
|
await zlib.list(1);
|
||||||
|
assert.strictEqual(h.getCalls().length, 1, '自定义镜像成功后仍请求了内置镜像');
|
||||||
|
} finally {
|
||||||
|
Object.assign(auth, orig);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('zlib: 全部失败时保留自定义镜像的首要错误', async () => {
|
||||||
|
const zlib = h.freshRequire('sources/zlib.js');
|
||||||
|
const auth = require('../sources/zlib-auth');
|
||||||
|
const orig = {
|
||||||
|
getCustomMirrors: auth.getCustomMirrors,
|
||||||
|
getSession: auth.getSession
|
||||||
|
};
|
||||||
|
auth.getCustomMirrors = () => ['https://custom.example'];
|
||||||
|
auth.getSession = () => ({ userId: '1', userKey: 'k', mirror: 'https://1lib.sk' });
|
||||||
|
try {
|
||||||
|
h.setHandler((url) => {
|
||||||
|
if (url.startsWith('https://custom.example/')) throw new Error('请求超时,站点无响应');
|
||||||
|
throw new Error('该镜像返回了非预期内容');
|
||||||
|
});
|
||||||
|
await assert.rejects(
|
||||||
|
zlib.list(1),
|
||||||
|
/自定义镜像失败:请求超时,站点无响应;其余 \d+ 个镜像也未成功/
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
Object.assign(auth, orig);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 实测:会话过期时 /file 返回 400 + {"success":0,"error":"Please login"}。
|
// 实测:会话过期时 /file 返回 400 + {"success":0,"error":"Please login"}。
|
||||||
// 若按 HTTP 状态码短路,真实原因会被吞掉,自动重登也不会触发。
|
// 若按 HTTP 状态码短路,真实原因会被吞掉,自动重登也不会触发。
|
||||||
test('zlib: 4xx+JSON 的会话过期能被识别并自动重新登录', async () => {
|
test('zlib: 4xx+JSON 的会话过期能被识别并自动重新登录', async () => {
|
||||||
|
|||||||
@@ -240,6 +240,28 @@ test('Z-Library 进入详情不消耗下载额度,点击下载后才解析并
|
|||||||
assert.match(onDemand, /每日免费下载额度有限,仅在点击后获取下载地址/);
|
assert.match(onDemand, /每日免费下载额度有限,仅在点击后获取下载地址/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Z-Library 账户设置提供可编辑的镜像站点列表', () => {
|
||||||
|
const html = fs.readFileSync(path.join(__dirname, '..', 'ui', 'index.html'), 'utf8');
|
||||||
|
const app = fs.readFileSync(path.join(__dirname, '..', 'ui', 'app.js'), 'utf8');
|
||||||
|
const preload = fs.readFileSync(path.join(__dirname, '..', '..', 'preload.js'), 'utf8');
|
||||||
|
const main = fs.readFileSync(path.join(__dirname, '..', '..', 'main.js'), 'utf8');
|
||||||
|
const css = fs.readFileSync(path.join(__dirname, '..', 'ui', 'style.css'), 'utf8');
|
||||||
|
|
||||||
|
const accountAt = html.indexOf('Z-Library 账号');
|
||||||
|
const mirrorsAt = html.indexOf('id="zlibMirrorBtn"');
|
||||||
|
const nextGroupAt = html.indexOf('<div class="settings-group">', accountAt + 30);
|
||||||
|
assert.ok(accountAt >= 0 && mirrorsAt > accountAt && mirrorsAt < nextGroupAt,
|
||||||
|
'镜像设置没有放在 Z-Library 账户设置组内');
|
||||||
|
assert.match(app, /window\.api\.zlib\.getMirrors\(\)/);
|
||||||
|
assert.match(app, /window\.api\.zlib\.setMirrors\(lines\)/);
|
||||||
|
assert.match(app, /一行一个地址,必须是 HTTPS/);
|
||||||
|
assert.match(preload, /getMirrors:\s*\(\)\s*=>\s*ipcRenderer\.invoke\('zlib:getMirrors'\)/);
|
||||||
|
assert.match(preload, /setMirrors:\s*\(list\)\s*=>\s*ipcRenderer\.invoke\('zlib:setMirrors', list\)/);
|
||||||
|
assert.match(main, /ipcMain\.handle\('zlib:getMirrors',[^\n]*wrap\(/);
|
||||||
|
assert.match(main, /ipcMain\.handle\('zlib:setMirrors',[^\n]*wrap\(/);
|
||||||
|
assert.match(css, /textarea\.modal-input\s*\{[^}]*height:\s*auto/);
|
||||||
|
});
|
||||||
|
|
||||||
test('漫画源按分类显示,MangaDex 可筛选中文章节并设置图片画质', () => {
|
test('漫画源按分类显示,MangaDex 可筛选中文章节并设置图片画质', () => {
|
||||||
const html = fs.readFileSync(path.join(__dirname, '..', 'ui', 'index.html'), 'utf8');
|
const html = fs.readFileSync(path.join(__dirname, '..', 'ui', 'index.html'), 'utf8');
|
||||||
const browse = fs.readFileSync(path.join(__dirname, '..', 'ui', 'views', 'browse.js'), 'utf8');
|
const browse = fs.readFileSync(path.join(__dirname, '..', 'ui', 'views', 'browse.js'), 'utf8');
|
||||||
|
|||||||
@@ -123,11 +123,32 @@ function write(creds) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 清除全部(含凭据)——用于"退出登录"
|
// 清除全部(含凭据)——用于"退出登录"
|
||||||
|
// 自定义镜像是站点配置不是凭据,退出登录后要保留,否则用户每次重新登录
|
||||||
|
// 都得重新填一遍地址,而默认域名可能全都已经失效。
|
||||||
function clear() {
|
function clear() {
|
||||||
sessionCreds = null;
|
sessionCreds = null;
|
||||||
|
const mirrors = getCustomMirrors();
|
||||||
for (const fp of [getFilePath(), getCredPath(), `${getFilePath()}.tmp`, `${getCredPath()}.tmp`]) {
|
for (const fp of [getFilePath(), getCredPath(), `${getFilePath()}.tmp`, `${getCredPath()}.tmp`]) {
|
||||||
try { fs.unlinkSync(fp); } catch (e) { /* ignore */ }
|
try { fs.unlinkSync(fp); } catch (e) { /* ignore */ }
|
||||||
}
|
}
|
||||||
|
if (mirrors.length) {
|
||||||
|
try { atomicWrite(getFilePath(), JSON.stringify({ customMirrors: mirrors }, null, 2)); }
|
||||||
|
catch (e) { /* 保留失败不影响退出登录 */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCustomMirrors() {
|
||||||
|
const meta = readMeta();
|
||||||
|
return (meta && Array.isArray(meta.customMirrors)) ? meta.customMirrors.slice() : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只动镜像字段:与 setSession 同理,不重写密文,避免一次读取失败清空凭据
|
||||||
|
function setCustomMirrors(list) {
|
||||||
|
const meta = readMeta() || {};
|
||||||
|
const next = { ...meta };
|
||||||
|
if (Array.isArray(list) && list.length) next.customMirrors = list.slice();
|
||||||
|
else delete next.customMirrors;
|
||||||
|
atomicWrite(getFilePath(), JSON.stringify(next, null, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 只清除会话令牌,保留邮箱密码以便自动重新登录
|
// 只清除会话令牌,保留邮箱密码以便自动重新登录
|
||||||
@@ -158,4 +179,7 @@ function setSession(userId, userKey, mirror) {
|
|||||||
atomicWrite(getFilePath(), JSON.stringify(next, null, 2));
|
atomicWrite(getFilePath(), JSON.stringify(next, null, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { init, read, write, clear, clearSession, hasCreds, getSession, setSession };
|
module.exports = {
|
||||||
|
init, read, write, clear, clearSession, hasCreds, getSession, setSession,
|
||||||
|
getCustomMirrors, setCustomMirrors
|
||||||
|
};
|
||||||
|
|||||||
+66
-6
@@ -43,13 +43,40 @@ function requestHeaders(mirror, includeForm = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getMirrors() {
|
function getMirrors() {
|
||||||
const custom = (auth.read() || {}).customMirrors;
|
const custom = auth.getCustomMirrors();
|
||||||
if (Array.isArray(custom) && custom.length) {
|
if (custom.length) {
|
||||||
return custom.concat(DEFAULT_MIRRORS.filter((m) => !custom.includes(m)));
|
return custom.concat(DEFAULT_MIRRORS.filter((m) => !custom.includes(m)));
|
||||||
}
|
}
|
||||||
return DEFAULT_MIRRORS.slice();
|
return DEFAULT_MIRRORS.slice();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MAX_CUSTOM_MIRRORS = 20;
|
||||||
|
|
||||||
|
// 镜像地址来自用户输入,会被直接拼进请求 URL,因此必须按 origin 归一:
|
||||||
|
// 带路径、查询串或凭据的地址会让后续 apiUrl() 拼出错误甚至泄露凭据的 URL。
|
||||||
|
function normalizeMirror(value) {
|
||||||
|
const raw = String(value || '').trim();
|
||||||
|
if (!raw) return '';
|
||||||
|
let url;
|
||||||
|
try { url = new URL(raw); } catch (e) { throw new Error(`镜像地址无效:${raw}`); }
|
||||||
|
if (url.protocol !== 'https:') throw new Error(`镜像必须使用 HTTPS:${raw}`);
|
||||||
|
if (url.username || url.password) throw new Error(`镜像地址不能包含账号信息:${raw}`);
|
||||||
|
return url.origin;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeMirrors(list) {
|
||||||
|
if (!Array.isArray(list)) throw new Error('镜像列表格式无效');
|
||||||
|
if (list.length > MAX_CUSTOM_MIRRORS) {
|
||||||
|
throw new Error(`最多只能保存 ${MAX_CUSTOM_MIRRORS} 个镜像`);
|
||||||
|
}
|
||||||
|
const out = [];
|
||||||
|
for (const item of list) {
|
||||||
|
const origin = normalizeMirror(item);
|
||||||
|
if (origin && !out.includes(origin)) out.push(origin);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
function apiUrl(base, path, params = {}) {
|
function apiUrl(base, path, params = {}) {
|
||||||
const u = new URL(base + path);
|
const u = new URL(base + path);
|
||||||
for (const [k, v] of Object.entries(params)) {
|
for (const [k, v] of Object.entries(params)) {
|
||||||
@@ -188,12 +215,17 @@ async function callOn(mirror, path, params, session, method) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function attempt(session, path, params, method) {
|
async function attempt(session, path, params, method) {
|
||||||
|
const custom = auth.getCustomMirrors();
|
||||||
const mirrors = getMirrors();
|
const mirrors = getMirrors();
|
||||||
const ordered = session.mirror
|
// 用户显式配置镜像后必须严格优先于旧会话镜像,否则界面写着“自定义优先”,
|
||||||
|
// 实际却仍先等待上次成功但现在已失效的默认域名。
|
||||||
|
const ordered = custom.length
|
||||||
|
? mirrors
|
||||||
|
: session.mirror
|
||||||
? [session.mirror, ...mirrors.filter((m) => m !== session.mirror)]
|
? [session.mirror, ...mirrors.filter((m) => m !== session.mirror)]
|
||||||
: mirrors;
|
: mirrors;
|
||||||
|
|
||||||
let lastErr;
|
const errors = [];
|
||||||
for (const m of ordered) {
|
for (const m of ordered) {
|
||||||
try {
|
try {
|
||||||
const j = await callOn(m, path, params, session, method);
|
const j = await callOn(m, path, params, session, method);
|
||||||
@@ -201,10 +233,18 @@ async function attempt(session, path, params, method) {
|
|||||||
return { ok: true, data: j };
|
return { ok: true, data: j };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.code === 'AUTH_STALE') return { ok: false, stale: true, error: e };
|
if (e.code === 'AUTH_STALE') return { ok: false, stale: true, error: e };
|
||||||
lastErr = e;
|
errors.push(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { ok: false, stale: false, error: lastErr };
|
if (errors.length <= 1) {
|
||||||
|
return { ok: false, stale: false, error: errors[0] };
|
||||||
|
}
|
||||||
|
const first = errors[0];
|
||||||
|
const label = custom.length ? '自定义镜像失败' : '首选镜像失败';
|
||||||
|
const summary = new Error(
|
||||||
|
`${label}:${(first && first.message) || String(first)};其余 ${errors.length - 1} 个镜像也未成功`
|
||||||
|
);
|
||||||
|
return { ok: false, stale: false, error: summary };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function apiCall(path, params = {}, method = 'GET') {
|
async function apiCall(path, params = {}, method = 'GET') {
|
||||||
@@ -379,5 +419,25 @@ module.exports = {
|
|||||||
return auth.hasCreds();
|
return auth.hasCreds();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 默认域名变动频繁且会整批失效,用户需要能自己补上可用地址而不必等新版本
|
||||||
|
getMirrorConfig() {
|
||||||
|
return { custom: auth.getCustomMirrors(), defaults: DEFAULT_MIRRORS.slice() };
|
||||||
|
},
|
||||||
|
|
||||||
|
setMirrors(list) {
|
||||||
|
const next = normalizeMirrors(list);
|
||||||
|
const previous = auth.getCustomMirrors();
|
||||||
|
auth.setCustomMirrors(next);
|
||||||
|
// 换镜像后旧会话绑定的域名可能已不在列表里,留着会一直优先命中失效地址
|
||||||
|
const session = auth.getSession();
|
||||||
|
if (session && session.mirror && !getMirrors().includes(session.mirror)) {
|
||||||
|
auth.clearSession();
|
||||||
|
}
|
||||||
|
for (const mirror of previous) {
|
||||||
|
if (!next.includes(mirror)) clearCookies(mirror);
|
||||||
|
}
|
||||||
|
return { custom: next, defaults: DEFAULT_MIRRORS.slice() };
|
||||||
|
},
|
||||||
|
|
||||||
setLoginTransport
|
setLoginTransport
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -124,6 +124,38 @@ $('zlibLogoutBtn').onclick = async () => {
|
|||||||
|
|
||||||
refreshZlibStatus();
|
refreshZlibStatus();
|
||||||
|
|
||||||
|
async function refreshZlibMirrors() {
|
||||||
|
const r = await window.api.zlib.getMirrors();
|
||||||
|
const custom = (r.ok && r.data && r.data.custom) || [];
|
||||||
|
$('zlibMirrorStatus').textContent = custom.length
|
||||||
|
? `已自定义 ${custom.length} 个镜像,优先于内置列表`
|
||||||
|
: '使用内置镜像列表';
|
||||||
|
}
|
||||||
|
|
||||||
|
$('zlibMirrorBtn').onclick = async () => {
|
||||||
|
const current = await window.api.zlib.getMirrors();
|
||||||
|
if (!current.ok) return;
|
||||||
|
const custom = (current.data && current.data.custom) || [];
|
||||||
|
const defaults = (current.data && current.data.defaults) || [];
|
||||||
|
const r = await openModal('Z-Library 镜像站点', `
|
||||||
|
<p style="margin-bottom:8px;">一行一个地址,必须是 HTTPS。自定义镜像会排在内置列表前面优先尝试;留空则只用内置列表。</p>
|
||||||
|
<div style="display:flex;flex-direction:column;gap:8px;">
|
||||||
|
<textarea id="zlibMirrorList" class="modal-input" rows="6" spellcheck="false"
|
||||||
|
placeholder="https://example.org">${escapeHtml(custom.join('\n'))}</textarea>
|
||||||
|
<div class="settings-item-desc">内置镜像:${escapeHtml(defaults.join('、'))}</div>
|
||||||
|
<div id="zlibMirrorErr" class="note-form-error"></div>
|
||||||
|
</div>
|
||||||
|
`, async () => {
|
||||||
|
const lines = $('zlibMirrorList').value.split('\n').map((s) => s.trim()).filter(Boolean);
|
||||||
|
const res = await window.api.zlib.setMirrors(lines);
|
||||||
|
if (!res.ok) { $('zlibMirrorErr').textContent = res.error || '保存失败'; return false; }
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
if (r) { refreshZlibMirrors(); refreshZlibStatus(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
refreshZlibMirrors();
|
||||||
|
|
||||||
async function refreshLibraryDir() {
|
async function refreshLibraryDir() {
|
||||||
const r = await window.api.library.getDir();
|
const r = await window.api.library.getDir();
|
||||||
if (r.ok && r.data) $('libDirPath').textContent = r.data.dir + (r.data.isDefault ? '(默认)' : '');
|
if (r.ok && r.data) $('libDirPath').textContent = r.data.dir + (r.data.isDefault ? '(默认)' : '');
|
||||||
|
|||||||
@@ -331,6 +331,13 @@
|
|||||||
<button id="zlibLoginBtn" class="tb-btn">登录</button>
|
<button id="zlibLoginBtn" class="tb-btn">登录</button>
|
||||||
<button id="zlibLogoutBtn" class="tb-btn ghost hidden">退出</button>
|
<button id="zlibLogoutBtn" class="tb-btn ghost hidden">退出</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="settings-item">
|
||||||
|
<div class="settings-item-info">
|
||||||
|
<div class="settings-item-label">镜像站点</div>
|
||||||
|
<div class="settings-item-desc" id="zlibMirrorStatus">使用内置镜像列表</div>
|
||||||
|
</div>
|
||||||
|
<button id="zlibMirrorBtn" class="tb-btn">设置</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
<div class="settings-item">
|
<div class="settings-item">
|
||||||
|
|||||||
@@ -1541,6 +1541,11 @@ body {
|
|||||||
}
|
}
|
||||||
.modal-body input[type="text"]:focus,
|
.modal-body input[type="text"]:focus,
|
||||||
.modal-input:focus { border-color: var(--accent); }
|
.modal-input:focus { border-color: var(--accent); }
|
||||||
|
/* .modal-input 写死了单行高度,多行输入必须覆盖,否则 textarea 被压成一行 */
|
||||||
|
textarea.modal-input {
|
||||||
|
height: auto; min-height: 92px; padding: 8px 12px;
|
||||||
|
line-height: 1.6; resize: vertical; font-family: inherit;
|
||||||
|
}
|
||||||
.modal-actions { display: flex; justify-content: flex-end; gap: 10px; }
|
.modal-actions { display: flex; justify-content: flex-end; gap: 10px; }
|
||||||
.modal-actions > .tb-btn { width: 72px; height: 32px; padding: 0; }
|
.modal-actions > .tb-btn { width: 72px; height: 32px; padding: 0; }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user