@jackwener/opencli 1.5.6 → 1.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +4 -2
- package/README.zh-CN.md +4 -1
- package/SKILL.md +879 -0
- package/dist/browser/cdp.d.ts +1 -0
- package/dist/browser/cdp.js +30 -27
- package/dist/browser/daemon-client.d.ts +7 -1
- package/dist/browser/daemon-client.js +3 -0
- package/dist/browser/dom-helpers.js +1 -0
- package/dist/browser/dom-helpers.test.js +14 -1
- package/dist/browser/mcp.js +18 -13
- package/dist/browser/page.js +22 -2
- package/dist/browser/page.test.d.ts +1 -0
- package/dist/browser/page.test.js +44 -0
- package/dist/browser/stealth.js +198 -0
- package/dist/browser/stealth.test.d.ts +1 -0
- package/dist/browser/stealth.test.js +134 -0
- package/dist/browser.test.js +1 -1
- package/dist/build-manifest.d.ts +1 -0
- package/dist/build-manifest.js +5 -1
- package/dist/build-manifest.test.js +2 -0
- package/dist/cli-manifest.json +544 -137
- package/dist/cli.js +20 -3
- package/dist/clis/antigravity/serve.d.ts +1 -1
- package/dist/clis/antigravity/serve.js +5 -8
- package/dist/clis/bilibili/subtitle.js +4 -0
- package/dist/clis/bilibili/subtitle.test.d.ts +1 -0
- package/dist/clis/bilibili/subtitle.test.js +48 -0
- package/dist/clis/chatwise/ask.js +0 -2
- package/dist/clis/chatwise/export.js +0 -2
- package/dist/clis/chatwise/history.js +0 -2
- package/dist/clis/chatwise/model.js +0 -2
- package/dist/clis/chatwise/new.js +1 -2
- package/dist/clis/chatwise/read.js +0 -2
- package/dist/clis/chatwise/screenshot.js +1 -2
- package/dist/clis/chatwise/send.js +0 -2
- package/dist/clis/chatwise/status.js +1 -2
- package/dist/clis/ctrip/search.d.ts +13 -0
- package/dist/clis/ctrip/search.js +73 -48
- package/dist/clis/ctrip/search.test.d.ts +1 -0
- package/dist/clis/ctrip/search.test.js +64 -0
- package/dist/clis/douyin/_shared/sts2.js +8 -2
- package/dist/clis/douyin/_shared/sts2.test.d.ts +1 -0
- package/dist/clis/douyin/_shared/sts2.test.js +27 -0
- package/dist/clis/douyin/activities.js +4 -2
- package/dist/clis/douyin/activities.test.js +34 -1
- package/dist/clis/douyin/collections.js +1 -1
- package/dist/clis/douyin/collections.test.js +24 -2
- package/dist/clis/douyin/draft.d.ts +8 -11
- package/dist/clis/douyin/draft.js +302 -185
- package/dist/clis/douyin/draft.test.d.ts +1 -1
- package/dist/clis/douyin/draft.test.js +357 -2
- package/dist/clis/douyin/hashtag.js +9 -2
- package/dist/clis/douyin/hashtag.test.js +35 -2
- package/dist/clis/douyin/profile.js +1 -1
- package/dist/clis/douyin/profile.test.js +36 -1
- package/dist/clis/douyin/videos.js +22 -5
- package/dist/clis/douyin/videos.test.js +45 -2
- package/dist/clis/facebook/search.test.d.ts +5 -0
- package/dist/clis/facebook/search.test.js +60 -0
- package/dist/clis/facebook/search.yaml +4 -3
- package/dist/clis/instagram/download.d.ts +16 -0
- package/dist/clis/instagram/download.js +225 -0
- package/dist/clis/instagram/download.test.d.ts +1 -0
- package/dist/clis/instagram/download.test.js +118 -0
- package/dist/clis/notebooklm/bind-current.d.ts +1 -0
- package/dist/clis/notebooklm/bind-current.js +29 -0
- package/dist/clis/notebooklm/bind-current.test.d.ts +1 -0
- package/dist/clis/notebooklm/bind-current.test.js +35 -0
- package/dist/clis/notebooklm/binding.test.d.ts +1 -0
- package/dist/clis/notebooklm/binding.test.js +44 -0
- package/dist/clis/notebooklm/compat.test.d.ts +3 -0
- package/dist/clis/notebooklm/compat.test.js +16 -0
- package/dist/clis/notebooklm/current.d.ts +1 -0
- package/dist/clis/notebooklm/current.js +28 -0
- package/dist/clis/notebooklm/get.d.ts +1 -0
- package/dist/clis/notebooklm/get.js +37 -0
- package/dist/clis/notebooklm/history.d.ts +1 -0
- package/dist/clis/notebooklm/history.js +25 -0
- package/dist/clis/notebooklm/history.test.d.ts +1 -0
- package/dist/clis/notebooklm/history.test.js +58 -0
- package/dist/clis/notebooklm/list.d.ts +1 -0
- package/dist/clis/notebooklm/list.js +35 -0
- package/dist/clis/notebooklm/note-list.d.ts +1 -0
- package/dist/clis/notebooklm/note-list.js +28 -0
- package/dist/clis/notebooklm/note-list.test.d.ts +1 -0
- package/dist/clis/notebooklm/note-list.test.js +56 -0
- package/dist/clis/notebooklm/notes-get.d.ts +1 -0
- package/dist/clis/notebooklm/notes-get.js +47 -0
- package/dist/clis/notebooklm/notes-get.test.d.ts +1 -0
- package/dist/clis/notebooklm/notes-get.test.js +72 -0
- package/dist/clis/notebooklm/rpc.d.ts +36 -0
- package/dist/clis/notebooklm/rpc.js +189 -0
- package/dist/clis/notebooklm/rpc.test.d.ts +1 -0
- package/dist/clis/notebooklm/rpc.test.js +105 -0
- package/dist/clis/notebooklm/shared.d.ts +87 -0
- package/dist/clis/notebooklm/shared.js +3 -0
- package/dist/clis/notebooklm/source-fulltext.d.ts +1 -0
- package/dist/clis/notebooklm/source-fulltext.js +44 -0
- package/dist/clis/notebooklm/source-fulltext.test.d.ts +1 -0
- package/dist/clis/notebooklm/source-fulltext.test.js +106 -0
- package/dist/clis/notebooklm/source-get.d.ts +1 -0
- package/dist/clis/notebooklm/source-get.js +40 -0
- package/dist/clis/notebooklm/source-get.test.d.ts +1 -0
- package/dist/clis/notebooklm/source-get.test.js +84 -0
- package/dist/clis/notebooklm/source-guide.d.ts +1 -0
- package/dist/clis/notebooklm/source-guide.js +44 -0
- package/dist/clis/notebooklm/source-guide.test.d.ts +1 -0
- package/dist/clis/notebooklm/source-guide.test.js +104 -0
- package/dist/clis/notebooklm/source-list.d.ts +1 -0
- package/dist/clis/notebooklm/source-list.js +30 -0
- package/dist/clis/notebooklm/status.d.ts +1 -0
- package/dist/clis/notebooklm/status.js +31 -0
- package/dist/clis/notebooklm/summary.d.ts +1 -0
- package/dist/clis/notebooklm/summary.js +30 -0
- package/dist/clis/notebooklm/summary.test.d.ts +1 -0
- package/dist/clis/notebooklm/summary.test.js +78 -0
- package/dist/clis/notebooklm/utils.d.ts +37 -0
- package/dist/clis/notebooklm/utils.js +739 -0
- package/dist/clis/notebooklm/utils.test.d.ts +1 -0
- package/dist/clis/notebooklm/utils.test.js +390 -0
- package/dist/clis/substack/utils.d.ts +4 -0
- package/dist/clis/substack/utils.js +8 -2
- package/dist/clis/substack/utils.test.d.ts +1 -0
- package/dist/clis/substack/utils.test.js +46 -0
- package/dist/clis/v2ex/hot.yaml +4 -1
- package/dist/clis/v2ex/latest.yaml +4 -1
- package/dist/clis/v2ex/topic.yaml +6 -1
- package/dist/clis/weixin/download.d.ts +9 -0
- package/dist/clis/weixin/download.js +76 -6
- package/dist/clis/weread/book.js +108 -2
- package/dist/clis/weread/commands.test.js +262 -152
- package/dist/clis/weread/utils.d.ts +10 -0
- package/dist/clis/weread/utils.js +27 -7
- package/dist/clis/xiaohongshu/comments.d.ts +3 -0
- package/dist/clis/xiaohongshu/comments.js +76 -17
- package/dist/clis/xiaohongshu/comments.test.js +70 -9
- package/dist/clis/xiaohongshu/download.d.ts +4 -1
- package/dist/clis/xiaohongshu/download.js +83 -22
- package/dist/clis/xiaohongshu/download.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/download.test.js +75 -0
- package/dist/clis/xiaohongshu/note-helpers.d.ts +12 -0
- package/dist/clis/xiaohongshu/note-helpers.js +23 -0
- package/dist/clis/xiaohongshu/note.d.ts +7 -0
- package/dist/clis/xiaohongshu/note.js +76 -0
- package/dist/clis/xiaohongshu/note.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/note.test.js +136 -0
- package/dist/clis/xiaohongshu/search.js +9 -0
- package/dist/clis/xiaohongshu/search.test.js +10 -4
- package/dist/clis/youtube/search.js +57 -17
- package/dist/clis/zhihu/question.js +19 -17
- package/dist/clis/zhihu/question.test.d.ts +1 -0
- package/dist/clis/zhihu/question.test.js +54 -0
- package/dist/commanderAdapter.js +9 -0
- package/dist/commanderAdapter.test.js +25 -0
- package/dist/commands/daemon.d.ts +9 -0
- package/dist/commands/daemon.js +124 -0
- package/dist/commands/daemon.test.d.ts +1 -0
- package/dist/commands/daemon.test.js +185 -0
- package/dist/completion.js +3 -1
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +2 -0
- package/dist/daemon.d.ts +1 -1
- package/dist/daemon.js +25 -14
- package/dist/daemon.test.d.ts +1 -0
- package/dist/daemon.test.js +65 -0
- package/dist/discovery.d.ts +9 -0
- package/dist/discovery.js +47 -2
- package/dist/electron-apps.d.ts +29 -0
- package/dist/electron-apps.js +65 -0
- package/dist/electron-apps.test.d.ts +1 -0
- package/dist/electron-apps.test.js +43 -0
- package/dist/engine.test.js +41 -9
- package/dist/execution.js +20 -16
- package/dist/idle-manager.d.ts +19 -0
- package/dist/idle-manager.js +54 -0
- package/dist/launcher.d.ts +36 -0
- package/dist/launcher.js +152 -0
- package/dist/launcher.test.d.ts +1 -0
- package/dist/launcher.test.js +57 -0
- package/dist/main.js +3 -3
- package/dist/registry.d.ts +1 -0
- package/dist/registry.js +31 -3
- package/dist/registry.test.js +13 -0
- package/dist/runtime.d.ts +5 -3
- package/dist/runtime.js +12 -5
- package/dist/serialization.d.ts +1 -0
- package/dist/serialization.js +3 -0
- package/dist/serialization.test.js +17 -1
- package/dist/tui.d.ts +7 -0
- package/dist/tui.js +52 -0
- package/dist/tui.test.d.ts +1 -0
- package/dist/tui.test.js +19 -0
- package/dist/weixin-download.test.js +14 -0
- package/docs/.vitepress/config.mts +1 -0
- package/docs/adapters/browser/notebooklm.md +69 -0
- package/docs/adapters/browser/xiaohongshu.md +19 -10
- package/docs/adapters/index.md +67 -66
- package/docs/guide/browser-bridge.md +12 -0
- package/docs/guide/troubleshooting.md +9 -4
- package/docs/superpowers/plans/2026-03-31-daemon-lifecycle-redesign.md +857 -0
- package/docs/superpowers/specs/2026-03-31-daemon-lifecycle-redesign.md +208 -0
- package/docs/zh/guide/browser-bridge.md +12 -0
- package/extension/dist/background.js +794 -513
- package/extension/src/background.test.ts +202 -2
- package/extension/src/background.ts +174 -10
- package/extension/src/cdp.ts +12 -0
- package/extension/src/protocol.ts +7 -5
- package/package.json +1 -1
- package/src/browser/cdp.ts +24 -17
- package/src/browser/daemon-client.ts +7 -1
- package/src/browser/dom-helpers.test.ts +15 -1
- package/src/browser/dom-helpers.ts +1 -0
- package/src/browser/mcp.ts +18 -13
- package/src/browser/page.test.ts +58 -0
- package/src/browser/page.ts +18 -2
- package/src/browser/stealth.test.ts +153 -0
- package/src/browser/stealth.ts +198 -0
- package/src/browser.test.ts +1 -1
- package/src/build-manifest.test.ts +2 -0
- package/src/build-manifest.ts +6 -1
- package/src/cli.ts +21 -3
- package/src/clis/antigravity/SKILL.md +3 -12
- package/src/clis/antigravity/serve.ts +5 -10
- package/src/clis/bilibili/subtitle.test.ts +60 -0
- package/src/clis/bilibili/subtitle.ts +4 -0
- package/src/clis/chatwise/ask.ts +0 -2
- package/src/clis/chatwise/export.ts +0 -2
- package/src/clis/chatwise/history.ts +0 -2
- package/src/clis/chatwise/model.ts +0 -2
- package/src/clis/chatwise/new.ts +1 -2
- package/src/clis/chatwise/read.ts +0 -2
- package/src/clis/chatwise/screenshot.ts +1 -2
- package/src/clis/chatwise/send.ts +0 -2
- package/src/clis/chatwise/status.ts +1 -2
- package/src/clis/ctrip/search.test.ts +73 -0
- package/src/clis/ctrip/search.ts +97 -47
- package/src/clis/douyin/_shared/sts2.test.ts +31 -0
- package/src/clis/douyin/_shared/sts2.ts +11 -3
- package/src/clis/douyin/activities.test.ts +41 -1
- package/src/clis/douyin/activities.ts +12 -3
- package/src/clis/douyin/collections.test.ts +35 -2
- package/src/clis/douyin/collections.ts +1 -1
- package/src/clis/douyin/draft.test.ts +444 -2
- package/src/clis/douyin/draft.ts +382 -218
- package/src/clis/douyin/hashtag.test.ts +42 -2
- package/src/clis/douyin/hashtag.ts +11 -3
- package/src/clis/douyin/profile.test.ts +43 -1
- package/src/clis/douyin/profile.ts +9 -2
- package/src/clis/douyin/videos.test.ts +52 -2
- package/src/clis/douyin/videos.ts +49 -15
- package/src/clis/facebook/search.test.ts +70 -0
- package/src/clis/facebook/search.yaml +4 -3
- package/src/clis/instagram/download.test.ts +159 -0
- package/src/clis/instagram/download.ts +286 -0
- package/src/clis/notebooklm/bind-current.test.ts +43 -0
- package/src/clis/notebooklm/bind-current.ts +36 -0
- package/src/clis/notebooklm/binding.test.ts +53 -0
- package/src/clis/notebooklm/compat.test.ts +19 -0
- package/src/clis/notebooklm/current.ts +38 -0
- package/src/clis/notebooklm/get.ts +53 -0
- package/src/clis/notebooklm/history.test.ts +70 -0
- package/src/clis/notebooklm/history.ts +36 -0
- package/src/clis/notebooklm/list.ts +40 -0
- package/src/clis/notebooklm/note-list.test.ts +64 -0
- package/src/clis/notebooklm/note-list.ts +42 -0
- package/src/clis/notebooklm/notes-get.test.ts +88 -0
- package/src/clis/notebooklm/notes-get.ts +67 -0
- package/src/clis/notebooklm/rpc.test.ts +126 -0
- package/src/clis/notebooklm/rpc.ts +286 -0
- package/src/clis/notebooklm/shared.ts +98 -0
- package/src/clis/notebooklm/source-fulltext.test.ts +123 -0
- package/src/clis/notebooklm/source-fulltext.ts +69 -0
- package/src/clis/notebooklm/source-get.test.ts +100 -0
- package/src/clis/notebooklm/source-get.ts +60 -0
- package/src/clis/notebooklm/source-guide.test.ts +121 -0
- package/src/clis/notebooklm/source-guide.ts +69 -0
- package/src/clis/notebooklm/source-list.ts +45 -0
- package/src/clis/notebooklm/status.ts +34 -0
- package/src/clis/notebooklm/summary.test.ts +94 -0
- package/src/clis/notebooklm/summary.ts +45 -0
- package/src/clis/notebooklm/utils.test.ts +446 -0
- package/src/clis/notebooklm/utils.ts +893 -0
- package/src/clis/substack/utils.test.ts +54 -0
- package/src/clis/substack/utils.ts +10 -2
- package/src/clis/v2ex/hot.yaml +4 -1
- package/src/clis/v2ex/latest.yaml +4 -1
- package/src/clis/v2ex/topic.yaml +6 -1
- package/src/clis/weixin/download.ts +95 -6
- package/src/clis/weread/book.ts +142 -2
- package/src/clis/weread/commands.test.ts +314 -154
- package/src/clis/weread/utils.ts +33 -4
- package/src/clis/xiaohongshu/comments.test.ts +85 -9
- package/src/clis/xiaohongshu/comments.ts +76 -17
- package/src/clis/xiaohongshu/download.test.ts +96 -0
- package/src/clis/xiaohongshu/download.ts +83 -22
- package/src/clis/xiaohongshu/note-helpers.ts +25 -0
- package/src/clis/xiaohongshu/note.test.ts +164 -0
- package/src/clis/xiaohongshu/note.ts +86 -0
- package/src/clis/xiaohongshu/search.test.ts +11 -4
- package/src/clis/xiaohongshu/search.ts +13 -0
- package/src/clis/youtube/search.ts +57 -17
- package/src/clis/zhihu/question.test.ts +71 -0
- package/src/clis/zhihu/question.ts +27 -15
- package/src/commanderAdapter.test.ts +30 -0
- package/src/commanderAdapter.ts +7 -0
- package/src/commands/daemon.test.ts +238 -0
- package/src/commands/daemon.ts +135 -0
- package/src/completion.ts +2 -1
- package/src/constants.ts +3 -0
- package/src/daemon.test.ts +88 -0
- package/src/daemon.ts +26 -14
- package/src/discovery.ts +52 -2
- package/src/electron-apps.test.ts +50 -0
- package/src/electron-apps.ts +89 -0
- package/src/engine.test.ts +45 -9
- package/src/execution.ts +24 -19
- package/src/idle-manager.ts +60 -0
- package/src/launcher.test.ts +67 -0
- package/src/launcher.ts +185 -0
- package/src/main.ts +3 -2
- package/src/registry.test.ts +15 -0
- package/src/registry.ts +32 -3
- package/src/runtime.ts +13 -7
- package/src/serialization.test.ts +19 -1
- package/src/serialization.ts +2 -0
- package/src/tui.test.ts +23 -0
- package/src/tui.ts +65 -0
- package/src/weixin-download.test.ts +27 -0
- package/tests/e2e/browser-public-extended.test.ts +6 -2
- package/chatwise-opencli.ps1 +0 -82
- package/dist/clis/chatwise/shared.d.ts +0 -2
- package/dist/clis/chatwise/shared.js +0 -6
- package/src/clis/chatwise/shared.ts +0 -8
package/dist/clis/weread/book.js
CHANGED
|
@@ -1,6 +1,107 @@
|
|
|
1
1
|
import { cli, Strategy } from '../../registry.js';
|
|
2
2
|
import { CliError } from '../../errors.js';
|
|
3
|
-
import { fetchPrivateApi,
|
|
3
|
+
import { fetchPrivateApi, fetchWebApi, resolveShelfReader, WEREAD_UA, WEREAD_WEB_ORIGIN, } from './utils.js';
|
|
4
|
+
function decodeHtmlText(value) {
|
|
5
|
+
return value
|
|
6
|
+
.replace(/<[^>]+>/g, '')
|
|
7
|
+
.replace(/&#x([0-9a-fA-F]+);/gi, (_, n) => String.fromCharCode(parseInt(n, 16)))
|
|
8
|
+
.replace(/&#(\d+);/g, (_, n) => String.fromCharCode(Number(n)))
|
|
9
|
+
.replace(/ /g, ' ')
|
|
10
|
+
.replace(/&/g, '&')
|
|
11
|
+
.replace(/"/g, '"')
|
|
12
|
+
.trim();
|
|
13
|
+
}
|
|
14
|
+
function normalizeSearchText(value) {
|
|
15
|
+
return value.replace(/\s+/g, ' ').trim();
|
|
16
|
+
}
|
|
17
|
+
function buildSearchIdentity(title, author) {
|
|
18
|
+
return `${normalizeSearchText(title)}\u0000${normalizeSearchText(author)}`;
|
|
19
|
+
}
|
|
20
|
+
function countSearchTitles(entries) {
|
|
21
|
+
const counts = new Map();
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
const key = normalizeSearchText(entry.title);
|
|
24
|
+
if (!key)
|
|
25
|
+
continue;
|
|
26
|
+
counts.set(key, (counts.get(key) || 0) + 1);
|
|
27
|
+
}
|
|
28
|
+
return counts;
|
|
29
|
+
}
|
|
30
|
+
function countSearchIdentities(entries) {
|
|
31
|
+
const counts = new Map();
|
|
32
|
+
for (const entry of entries) {
|
|
33
|
+
const key = buildSearchIdentity(entry.title, entry.author);
|
|
34
|
+
if (!normalizeSearchText(entry.title) || !normalizeSearchText(entry.author))
|
|
35
|
+
continue;
|
|
36
|
+
counts.set(key, (counts.get(key) || 0) + 1);
|
|
37
|
+
}
|
|
38
|
+
return counts;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Reuse the public search page as a last-resort reader URL source when the
|
|
42
|
+
* cached shelf page cannot provide a trustworthy bookId-to-reader mapping.
|
|
43
|
+
*/
|
|
44
|
+
async function resolveSearchReaderUrl(title, author) {
|
|
45
|
+
const normalizedTitle = normalizeSearchText(title);
|
|
46
|
+
const normalizedAuthor = normalizeSearchText(author);
|
|
47
|
+
if (!normalizedTitle)
|
|
48
|
+
return '';
|
|
49
|
+
try {
|
|
50
|
+
const [data, htmlEntries] = await Promise.all([
|
|
51
|
+
fetchWebApi('/search/global', { keyword: normalizedTitle }),
|
|
52
|
+
(async () => {
|
|
53
|
+
const url = new URL('/web/search/books', WEREAD_WEB_ORIGIN);
|
|
54
|
+
url.searchParams.set('keyword', normalizedTitle);
|
|
55
|
+
const resp = await fetch(url.toString(), {
|
|
56
|
+
headers: { 'User-Agent': WEREAD_UA },
|
|
57
|
+
});
|
|
58
|
+
if (!resp.ok)
|
|
59
|
+
return [];
|
|
60
|
+
const html = await resp.text();
|
|
61
|
+
const items = Array.from(html.matchAll(/<li[^>]*class="wr_bookList_item"[^>]*>([\s\S]*?)<\/li>/g));
|
|
62
|
+
return items.map((match) => {
|
|
63
|
+
const chunk = match[1];
|
|
64
|
+
const hrefMatch = chunk.match(/<a[^>]*href="([^"]+)"[^>]*class="wr_bookList_item_link"[^>]*>|<a[^>]*class="wr_bookList_item_link"[^>]*href="([^"]+)"[^>]*>/);
|
|
65
|
+
const titleMatch = chunk.match(/<p[^>]*class="wr_bookList_item_title"[^>]*>([\s\S]*?)<\/p>/);
|
|
66
|
+
const authorMatch = chunk.match(/<p[^>]*class="wr_bookList_item_author"[^>]*>([\s\S]*?)<\/p>/);
|
|
67
|
+
const href = hrefMatch?.[1] || hrefMatch?.[2] || '';
|
|
68
|
+
return {
|
|
69
|
+
title: decodeHtmlText(titleMatch?.[1] || ''),
|
|
70
|
+
author: decodeHtmlText(authorMatch?.[1] || ''),
|
|
71
|
+
url: href ? new URL(href, WEREAD_WEB_ORIGIN).toString() : '',
|
|
72
|
+
};
|
|
73
|
+
}).filter((entry) => entry.title && entry.url);
|
|
74
|
+
})(),
|
|
75
|
+
]);
|
|
76
|
+
const books = Array.isArray(data?.books) ? data.books : [];
|
|
77
|
+
const apiIdentityCounts = countSearchIdentities(books.map((item) => ({
|
|
78
|
+
title: item.bookInfo?.title ?? '',
|
|
79
|
+
author: item.bookInfo?.author ?? '',
|
|
80
|
+
})));
|
|
81
|
+
const htmlIdentityCounts = countSearchIdentities(htmlEntries.filter((entry) => entry.author));
|
|
82
|
+
const identityKey = buildSearchIdentity(normalizedTitle, normalizedAuthor);
|
|
83
|
+
if (normalizedAuthor &&
|
|
84
|
+
(apiIdentityCounts.get(identityKey) || 0) === 1 &&
|
|
85
|
+
(htmlIdentityCounts.get(identityKey) || 0) === 1) {
|
|
86
|
+
const exactMatch = htmlEntries.find((entry) => buildSearchIdentity(entry.title, entry.author) === identityKey);
|
|
87
|
+
if (exactMatch?.url)
|
|
88
|
+
return exactMatch.url;
|
|
89
|
+
}
|
|
90
|
+
const sameTitleHtmlEntries = htmlEntries.filter((entry) => normalizeSearchText(entry.title) === normalizedTitle);
|
|
91
|
+
if (normalizedAuthor && sameTitleHtmlEntries.some((entry) => normalizeSearchText(entry.author))) {
|
|
92
|
+
return '';
|
|
93
|
+
}
|
|
94
|
+
const apiTitleCounts = countSearchTitles(books.map((item) => ({ title: item.bookInfo?.title ?? '' })));
|
|
95
|
+
const htmlTitleCounts = countSearchTitles(htmlEntries);
|
|
96
|
+
if ((apiTitleCounts.get(normalizedTitle) || 0) !== 1 || (htmlTitleCounts.get(normalizedTitle) || 0) !== 1) {
|
|
97
|
+
return '';
|
|
98
|
+
}
|
|
99
|
+
return htmlEntries.find((entry) => normalizeSearchText(entry.title) === normalizedTitle)?.url || '';
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return '';
|
|
103
|
+
}
|
|
104
|
+
}
|
|
4
105
|
/**
|
|
5
106
|
* Read visible book metadata from the web reader cover/flyleaf page.
|
|
6
107
|
* This path is used as a fallback when the private API session has expired.
|
|
@@ -92,7 +193,12 @@ cli({
|
|
|
92
193
|
if (!(error instanceof CliError) || error.code !== 'AUTH_REQUIRED') {
|
|
93
194
|
throw error;
|
|
94
195
|
}
|
|
95
|
-
const readerUrl = await
|
|
196
|
+
const { readerUrl: resolvedReaderUrl, snapshot } = await resolveShelfReader(page, bookId);
|
|
197
|
+
let readerUrl = resolvedReaderUrl;
|
|
198
|
+
if (!readerUrl) {
|
|
199
|
+
const cachedBook = snapshot.rawBooks.find((book) => String(book?.bookId || '').trim() === bookId);
|
|
200
|
+
readerUrl = await resolveSearchReaderUrl(String(cachedBook?.title || ''), String(cachedBook?.author || ''));
|
|
201
|
+
}
|
|
96
202
|
if (!readerUrl) {
|
|
97
203
|
throw error;
|
|
98
204
|
}
|
|
@@ -18,8 +18,18 @@ describe('weread book-id positional args', () => {
|
|
|
18
18
|
const book = getRegistry().get('weread/book');
|
|
19
19
|
const highlights = getRegistry().get('weread/highlights');
|
|
20
20
|
const notes = getRegistry().get('weread/notes');
|
|
21
|
+
const repeatValue = (value, count) => Array.from({ length: count }, () => value);
|
|
22
|
+
const createPageStub = (...evaluateResults) => ({
|
|
23
|
+
getCookies: vi.fn().mockResolvedValue([
|
|
24
|
+
{ name: 'wr_vid', value: '70486028', domain: '.weread.qq.com' },
|
|
25
|
+
]),
|
|
26
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
27
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
28
|
+
evaluate: vi.fn().mockImplementation(async () => evaluateResults.shift()),
|
|
29
|
+
});
|
|
21
30
|
beforeEach(() => {
|
|
22
31
|
mockFetchPrivateApi.mockReset();
|
|
32
|
+
vi.unstubAllGlobals();
|
|
23
33
|
});
|
|
24
34
|
it('passes the positional book-id to book details', async () => {
|
|
25
35
|
mockFetchPrivateApi.mockResolvedValue({ title: 'Three Body', newRating: 880 });
|
|
@@ -28,33 +38,23 @@ describe('weread book-id positional args', () => {
|
|
|
28
38
|
});
|
|
29
39
|
it('falls back to the shelf reader page when private API auth has expired', async () => {
|
|
30
40
|
mockFetchPrivateApi.mockRejectedValue(new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first'));
|
|
31
|
-
const page = {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
intro: '对中国未来几十年的预测。',
|
|
49
|
-
category: '',
|
|
50
|
-
rating: '84.1%',
|
|
51
|
-
metadataReady: true,
|
|
52
|
-
}),
|
|
53
|
-
getCookies: vi.fn().mockResolvedValue([
|
|
54
|
-
{ name: 'wr_vid', value: '70486028', domain: '.weread.qq.com' },
|
|
55
|
-
]),
|
|
56
|
-
wait: vi.fn().mockResolvedValue(undefined),
|
|
57
|
-
};
|
|
41
|
+
const page = createPageStub({
|
|
42
|
+
cacheFound: true,
|
|
43
|
+
rawBooks: [
|
|
44
|
+
{ bookId: 'MP_WXS_3634777637', title: '文明、现代化、价值投资与中国', author: '李录' },
|
|
45
|
+
],
|
|
46
|
+
shelfIndexes: [
|
|
47
|
+
{ bookId: 'MP_WXS_3634777637', idx: 0, role: 'book' },
|
|
48
|
+
],
|
|
49
|
+
}, ['https://weread.qq.com/web/reader/6f5323f071bd7f7b6f521e8'], {
|
|
50
|
+
title: '文明、现代化、价值投资与中国',
|
|
51
|
+
author: '李录',
|
|
52
|
+
publisher: '中信出版集团',
|
|
53
|
+
intro: '对中国未来几十年的预测。',
|
|
54
|
+
category: '',
|
|
55
|
+
rating: '84.1%',
|
|
56
|
+
metadataReady: true,
|
|
57
|
+
});
|
|
58
58
|
const result = await book.func(page, { 'book-id': 'MP_WXS_3634777637' });
|
|
59
59
|
expect(page.goto).toHaveBeenNthCalledWith(1, 'https://weread.qq.com/web/shelf');
|
|
60
60
|
expect(page.goto).toHaveBeenNthCalledWith(2, 'https://weread.qq.com/web/reader/6f5323f071bd7f7b6f521e8');
|
|
@@ -72,41 +72,31 @@ describe('weread book-id positional args', () => {
|
|
|
72
72
|
});
|
|
73
73
|
it('keeps mixed shelf entries aligned when resolving MP_WXS reader urls', async () => {
|
|
74
74
|
mockFetchPrivateApi.mockRejectedValue(new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first'));
|
|
75
|
-
const page = {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
intro: '第一篇文章。',
|
|
101
|
-
category: '',
|
|
102
|
-
rating: '',
|
|
103
|
-
metadataReady: true,
|
|
104
|
-
}),
|
|
105
|
-
getCookies: vi.fn().mockResolvedValue([
|
|
106
|
-
{ name: 'wr_vid', value: '70486028', domain: '.weread.qq.com' },
|
|
107
|
-
]),
|
|
108
|
-
wait: vi.fn().mockResolvedValue(undefined),
|
|
109
|
-
};
|
|
75
|
+
const page = createPageStub({
|
|
76
|
+
cacheFound: true,
|
|
77
|
+
rawBooks: [
|
|
78
|
+
{ bookId: 'MP_WXS_1', title: '公众号文章一', author: '作者甲' },
|
|
79
|
+
{ bookId: 'BOOK_2', title: '普通书二', author: '作者乙' },
|
|
80
|
+
{ bookId: 'MP_WXS_3', title: '公众号文章三', author: '作者丙' },
|
|
81
|
+
],
|
|
82
|
+
shelfIndexes: [
|
|
83
|
+
{ bookId: 'MP_WXS_1', idx: 0, role: 'mp' },
|
|
84
|
+
{ bookId: 'BOOK_2', idx: 1, role: 'book' },
|
|
85
|
+
{ bookId: 'MP_WXS_3', idx: 2, role: 'mp' },
|
|
86
|
+
],
|
|
87
|
+
}, [
|
|
88
|
+
'https://weread.qq.com/web/reader/mp1',
|
|
89
|
+
'https://weread.qq.com/web/reader/book2',
|
|
90
|
+
'https://weread.qq.com/web/reader/mp3',
|
|
91
|
+
], {
|
|
92
|
+
title: '公众号文章一',
|
|
93
|
+
author: '作者甲',
|
|
94
|
+
publisher: '微信读书',
|
|
95
|
+
intro: '第一篇文章。',
|
|
96
|
+
category: '',
|
|
97
|
+
rating: '',
|
|
98
|
+
metadataReady: true,
|
|
99
|
+
});
|
|
110
100
|
const result = await book.func(page, { 'book-id': 'MP_WXS_1' });
|
|
111
101
|
expect(page.goto).toHaveBeenNthCalledWith(1, 'https://weread.qq.com/web/shelf');
|
|
112
102
|
expect(page.goto).toHaveBeenNthCalledWith(2, 'https://weread.qq.com/web/reader/mp1');
|
|
@@ -123,79 +113,209 @@ describe('weread book-id positional args', () => {
|
|
|
123
113
|
});
|
|
124
114
|
it('rethrows AUTH_REQUIRED when shelf ordering is incomplete and reader urls cannot be trusted', async () => {
|
|
125
115
|
mockFetchPrivateApi.mockRejectedValue(new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first'));
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
'https://weread.qq.com/web/reader/book2',
|
|
141
|
-
'https://weread.qq.com/web/reader/book1',
|
|
142
|
-
]),
|
|
143
|
-
getCookies: vi.fn().mockResolvedValue([
|
|
144
|
-
{ name: 'wr_vid', value: '70486028', domain: '.weread.qq.com' },
|
|
145
|
-
]),
|
|
146
|
-
wait: vi.fn().mockResolvedValue(undefined),
|
|
147
|
-
};
|
|
116
|
+
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('network disabled')));
|
|
117
|
+
const page = createPageStub({
|
|
118
|
+
cacheFound: true,
|
|
119
|
+
rawBooks: [
|
|
120
|
+
{ bookId: 'BOOK_1', title: '第一本', author: '作者甲' },
|
|
121
|
+
{ bookId: 'BOOK_2', title: '第二本', author: '作者乙' },
|
|
122
|
+
],
|
|
123
|
+
shelfIndexes: [
|
|
124
|
+
{ bookId: 'BOOK_2', idx: 0, role: 'book' },
|
|
125
|
+
],
|
|
126
|
+
}, [
|
|
127
|
+
'https://weread.qq.com/web/reader/book2',
|
|
128
|
+
'https://weread.qq.com/web/reader/book1',
|
|
129
|
+
]);
|
|
148
130
|
await expect(book.func(page, { 'book-id': 'BOOK_1' })).rejects.toMatchObject({
|
|
149
131
|
code: 'AUTH_REQUIRED',
|
|
150
132
|
message: 'Not logged in to WeRead',
|
|
151
133
|
});
|
|
152
134
|
expect(page.goto).toHaveBeenCalledTimes(1);
|
|
153
|
-
expect(page.goto).
|
|
135
|
+
expect(page.goto).toHaveBeenNthCalledWith(1, 'https://weread.qq.com/web/shelf');
|
|
154
136
|
});
|
|
155
|
-
it('
|
|
137
|
+
it('falls back to the public search page when a cached ordinary book has no trusted shelf reader url', async () => {
|
|
156
138
|
mockFetchPrivateApi.mockRejectedValue(new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first'));
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
139
|
+
const fetchMock = vi.fn()
|
|
140
|
+
.mockResolvedValueOnce({
|
|
141
|
+
ok: true,
|
|
142
|
+
json: () => Promise.resolve({
|
|
143
|
+
books: [
|
|
144
|
+
{
|
|
145
|
+
bookInfo: {
|
|
146
|
+
title: '数据化运营:系统方法与实践案例',
|
|
147
|
+
author: '赵宏田 江丽萍 李宁',
|
|
148
|
+
bookId: '22920382',
|
|
149
|
+
},
|
|
150
|
+
},
|
|
168
151
|
],
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
author: '
|
|
188
|
-
|
|
189
|
-
|
|
152
|
+
}),
|
|
153
|
+
})
|
|
154
|
+
.mockResolvedValueOnce({
|
|
155
|
+
ok: true,
|
|
156
|
+
text: () => Promise.resolve(`
|
|
157
|
+
<ul class="search_bookDetail_list">
|
|
158
|
+
<li class="wr_bookList_item">
|
|
159
|
+
<a class="wr_bookList_item_link" href="/web/reader/book229"></a>
|
|
160
|
+
<p class="wr_bookList_item_title">数据化运营:系统方法与实践案例</p>
|
|
161
|
+
<p class="wr_bookList_item_author">赵宏田 江丽萍 李宁</p>
|
|
162
|
+
</li>
|
|
163
|
+
</ul>
|
|
164
|
+
`),
|
|
165
|
+
});
|
|
166
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
167
|
+
const staleSnapshot = {
|
|
168
|
+
cacheFound: true,
|
|
169
|
+
rawBooks: [
|
|
170
|
+
{ bookId: '22920382', title: '数据化运营:系统方法与实践案例', author: '赵宏田 江丽萍 李宁' },
|
|
171
|
+
],
|
|
172
|
+
shelfIndexes: [
|
|
173
|
+
{ bookId: 'stale-entry', idx: 0, role: 'book' },
|
|
174
|
+
],
|
|
175
|
+
};
|
|
176
|
+
const page = createPageStub(...repeatValue(staleSnapshot, 2), {
|
|
177
|
+
title: '数据化运营:系统方法与实践案例',
|
|
178
|
+
author: '赵宏田 江丽萍 李宁',
|
|
179
|
+
publisher: '电子工业出版社',
|
|
180
|
+
intro: '一本关于数据化运营的方法论书籍。',
|
|
181
|
+
category: '',
|
|
182
|
+
rating: '',
|
|
183
|
+
metadataReady: true,
|
|
184
|
+
});
|
|
185
|
+
const result = await book.func(page, { 'book-id': '22920382' });
|
|
186
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
187
|
+
expect(String(fetchMock.mock.calls[0][0])).toContain('/web/search/global?keyword=');
|
|
188
|
+
expect(String(fetchMock.mock.calls[1][0])).toContain('/web/search/books?keyword=');
|
|
189
|
+
expect(page.goto).toHaveBeenNthCalledWith(1, 'https://weread.qq.com/web/shelf');
|
|
190
|
+
expect(page.goto).toHaveBeenNthCalledWith(2, 'https://weread.qq.com/web/reader/book229');
|
|
191
|
+
expect(result).toEqual([
|
|
192
|
+
{
|
|
193
|
+
title: '数据化运营:系统方法与实践案例',
|
|
194
|
+
author: '赵宏田 江丽萍 李宁',
|
|
195
|
+
publisher: '电子工业出版社',
|
|
196
|
+
intro: '一本关于数据化运营的方法论书籍。',
|
|
190
197
|
category: '',
|
|
191
198
|
rating: '',
|
|
192
|
-
|
|
199
|
+
},
|
|
200
|
+
]);
|
|
201
|
+
});
|
|
202
|
+
it('rethrows AUTH_REQUIRED when search fallback finds the same title with a different visible author', async () => {
|
|
203
|
+
mockFetchPrivateApi.mockRejectedValue(new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first'));
|
|
204
|
+
const fetchMock = vi.fn()
|
|
205
|
+
.mockResolvedValueOnce({
|
|
206
|
+
ok: true,
|
|
207
|
+
json: () => Promise.resolve({
|
|
208
|
+
books: [
|
|
209
|
+
{
|
|
210
|
+
bookInfo: {
|
|
211
|
+
title: '文明',
|
|
212
|
+
author: '作者乙',
|
|
213
|
+
bookId: 'wrong-book',
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
],
|
|
193
217
|
}),
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
218
|
+
})
|
|
219
|
+
.mockResolvedValueOnce({
|
|
220
|
+
ok: true,
|
|
221
|
+
text: () => Promise.resolve(`
|
|
222
|
+
<ul class="search_bookDetail_list">
|
|
223
|
+
<li class="wr_bookList_item">
|
|
224
|
+
<a class="wr_bookList_item_link" href="/web/reader/wrong-reader"></a>
|
|
225
|
+
<p class="wr_bookList_item_title">文明</p>
|
|
226
|
+
<p class="wr_bookList_item_author">作者乙</p>
|
|
227
|
+
</li>
|
|
228
|
+
</ul>
|
|
229
|
+
`),
|
|
230
|
+
});
|
|
231
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
232
|
+
const staleSnapshot = {
|
|
233
|
+
cacheFound: true,
|
|
234
|
+
rawBooks: [
|
|
235
|
+
{ bookId: 'BOOK_1', title: '文明', author: '作者甲' },
|
|
236
|
+
],
|
|
237
|
+
shelfIndexes: [
|
|
238
|
+
{ bookId: 'stale-entry', idx: 0, role: 'book' },
|
|
239
|
+
],
|
|
240
|
+
};
|
|
241
|
+
const page = createPageStub(...repeatValue(staleSnapshot, 2));
|
|
242
|
+
await expect(book.func(page, { 'book-id': 'BOOK_1' })).rejects.toMatchObject({
|
|
243
|
+
code: 'AUTH_REQUIRED',
|
|
244
|
+
message: 'Not logged in to WeRead',
|
|
245
|
+
});
|
|
246
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
247
|
+
expect(page.goto).toHaveBeenNthCalledWith(1, 'https://weread.qq.com/web/shelf');
|
|
248
|
+
});
|
|
249
|
+
it('falls back to raw cache order when shelf indexes never hydrate but rendered reader urls cover every cached entry', async () => {
|
|
250
|
+
mockFetchPrivateApi.mockRejectedValue(new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first'));
|
|
251
|
+
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('network disabled')));
|
|
252
|
+
const emptyIndexSnapshot = {
|
|
253
|
+
cacheFound: true,
|
|
254
|
+
rawBooks: [
|
|
255
|
+
{ bookId: '22920382', title: '数据化运营:系统方法与实践案例', author: '赵宏田 江丽萍 李宁' },
|
|
256
|
+
{ bookId: 'MP_WXS_3634777637', title: '方伟看10年', author: '公众号' },
|
|
257
|
+
],
|
|
258
|
+
shelfIndexes: [],
|
|
198
259
|
};
|
|
260
|
+
const page = createPageStub(...repeatValue(emptyIndexSnapshot, 2), [
|
|
261
|
+
'https://weread.qq.com/web/reader/book229',
|
|
262
|
+
'https://weread.qq.com/web/reader/mp3634',
|
|
263
|
+
], {
|
|
264
|
+
title: '方伟看10年',
|
|
265
|
+
author: '公众号',
|
|
266
|
+
publisher: '',
|
|
267
|
+
intro: '公众号文章详情。',
|
|
268
|
+
category: '',
|
|
269
|
+
rating: '',
|
|
270
|
+
metadataReady: true,
|
|
271
|
+
});
|
|
272
|
+
const result = await book.func(page, { 'book-id': 'MP_WXS_3634777637' });
|
|
273
|
+
expect(page.goto).toHaveBeenNthCalledWith(1, 'https://weread.qq.com/web/shelf');
|
|
274
|
+
expect(page.goto).toHaveBeenNthCalledWith(2, 'https://weread.qq.com/web/reader/mp3634');
|
|
275
|
+
expect(result).toEqual([
|
|
276
|
+
{
|
|
277
|
+
title: '方伟看10年',
|
|
278
|
+
author: '公众号',
|
|
279
|
+
publisher: '',
|
|
280
|
+
intro: '公众号文章详情。',
|
|
281
|
+
category: '',
|
|
282
|
+
rating: '',
|
|
283
|
+
},
|
|
284
|
+
]);
|
|
285
|
+
});
|
|
286
|
+
it('waits for shelf indexes to hydrate before resolving a trusted reader url', async () => {
|
|
287
|
+
mockFetchPrivateApi.mockRejectedValue(new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first'));
|
|
288
|
+
const page = createPageStub({
|
|
289
|
+
cacheFound: true,
|
|
290
|
+
rawBooks: [
|
|
291
|
+
{ bookId: 'BOOK_1', title: '第一本', author: '作者甲' },
|
|
292
|
+
{ bookId: 'BOOK_2', title: '第二本', author: '作者乙' },
|
|
293
|
+
],
|
|
294
|
+
shelfIndexes: [
|
|
295
|
+
{ bookId: 'BOOK_2', idx: 0, role: 'book' },
|
|
296
|
+
],
|
|
297
|
+
}, {
|
|
298
|
+
cacheFound: true,
|
|
299
|
+
rawBooks: [
|
|
300
|
+
{ bookId: 'BOOK_1', title: '第一本', author: '作者甲' },
|
|
301
|
+
{ bookId: 'BOOK_2', title: '第二本', author: '作者乙' },
|
|
302
|
+
],
|
|
303
|
+
shelfIndexes: [
|
|
304
|
+
{ bookId: 'BOOK_2', idx: 0, role: 'book' },
|
|
305
|
+
{ bookId: 'BOOK_1', idx: 1, role: 'book' },
|
|
306
|
+
],
|
|
307
|
+
}, [
|
|
308
|
+
'https://weread.qq.com/web/reader/book2',
|
|
309
|
+
'https://weread.qq.com/web/reader/book1',
|
|
310
|
+
], {
|
|
311
|
+
title: '第一本',
|
|
312
|
+
author: '作者甲',
|
|
313
|
+
publisher: '出版社甲',
|
|
314
|
+
intro: '简介甲',
|
|
315
|
+
category: '',
|
|
316
|
+
rating: '',
|
|
317
|
+
metadataReady: true,
|
|
318
|
+
});
|
|
199
319
|
const result = await book.func(page, { 'book-id': 'BOOK_1' });
|
|
200
320
|
expect(page.goto).toHaveBeenNthCalledWith(1, 'https://weread.qq.com/web/shelf');
|
|
201
321
|
expect(page.goto).toHaveBeenNthCalledWith(2, 'https://weread.qq.com/web/reader/book1');
|
|
@@ -212,35 +332,25 @@ describe('weread book-id positional args', () => {
|
|
|
212
332
|
});
|
|
213
333
|
it('rethrows AUTH_REQUIRED when the reader page lacks stable cover metadata', async () => {
|
|
214
334
|
mockFetchPrivateApi.mockRejectedValue(new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first'));
|
|
215
|
-
const page = {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
intro: '这是正文第一段,不应该被当成简介。',
|
|
235
|
-
category: '',
|
|
236
|
-
rating: '',
|
|
237
|
-
metadataReady: false,
|
|
238
|
-
}),
|
|
239
|
-
getCookies: vi.fn().mockResolvedValue([
|
|
240
|
-
{ name: 'wr_vid', value: '70486028', domain: '.weread.qq.com' },
|
|
241
|
-
]),
|
|
242
|
-
wait: vi.fn().mockResolvedValue(undefined),
|
|
243
|
-
};
|
|
335
|
+
const page = createPageStub({
|
|
336
|
+
cacheFound: true,
|
|
337
|
+
rawBooks: [
|
|
338
|
+
{ bookId: 'BOOK_1', title: '第一本', author: '作者甲' },
|
|
339
|
+
],
|
|
340
|
+
shelfIndexes: [
|
|
341
|
+
{ bookId: 'BOOK_1', idx: 0, role: 'book' },
|
|
342
|
+
],
|
|
343
|
+
}, [
|
|
344
|
+
'https://weread.qq.com/web/reader/book1',
|
|
345
|
+
], {
|
|
346
|
+
title: '',
|
|
347
|
+
author: '',
|
|
348
|
+
publisher: '',
|
|
349
|
+
intro: '这是正文第一段,不应该被当成简介。',
|
|
350
|
+
category: '',
|
|
351
|
+
rating: '',
|
|
352
|
+
metadataReady: false,
|
|
353
|
+
});
|
|
244
354
|
await expect(book.func(page, { 'book-id': 'BOOK_1' })).rejects.toMatchObject({
|
|
245
355
|
code: 'AUTH_REQUIRED',
|
|
246
356
|
message: 'Not logged in to WeRead',
|
|
@@ -31,6 +31,10 @@ export interface WebShelfEntry {
|
|
|
31
31
|
author: string;
|
|
32
32
|
readerUrl: string;
|
|
33
33
|
}
|
|
34
|
+
export interface WebShelfReaderResolution {
|
|
35
|
+
snapshot: WebShelfSnapshot;
|
|
36
|
+
readerUrl: string | null;
|
|
37
|
+
}
|
|
34
38
|
/**
|
|
35
39
|
* Fetch a public WeRead web endpoint (Node.js direct fetch).
|
|
36
40
|
* Used by search and ranking commands (browser: false).
|
|
@@ -62,5 +66,11 @@ export declare function loadWebShelfSnapshot(page: IPage): Promise<WebShelfSnaps
|
|
|
62
66
|
* shelf cache order with the visible shelf links rendered on the page.
|
|
63
67
|
*/
|
|
64
68
|
export declare function resolveShelfReaderUrl(page: IPage, bookId: string): Promise<string | null>;
|
|
69
|
+
/**
|
|
70
|
+
* Resolve the current reader URL for a shelf entry and return the parsed shelf
|
|
71
|
+
* snapshot used during resolution, so callers can reuse cached title/author
|
|
72
|
+
* metadata without loading the shelf page twice.
|
|
73
|
+
*/
|
|
74
|
+
export declare function resolveShelfReader(page: IPage, bookId: string): Promise<WebShelfReaderResolution>;
|
|
65
75
|
/** Format a Unix timestamp (seconds) to YYYY-MM-DD in UTC+8. Returns '-' for invalid input. */
|
|
66
76
|
export declare function formatDate(ts: number | undefined | null): string;
|