diff --git a/package-lock.json b/package-lock.json index 58f093a..ce34126 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "peoplelib", - "version": "2.1.5", + "version": "2.1.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "peoplelib", - "version": "2.1.5", + "version": "2.1.6", "license": "MIT", "dependencies": { "foliate-js": "1.0.1", diff --git a/package.json b/package.json index f311d91..57a7550 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "peoplelib", - "version": "2.1.5", + "version": "2.1.6", "description": "多源开放文献、电子书与本地书库客户端", "main": "main.js", "author": "peoplelib", diff --git a/src/_test/sources.test.js b/src/_test/sources.test.js index 747dd50..ad6f3bf 100644 --- a/src/_test/sources.test.js +++ b/src/_test/sources.test.js @@ -624,6 +624,53 @@ test('zlib: 移除当前会话的非内置镜像会清除旧会话', () => { } }); +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"}。 // 若按 HTTP 状态码短路,真实原因会被吞掉,自动重登也不会触发。 test('zlib: 4xx+JSON 的会话过期能被识别并自动重新登录', async () => { diff --git a/src/sources/zlib.js b/src/sources/zlib.js index dfad890..5e601d9 100644 --- a/src/sources/zlib.js +++ b/src/sources/zlib.js @@ -215,12 +215,17 @@ async function callOn(mirror, path, params, session, method) { } async function attempt(session, path, params, method) { + const custom = auth.getCustomMirrors(); const mirrors = getMirrors(); - const ordered = session.mirror + // 用户显式配置镜像后必须严格优先于旧会话镜像,否则界面写着“自定义优先”, + // 实际却仍先等待上次成功但现在已失效的默认域名。 + const ordered = custom.length + ? mirrors + : session.mirror ? [session.mirror, ...mirrors.filter((m) => m !== session.mirror)] : mirrors; - let lastErr; + const errors = []; for (const m of ordered) { try { const j = await callOn(m, path, params, session, method); @@ -228,10 +233,18 @@ async function attempt(session, path, params, method) { return { ok: true, data: j }; } catch (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') {