@jackwener/opencli 1.5.6 → 1.5.8
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 +34 -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/extension-manifest-regression.test.js +1 -0
- 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 +250 -11
- package/extension/manifest.json +2 -1
- package/extension/src/background.test.ts +202 -2
- package/extension/src/background.ts +175 -10
- package/extension/src/cdp.test.ts +75 -0
- package/extension/src/cdp.ts +89 -3
- 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/extension-manifest-regression.test.ts +1 -0
- 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
|
@@ -23,8 +23,20 @@ describe('weread book-id positional args', () => {
|
|
|
23
23
|
const highlights = getRegistry().get('weread/highlights');
|
|
24
24
|
const notes = getRegistry().get('weread/notes');
|
|
25
25
|
|
|
26
|
+
const repeatValue = <T,>(value: T, count: number): T[] => Array.from({ length: count }, () => value);
|
|
27
|
+
|
|
28
|
+
const createPageStub = (...evaluateResults: unknown[]) => ({
|
|
29
|
+
getCookies: vi.fn().mockResolvedValue([
|
|
30
|
+
{ name: 'wr_vid', value: '70486028', domain: '.weread.qq.com' },
|
|
31
|
+
]),
|
|
32
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
33
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
34
|
+
evaluate: vi.fn().mockImplementation(async () => evaluateResults.shift()),
|
|
35
|
+
});
|
|
36
|
+
|
|
26
37
|
beforeEach(() => {
|
|
27
38
|
mockFetchPrivateApi.mockReset();
|
|
39
|
+
vi.unstubAllGlobals();
|
|
28
40
|
});
|
|
29
41
|
|
|
30
42
|
it('passes the positional book-id to book details', async () => {
|
|
@@ -40,33 +52,27 @@ describe('weread book-id positional args', () => {
|
|
|
40
52
|
new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first'),
|
|
41
53
|
);
|
|
42
54
|
|
|
43
|
-
const page =
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}),
|
|
65
|
-
getCookies: vi.fn().mockResolvedValue([
|
|
66
|
-
{ name: 'wr_vid', value: '70486028', domain: '.weread.qq.com' },
|
|
67
|
-
]),
|
|
68
|
-
wait: vi.fn().mockResolvedValue(undefined),
|
|
69
|
-
} as any;
|
|
55
|
+
const page = createPageStub(
|
|
56
|
+
{
|
|
57
|
+
cacheFound: true,
|
|
58
|
+
rawBooks: [
|
|
59
|
+
{ bookId: 'MP_WXS_3634777637', title: '文明、现代化、价值投资与中国', author: '李录' },
|
|
60
|
+
],
|
|
61
|
+
shelfIndexes: [
|
|
62
|
+
{ bookId: 'MP_WXS_3634777637', idx: 0, role: 'book' },
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
['https://weread.qq.com/web/reader/6f5323f071bd7f7b6f521e8'],
|
|
66
|
+
{
|
|
67
|
+
title: '文明、现代化、价值投资与中国',
|
|
68
|
+
author: '李录',
|
|
69
|
+
publisher: '中信出版集团',
|
|
70
|
+
intro: '对中国未来几十年的预测。',
|
|
71
|
+
category: '',
|
|
72
|
+
rating: '84.1%',
|
|
73
|
+
metadataReady: true,
|
|
74
|
+
},
|
|
75
|
+
) as any;
|
|
70
76
|
|
|
71
77
|
const result = await book!.func!(page, { 'book-id': 'MP_WXS_3634777637' });
|
|
72
78
|
|
|
@@ -90,41 +96,35 @@ describe('weread book-id positional args', () => {
|
|
|
90
96
|
new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first'),
|
|
91
97
|
);
|
|
92
98
|
|
|
93
|
-
const page =
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}),
|
|
123
|
-
getCookies: vi.fn().mockResolvedValue([
|
|
124
|
-
{ name: 'wr_vid', value: '70486028', domain: '.weread.qq.com' },
|
|
125
|
-
]),
|
|
126
|
-
wait: vi.fn().mockResolvedValue(undefined),
|
|
127
|
-
} as any;
|
|
99
|
+
const page = createPageStub(
|
|
100
|
+
{
|
|
101
|
+
cacheFound: true,
|
|
102
|
+
rawBooks: [
|
|
103
|
+
{ bookId: 'MP_WXS_1', title: '公众号文章一', author: '作者甲' },
|
|
104
|
+
{ bookId: 'BOOK_2', title: '普通书二', author: '作者乙' },
|
|
105
|
+
{ bookId: 'MP_WXS_3', title: '公众号文章三', author: '作者丙' },
|
|
106
|
+
],
|
|
107
|
+
shelfIndexes: [
|
|
108
|
+
{ bookId: 'MP_WXS_1', idx: 0, role: 'mp' },
|
|
109
|
+
{ bookId: 'BOOK_2', idx: 1, role: 'book' },
|
|
110
|
+
{ bookId: 'MP_WXS_3', idx: 2, role: 'mp' },
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
[
|
|
114
|
+
'https://weread.qq.com/web/reader/mp1',
|
|
115
|
+
'https://weread.qq.com/web/reader/book2',
|
|
116
|
+
'https://weread.qq.com/web/reader/mp3',
|
|
117
|
+
],
|
|
118
|
+
{
|
|
119
|
+
title: '公众号文章一',
|
|
120
|
+
author: '作者甲',
|
|
121
|
+
publisher: '微信读书',
|
|
122
|
+
intro: '第一篇文章。',
|
|
123
|
+
category: '',
|
|
124
|
+
rating: '',
|
|
125
|
+
metadataReady: true,
|
|
126
|
+
},
|
|
127
|
+
) as any;
|
|
128
128
|
|
|
129
129
|
const result = await book!.func!(page, { 'book-id': 'MP_WXS_1' });
|
|
130
130
|
|
|
@@ -146,85 +146,251 @@ describe('weread book-id positional args', () => {
|
|
|
146
146
|
mockFetchPrivateApi.mockRejectedValue(
|
|
147
147
|
new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first'),
|
|
148
148
|
);
|
|
149
|
+
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('network disabled')));
|
|
149
150
|
|
|
150
|
-
const page =
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
]),
|
|
167
|
-
getCookies: vi.fn().mockResolvedValue([
|
|
168
|
-
{ name: 'wr_vid', value: '70486028', domain: '.weread.qq.com' },
|
|
169
|
-
]),
|
|
170
|
-
wait: vi.fn().mockResolvedValue(undefined),
|
|
171
|
-
} as any;
|
|
151
|
+
const page = createPageStub(
|
|
152
|
+
{
|
|
153
|
+
cacheFound: true,
|
|
154
|
+
rawBooks: [
|
|
155
|
+
{ bookId: 'BOOK_1', title: '第一本', author: '作者甲' },
|
|
156
|
+
{ bookId: 'BOOK_2', title: '第二本', author: '作者乙' },
|
|
157
|
+
],
|
|
158
|
+
shelfIndexes: [
|
|
159
|
+
{ bookId: 'BOOK_2', idx: 0, role: 'book' },
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
[
|
|
163
|
+
'https://weread.qq.com/web/reader/book2',
|
|
164
|
+
'https://weread.qq.com/web/reader/book1',
|
|
165
|
+
],
|
|
166
|
+
) as any;
|
|
172
167
|
|
|
173
168
|
await expect(book!.func!(page, { 'book-id': 'BOOK_1' })).rejects.toMatchObject({
|
|
174
169
|
code: 'AUTH_REQUIRED',
|
|
175
170
|
message: 'Not logged in to WeRead',
|
|
176
171
|
});
|
|
177
172
|
expect(page.goto).toHaveBeenCalledTimes(1);
|
|
178
|
-
expect(page.goto).
|
|
173
|
+
expect(page.goto).toHaveBeenNthCalledWith(1, 'https://weread.qq.com/web/shelf');
|
|
179
174
|
});
|
|
180
175
|
|
|
181
|
-
it('
|
|
176
|
+
it('falls back to the public search page when a cached ordinary book has no trusted shelf reader url', async () => {
|
|
182
177
|
mockFetchPrivateApi.mockRejectedValue(
|
|
183
178
|
new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first'),
|
|
184
179
|
);
|
|
185
180
|
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
.
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
181
|
+
const fetchMock = vi.fn()
|
|
182
|
+
.mockResolvedValueOnce({
|
|
183
|
+
ok: true,
|
|
184
|
+
json: () => Promise.resolve({
|
|
185
|
+
books: [
|
|
186
|
+
{
|
|
187
|
+
bookInfo: {
|
|
188
|
+
title: '数据化运营:系统方法与实践案例',
|
|
189
|
+
author: '赵宏田 江丽萍 李宁',
|
|
190
|
+
bookId: '22920382',
|
|
191
|
+
},
|
|
192
|
+
},
|
|
197
193
|
],
|
|
198
|
-
})
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
194
|
+
}),
|
|
195
|
+
})
|
|
196
|
+
.mockResolvedValueOnce({
|
|
197
|
+
ok: true,
|
|
198
|
+
text: () => Promise.resolve(`
|
|
199
|
+
<ul class="search_bookDetail_list">
|
|
200
|
+
<li class="wr_bookList_item">
|
|
201
|
+
<a class="wr_bookList_item_link" href="/web/reader/book229"></a>
|
|
202
|
+
<p class="wr_bookList_item_title">数据化运营:系统方法与实践案例</p>
|
|
203
|
+
<p class="wr_bookList_item_author">赵宏田 江丽萍 李宁</p>
|
|
204
|
+
</li>
|
|
205
|
+
</ul>
|
|
206
|
+
`),
|
|
207
|
+
});
|
|
208
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
209
|
+
|
|
210
|
+
const staleSnapshot = {
|
|
211
|
+
cacheFound: true,
|
|
212
|
+
rawBooks: [
|
|
213
|
+
{ bookId: '22920382', title: '数据化运营:系统方法与实践案例', author: '赵宏田 江丽萍 李宁' },
|
|
214
|
+
],
|
|
215
|
+
shelfIndexes: [
|
|
216
|
+
{ bookId: 'stale-entry', idx: 0, role: 'book' },
|
|
217
|
+
],
|
|
218
|
+
};
|
|
219
|
+
const page = createPageStub(
|
|
220
|
+
...repeatValue(staleSnapshot, 2),
|
|
221
|
+
{
|
|
222
|
+
title: '数据化运营:系统方法与实践案例',
|
|
223
|
+
author: '赵宏田 江丽萍 李宁',
|
|
224
|
+
publisher: '电子工业出版社',
|
|
225
|
+
intro: '一本关于数据化运营的方法论书籍。',
|
|
226
|
+
category: '',
|
|
227
|
+
rating: '',
|
|
228
|
+
metadataReady: true,
|
|
229
|
+
},
|
|
230
|
+
) as any;
|
|
231
|
+
|
|
232
|
+
const result = await book!.func!(page, { 'book-id': '22920382' });
|
|
233
|
+
|
|
234
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
235
|
+
expect(String(fetchMock.mock.calls[0][0])).toContain('/web/search/global?keyword=');
|
|
236
|
+
expect(String(fetchMock.mock.calls[1][0])).toContain('/web/search/books?keyword=');
|
|
237
|
+
expect(page.goto).toHaveBeenNthCalledWith(1, 'https://weread.qq.com/web/shelf');
|
|
238
|
+
expect(page.goto).toHaveBeenNthCalledWith(2, 'https://weread.qq.com/web/reader/book229');
|
|
239
|
+
expect(result).toEqual([
|
|
240
|
+
{
|
|
241
|
+
title: '数据化运营:系统方法与实践案例',
|
|
242
|
+
author: '赵宏田 江丽萍 李宁',
|
|
243
|
+
publisher: '电子工业出版社',
|
|
244
|
+
intro: '一本关于数据化运营的方法论书籍。',
|
|
245
|
+
category: '',
|
|
246
|
+
rating: '',
|
|
247
|
+
},
|
|
248
|
+
]);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('rethrows AUTH_REQUIRED when search fallback finds the same title with a different visible author', async () => {
|
|
252
|
+
mockFetchPrivateApi.mockRejectedValue(
|
|
253
|
+
new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first'),
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
const fetchMock = vi.fn()
|
|
257
|
+
.mockResolvedValueOnce({
|
|
258
|
+
ok: true,
|
|
259
|
+
json: () => Promise.resolve({
|
|
260
|
+
books: [
|
|
261
|
+
{
|
|
262
|
+
bookInfo: {
|
|
263
|
+
title: '文明',
|
|
264
|
+
author: '作者乙',
|
|
265
|
+
bookId: 'wrong-book',
|
|
266
|
+
},
|
|
267
|
+
},
|
|
208
268
|
],
|
|
209
|
-
})
|
|
210
|
-
.mockResolvedValueOnce([
|
|
211
|
-
'https://weread.qq.com/web/reader/book2',
|
|
212
|
-
'https://weread.qq.com/web/reader/book1',
|
|
213
|
-
])
|
|
214
|
-
.mockResolvedValueOnce({
|
|
215
|
-
title: '第一本',
|
|
216
|
-
author: '作者甲',
|
|
217
|
-
publisher: '出版社甲',
|
|
218
|
-
intro: '简介甲',
|
|
219
|
-
category: '',
|
|
220
|
-
rating: '',
|
|
221
|
-
metadataReady: true,
|
|
222
269
|
}),
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
270
|
+
})
|
|
271
|
+
.mockResolvedValueOnce({
|
|
272
|
+
ok: true,
|
|
273
|
+
text: () => Promise.resolve(`
|
|
274
|
+
<ul class="search_bookDetail_list">
|
|
275
|
+
<li class="wr_bookList_item">
|
|
276
|
+
<a class="wr_bookList_item_link" href="/web/reader/wrong-reader"></a>
|
|
277
|
+
<p class="wr_bookList_item_title">文明</p>
|
|
278
|
+
<p class="wr_bookList_item_author">作者乙</p>
|
|
279
|
+
</li>
|
|
280
|
+
</ul>
|
|
281
|
+
`),
|
|
282
|
+
});
|
|
283
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
284
|
+
|
|
285
|
+
const staleSnapshot = {
|
|
286
|
+
cacheFound: true,
|
|
287
|
+
rawBooks: [
|
|
288
|
+
{ bookId: 'BOOK_1', title: '文明', author: '作者甲' },
|
|
289
|
+
],
|
|
290
|
+
shelfIndexes: [
|
|
291
|
+
{ bookId: 'stale-entry', idx: 0, role: 'book' },
|
|
292
|
+
],
|
|
293
|
+
};
|
|
294
|
+
const page = createPageStub(
|
|
295
|
+
...repeatValue(staleSnapshot, 2),
|
|
296
|
+
) as any;
|
|
297
|
+
|
|
298
|
+
await expect(book!.func!(page, { 'book-id': 'BOOK_1' })).rejects.toMatchObject({
|
|
299
|
+
code: 'AUTH_REQUIRED',
|
|
300
|
+
message: 'Not logged in to WeRead',
|
|
301
|
+
});
|
|
302
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
303
|
+
expect(page.goto).toHaveBeenNthCalledWith(1, 'https://weread.qq.com/web/shelf');
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('falls back to raw cache order when shelf indexes never hydrate but rendered reader urls cover every cached entry', async () => {
|
|
307
|
+
mockFetchPrivateApi.mockRejectedValue(
|
|
308
|
+
new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first'),
|
|
309
|
+
);
|
|
310
|
+
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('network disabled')));
|
|
311
|
+
|
|
312
|
+
const emptyIndexSnapshot = {
|
|
313
|
+
cacheFound: true,
|
|
314
|
+
rawBooks: [
|
|
315
|
+
{ bookId: '22920382', title: '数据化运营:系统方法与实践案例', author: '赵宏田 江丽萍 李宁' },
|
|
316
|
+
{ bookId: 'MP_WXS_3634777637', title: '方伟看10年', author: '公众号' },
|
|
317
|
+
],
|
|
318
|
+
shelfIndexes: [],
|
|
319
|
+
};
|
|
320
|
+
const page = createPageStub(
|
|
321
|
+
...repeatValue(emptyIndexSnapshot, 2),
|
|
322
|
+
[
|
|
323
|
+
'https://weread.qq.com/web/reader/book229',
|
|
324
|
+
'https://weread.qq.com/web/reader/mp3634',
|
|
325
|
+
],
|
|
326
|
+
{
|
|
327
|
+
title: '方伟看10年',
|
|
328
|
+
author: '公众号',
|
|
329
|
+
publisher: '',
|
|
330
|
+
intro: '公众号文章详情。',
|
|
331
|
+
category: '',
|
|
332
|
+
rating: '',
|
|
333
|
+
metadataReady: true,
|
|
334
|
+
},
|
|
335
|
+
) as any;
|
|
336
|
+
|
|
337
|
+
const result = await book!.func!(page, { 'book-id': 'MP_WXS_3634777637' });
|
|
338
|
+
|
|
339
|
+
expect(page.goto).toHaveBeenNthCalledWith(1, 'https://weread.qq.com/web/shelf');
|
|
340
|
+
expect(page.goto).toHaveBeenNthCalledWith(2, 'https://weread.qq.com/web/reader/mp3634');
|
|
341
|
+
expect(result).toEqual([
|
|
342
|
+
{
|
|
343
|
+
title: '方伟看10年',
|
|
344
|
+
author: '公众号',
|
|
345
|
+
publisher: '',
|
|
346
|
+
intro: '公众号文章详情。',
|
|
347
|
+
category: '',
|
|
348
|
+
rating: '',
|
|
349
|
+
},
|
|
350
|
+
]);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('waits for shelf indexes to hydrate before resolving a trusted reader url', async () => {
|
|
354
|
+
mockFetchPrivateApi.mockRejectedValue(
|
|
355
|
+
new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first'),
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
const page = createPageStub(
|
|
359
|
+
{
|
|
360
|
+
cacheFound: true,
|
|
361
|
+
rawBooks: [
|
|
362
|
+
{ bookId: 'BOOK_1', title: '第一本', author: '作者甲' },
|
|
363
|
+
{ bookId: 'BOOK_2', title: '第二本', author: '作者乙' },
|
|
364
|
+
],
|
|
365
|
+
shelfIndexes: [
|
|
366
|
+
{ bookId: 'BOOK_2', idx: 0, role: 'book' },
|
|
367
|
+
],
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
cacheFound: true,
|
|
371
|
+
rawBooks: [
|
|
372
|
+
{ bookId: 'BOOK_1', title: '第一本', author: '作者甲' },
|
|
373
|
+
{ bookId: 'BOOK_2', title: '第二本', author: '作者乙' },
|
|
374
|
+
],
|
|
375
|
+
shelfIndexes: [
|
|
376
|
+
{ bookId: 'BOOK_2', idx: 0, role: 'book' },
|
|
377
|
+
{ bookId: 'BOOK_1', idx: 1, role: 'book' },
|
|
378
|
+
],
|
|
379
|
+
},
|
|
380
|
+
[
|
|
381
|
+
'https://weread.qq.com/web/reader/book2',
|
|
382
|
+
'https://weread.qq.com/web/reader/book1',
|
|
383
|
+
],
|
|
384
|
+
{
|
|
385
|
+
title: '第一本',
|
|
386
|
+
author: '作者甲',
|
|
387
|
+
publisher: '出版社甲',
|
|
388
|
+
intro: '简介甲',
|
|
389
|
+
category: '',
|
|
390
|
+
rating: '',
|
|
391
|
+
metadataReady: true,
|
|
392
|
+
},
|
|
393
|
+
) as any;
|
|
228
394
|
|
|
229
395
|
const result = await book!.func!(page, { 'book-id': 'BOOK_1' });
|
|
230
396
|
|
|
@@ -247,35 +413,29 @@ describe('weread book-id positional args', () => {
|
|
|
247
413
|
new CliError('AUTH_REQUIRED', 'Not logged in to WeRead', 'Please log in to weread.qq.com in Chrome first'),
|
|
248
414
|
);
|
|
249
415
|
|
|
250
|
-
const page =
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}),
|
|
274
|
-
getCookies: vi.fn().mockResolvedValue([
|
|
275
|
-
{ name: 'wr_vid', value: '70486028', domain: '.weread.qq.com' },
|
|
276
|
-
]),
|
|
277
|
-
wait: vi.fn().mockResolvedValue(undefined),
|
|
278
|
-
} as any;
|
|
416
|
+
const page = createPageStub(
|
|
417
|
+
{
|
|
418
|
+
cacheFound: true,
|
|
419
|
+
rawBooks: [
|
|
420
|
+
{ bookId: 'BOOK_1', title: '第一本', author: '作者甲' },
|
|
421
|
+
],
|
|
422
|
+
shelfIndexes: [
|
|
423
|
+
{ bookId: 'BOOK_1', idx: 0, role: 'book' },
|
|
424
|
+
],
|
|
425
|
+
},
|
|
426
|
+
[
|
|
427
|
+
'https://weread.qq.com/web/reader/book1',
|
|
428
|
+
],
|
|
429
|
+
{
|
|
430
|
+
title: '',
|
|
431
|
+
author: '',
|
|
432
|
+
publisher: '',
|
|
433
|
+
intro: '这是正文第一段,不应该被当成简介。',
|
|
434
|
+
category: '',
|
|
435
|
+
rating: '',
|
|
436
|
+
metadataReady: false,
|
|
437
|
+
},
|
|
438
|
+
) as any;
|
|
279
439
|
|
|
280
440
|
await expect(book!.func!(page, { 'book-id': 'BOOK_1' })).rejects.toMatchObject({
|
|
281
441
|
code: 'AUTH_REQUIRED',
|
package/src/clis/weread/utils.ts
CHANGED
|
@@ -42,6 +42,11 @@ export interface WebShelfEntry {
|
|
|
42
42
|
readerUrl: string;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
export interface WebShelfReaderResolution {
|
|
46
|
+
snapshot: WebShelfSnapshot;
|
|
47
|
+
readerUrl: string | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
45
50
|
interface WebShelfStorageKeys {
|
|
46
51
|
rawBooksKey: string;
|
|
47
52
|
shelfIndexesKey: string;
|
|
@@ -331,11 +336,29 @@ async function waitForTrustedWebShelfSnapshot(page: IPage, snapshot: WebShelfSna
|
|
|
331
336
|
* shelf cache order with the visible shelf links rendered on the page.
|
|
332
337
|
*/
|
|
333
338
|
export async function resolveShelfReaderUrl(page: IPage, bookId: string): Promise<string | null> {
|
|
339
|
+
const resolution = await resolveShelfReader(page, bookId);
|
|
340
|
+
return resolution.readerUrl;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Resolve the current reader URL for a shelf entry and return the parsed shelf
|
|
345
|
+
* snapshot used during resolution, so callers can reuse cached title/author
|
|
346
|
+
* metadata without loading the shelf page twice.
|
|
347
|
+
*/
|
|
348
|
+
export async function resolveShelfReader(page: IPage, bookId: string): Promise<WebShelfReaderResolution> {
|
|
334
349
|
const { snapshot: initialSnapshot, currentVid } = await loadWebShelfSnapshotWithVid(page);
|
|
335
350
|
const snapshot = await waitForTrustedWebShelfSnapshot(page, initialSnapshot, currentVid);
|
|
336
|
-
if (!snapshot.cacheFound)
|
|
351
|
+
if (!snapshot.cacheFound) {
|
|
352
|
+
return { snapshot, readerUrl: null };
|
|
353
|
+
}
|
|
354
|
+
const rawBookIds = getUniqueRawBookIds(snapshot);
|
|
337
355
|
const trustedIndexedBookIds = getTrustedIndexedBookIds(snapshot);
|
|
338
|
-
|
|
356
|
+
const canUseRawOrderFallback = trustedIndexedBookIds.length === 0
|
|
357
|
+
&& rawBookIds.length > 0
|
|
358
|
+
&& snapshot.shelfIndexes.length === 0;
|
|
359
|
+
if (trustedIndexedBookIds.length === 0 && !canUseRawOrderFallback) {
|
|
360
|
+
return { snapshot, readerUrl: null };
|
|
361
|
+
}
|
|
339
362
|
|
|
340
363
|
const readerUrls = await page.evaluate(`
|
|
341
364
|
(() => Array.from(document.querySelectorAll('a.shelfBook[href]'))
|
|
@@ -345,11 +368,17 @@ export async function resolveShelfReaderUrl(page: IPage, bookId: string): Promis
|
|
|
345
368
|
})
|
|
346
369
|
.filter(Boolean))
|
|
347
370
|
`) as string[];
|
|
348
|
-
|
|
371
|
+
const expectedEntryCount = trustedIndexedBookIds.length > 0 ? trustedIndexedBookIds.length : rawBookIds.length;
|
|
372
|
+
if (readerUrls.length !== expectedEntryCount) {
|
|
373
|
+
return { snapshot, readerUrl: null };
|
|
374
|
+
}
|
|
349
375
|
const entries = buildWebShelfEntries(snapshot, readerUrls);
|
|
350
376
|
|
|
351
377
|
const entry = entries.find((candidate) => candidate.bookId === bookId);
|
|
352
|
-
return
|
|
378
|
+
return {
|
|
379
|
+
snapshot,
|
|
380
|
+
readerUrl: entry?.readerUrl || null,
|
|
381
|
+
};
|
|
353
382
|
}
|
|
354
383
|
|
|
355
384
|
/** Format a Unix timestamp (seconds) to YYYY-MM-DD in UTC+8. Returns '-' for invalid input. */
|