@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
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import type { IPage } from '../../types.js';
|
|
3
|
+
import { __test__, loadSubstackArchive, loadSubstackFeed } from './utils.js';
|
|
4
|
+
|
|
5
|
+
function createPageMock(evaluateResult: unknown): IPage {
|
|
6
|
+
return {
|
|
7
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
8
|
+
evaluate: vi.fn().mockResolvedValue(evaluateResult),
|
|
9
|
+
snapshot: vi.fn().mockResolvedValue(undefined),
|
|
10
|
+
click: vi.fn().mockResolvedValue(undefined),
|
|
11
|
+
typeText: vi.fn().mockResolvedValue(undefined),
|
|
12
|
+
pressKey: vi.fn().mockResolvedValue(undefined),
|
|
13
|
+
scrollTo: vi.fn().mockResolvedValue(undefined),
|
|
14
|
+
getFormState: vi.fn().mockResolvedValue({}),
|
|
15
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
16
|
+
tabs: vi.fn().mockResolvedValue([]),
|
|
17
|
+
closeTab: vi.fn().mockResolvedValue(undefined),
|
|
18
|
+
newTab: vi.fn().mockResolvedValue(undefined),
|
|
19
|
+
selectTab: vi.fn().mockResolvedValue(undefined),
|
|
20
|
+
networkRequests: vi.fn().mockResolvedValue([]),
|
|
21
|
+
consoleMessages: vi.fn().mockResolvedValue([]),
|
|
22
|
+
scroll: vi.fn().mockResolvedValue(undefined),
|
|
23
|
+
autoScroll: vi.fn().mockResolvedValue(undefined),
|
|
24
|
+
installInterceptor: vi.fn().mockResolvedValue(undefined),
|
|
25
|
+
getInterceptedRequests: vi.fn().mockResolvedValue([]),
|
|
26
|
+
getCookies: vi.fn().mockResolvedValue([]),
|
|
27
|
+
screenshot: vi.fn().mockResolvedValue(''),
|
|
28
|
+
waitForCapture: vi.fn().mockResolvedValue(undefined),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('substack utils wait selectors', () => {
|
|
33
|
+
it('waits for both feed link shapes before scraping the feed', async () => {
|
|
34
|
+
const page = createPageMock([]);
|
|
35
|
+
|
|
36
|
+
await loadSubstackFeed(page, 'https://substack.com/', 5);
|
|
37
|
+
|
|
38
|
+
expect(page.wait).toHaveBeenCalledWith({
|
|
39
|
+
selector: __test__.FEED_POST_LINK_SELECTOR,
|
|
40
|
+
timeout: 5,
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('waits for archive post links before scraping archive pages', async () => {
|
|
45
|
+
const page = createPageMock([]);
|
|
46
|
+
|
|
47
|
+
await loadSubstackArchive(page, 'https://example.substack.com', 5);
|
|
48
|
+
|
|
49
|
+
expect(page.wait).toHaveBeenCalledWith({
|
|
50
|
+
selector: __test__.ARCHIVE_POST_LINK_SELECTOR,
|
|
51
|
+
timeout: 5,
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { CommandExecutionError } from '../../errors.js';
|
|
2
2
|
import type { IPage } from '../../types.js';
|
|
3
3
|
|
|
4
|
+
const FEED_POST_LINK_SELECTOR = 'a[href*="/home/post/"], a[href*="/p/"]';
|
|
5
|
+
const ARCHIVE_POST_LINK_SELECTOR = 'a[href*="/p/"]';
|
|
6
|
+
|
|
4
7
|
export function buildSubstackBrowseUrl(category?: string): string {
|
|
5
8
|
if (!category || category === 'all') return 'https://substack.com/';
|
|
6
9
|
const slug = category === 'tech' ? 'technology' : category;
|
|
@@ -10,7 +13,7 @@ export function buildSubstackBrowseUrl(category?: string): string {
|
|
|
10
13
|
export async function loadSubstackFeed(page: IPage, url: string, limit: number): Promise<any[]> {
|
|
11
14
|
if (!page) throw new CommandExecutionError('Browser session required for substack feed');
|
|
12
15
|
await page.goto(url);
|
|
13
|
-
await page.wait({ selector:
|
|
16
|
+
await page.wait({ selector: FEED_POST_LINK_SELECTOR, timeout: 5 });
|
|
14
17
|
const data = await page.evaluate(`
|
|
15
18
|
(async () => {
|
|
16
19
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
@@ -79,7 +82,7 @@ export async function loadSubstackFeed(page: IPage, url: string, limit: number):
|
|
|
79
82
|
export async function loadSubstackArchive(page: IPage, baseUrl: string, limit: number): Promise<any[]> {
|
|
80
83
|
if (!page) throw new CommandExecutionError('Browser session required for substack archive');
|
|
81
84
|
await page.goto(`${baseUrl}/archive`);
|
|
82
|
-
await page.wait({ selector:
|
|
85
|
+
await page.wait({ selector: ARCHIVE_POST_LINK_SELECTOR, timeout: 5 });
|
|
83
86
|
const data = await page.evaluate(`
|
|
84
87
|
(async () => {
|
|
85
88
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
@@ -131,3 +134,8 @@ export async function loadSubstackArchive(page: IPage, baseUrl: string, limit: n
|
|
|
131
134
|
|
|
132
135
|
return Array.isArray(data) ? data : [];
|
|
133
136
|
}
|
|
137
|
+
|
|
138
|
+
export const __test__ = {
|
|
139
|
+
FEED_POST_LINK_SELECTOR,
|
|
140
|
+
ARCHIVE_POST_LINK_SELECTOR,
|
|
141
|
+
};
|
package/src/clis/v2ex/hot.yaml
CHANGED
|
@@ -16,10 +16,13 @@ pipeline:
|
|
|
16
16
|
url: https://www.v2ex.com/api/topics/hot.json
|
|
17
17
|
|
|
18
18
|
- map:
|
|
19
|
+
id: ${{ item.id }}
|
|
19
20
|
rank: ${{ index + 1 }}
|
|
20
21
|
title: ${{ item.title }}
|
|
22
|
+
node: ${{ item.node.title }}
|
|
21
23
|
replies: ${{ item.replies }}
|
|
24
|
+
url: ${{ item.url }}
|
|
22
25
|
|
|
23
26
|
- limit: ${{ args.limit }}
|
|
24
27
|
|
|
25
|
-
columns: [rank, title, replies]
|
|
28
|
+
columns: [id, rank, title, node, replies, url]
|
|
@@ -16,10 +16,13 @@ pipeline:
|
|
|
16
16
|
url: https://www.v2ex.com/api/topics/latest.json
|
|
17
17
|
|
|
18
18
|
- map:
|
|
19
|
+
id: ${{ item.id }}
|
|
19
20
|
rank: ${{ index + 1 }}
|
|
20
21
|
title: ${{ item.title }}
|
|
22
|
+
node: ${{ item.node.title }}
|
|
21
23
|
replies: ${{ item.replies }}
|
|
24
|
+
url: ${{ item.url }}
|
|
22
25
|
|
|
23
26
|
- limit: ${{ args.limit }}
|
|
24
27
|
|
|
25
|
-
columns: [rank, title, replies]
|
|
28
|
+
columns: [id, rank, title, node, replies, url]
|
package/src/clis/v2ex/topic.yaml
CHANGED
|
@@ -19,10 +19,15 @@ pipeline:
|
|
|
19
19
|
id: ${{ args.id }}
|
|
20
20
|
|
|
21
21
|
- map:
|
|
22
|
+
id: ${{ item.id }}
|
|
22
23
|
title: ${{ item.title }}
|
|
24
|
+
content: ${{ item.content }}
|
|
25
|
+
member: ${{ item.member.username }}
|
|
26
|
+
created: ${{ item.created }}
|
|
27
|
+
node: ${{ item.node.title }}
|
|
23
28
|
replies: ${{ item.replies }}
|
|
24
29
|
url: ${{ item.url }}
|
|
25
30
|
|
|
26
31
|
- limit: 1
|
|
27
32
|
|
|
28
|
-
columns: [title, replies, url]
|
|
33
|
+
columns: [id, title, content, member, created, node, replies, url]
|
|
@@ -110,6 +110,37 @@ export function extractWechatPublishTime(
|
|
|
110
110
|
return formatWechatTimestamp(rawCreateTime);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Detect WeChat anti-bot / verification gate pages before we try to parse the article.
|
|
115
|
+
*/
|
|
116
|
+
export function detectWechatAccessIssue(
|
|
117
|
+
pageText: string | null | undefined,
|
|
118
|
+
htmlStr: string,
|
|
119
|
+
): string {
|
|
120
|
+
const normalizedText = (pageText || '').replace(/\s+/g, ' ').trim();
|
|
121
|
+
|
|
122
|
+
if (
|
|
123
|
+
/环境异常/.test(normalizedText) &&
|
|
124
|
+
/(完成验证后即可继续访问|去验证)/.test(normalizedText)
|
|
125
|
+
) {
|
|
126
|
+
return 'environment verification required';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (/secitptpage\/verify\.html/.test(htmlStr) || /id=["']js_verify["']/.test(htmlStr)) {
|
|
130
|
+
return 'environment verification required';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return '';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function pickFirstWechatMetaText(...candidates: Array<string | null | undefined>): string {
|
|
137
|
+
for (const candidate of candidates) {
|
|
138
|
+
const normalized = (candidate || '').replace(/\s+/g, ' ').trim();
|
|
139
|
+
if (normalized && normalized !== 'Name cleared') return normalized;
|
|
140
|
+
}
|
|
141
|
+
return '';
|
|
142
|
+
}
|
|
143
|
+
|
|
113
144
|
/**
|
|
114
145
|
* Build a self-contained helper for execution inside page.evaluate().
|
|
115
146
|
*/
|
|
@@ -161,6 +192,31 @@ export function buildExtractWechatPublishTimeJs(): string {
|
|
|
161
192
|
}.toString()})`;
|
|
162
193
|
}
|
|
163
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Build a self-contained access-issue detector for execution inside page.evaluate().
|
|
197
|
+
*/
|
|
198
|
+
export function buildDetectWechatAccessIssueJs(): string {
|
|
199
|
+
return `(${function detectWechatAccessIssueInPage(
|
|
200
|
+
pageText: string | null | undefined,
|
|
201
|
+
htmlStr: string,
|
|
202
|
+
) {
|
|
203
|
+
const normalizedText = (pageText || '').replace(/\s+/g, ' ').trim();
|
|
204
|
+
|
|
205
|
+
if (
|
|
206
|
+
/环境异常/.test(normalizedText) &&
|
|
207
|
+
/(完成验证后即可继续访问|去验证)/.test(normalizedText)
|
|
208
|
+
) {
|
|
209
|
+
return 'environment verification required';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (/secitptpage\/verify\.html/.test(htmlStr) || /id=["']js_verify["']/.test(htmlStr)) {
|
|
213
|
+
return 'environment verification required';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return '';
|
|
217
|
+
}.toString()})`;
|
|
218
|
+
}
|
|
219
|
+
|
|
164
220
|
// ============================================================
|
|
165
221
|
// CLI Registration
|
|
166
222
|
// ============================================================
|
|
@@ -196,18 +252,34 @@ cli({
|
|
|
196
252
|
title: '',
|
|
197
253
|
author: '',
|
|
198
254
|
publishTime: '',
|
|
255
|
+
errorHint: '',
|
|
199
256
|
contentHtml: '',
|
|
200
257
|
codeBlocks: [],
|
|
201
258
|
imageUrls: []
|
|
202
259
|
};
|
|
203
260
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
261
|
+
const pickFirstText = (...selectors) => {
|
|
262
|
+
for (const selector of selectors) {
|
|
263
|
+
const text = document.querySelector(selector)?.textContent?.replace(/\\s+/g, ' ').trim() || '';
|
|
264
|
+
if (text && text !== 'Name cleared') return text;
|
|
265
|
+
}
|
|
266
|
+
return '';
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
// WeChat has multiple article templates. Newer pages use #js_text_title.
|
|
270
|
+
result.title = pickFirstText(
|
|
271
|
+
'#activity-name',
|
|
272
|
+
'#js_text_title',
|
|
273
|
+
'.rich_media_title',
|
|
274
|
+
);
|
|
207
275
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
276
|
+
result.author = pickFirstText(
|
|
277
|
+
'#js_name',
|
|
278
|
+
'.wx_follow_nickname',
|
|
279
|
+
'#profileBt .profile_nickname',
|
|
280
|
+
'.rich_media_meta.rich_media_meta_nickname',
|
|
281
|
+
'.rich_media_meta_nickname',
|
|
282
|
+
);
|
|
211
283
|
|
|
212
284
|
// Publish time: prefer the rendered DOM text, then fall back to numeric create_time values.
|
|
213
285
|
const publishTimeEl = document.querySelector('#publish_time');
|
|
@@ -217,6 +289,13 @@ cli({
|
|
|
217
289
|
document.documentElement.innerHTML,
|
|
218
290
|
);
|
|
219
291
|
|
|
292
|
+
const detectWechatAccessIssue = ${buildDetectWechatAccessIssueJs()};
|
|
293
|
+
result.errorHint = detectWechatAccessIssue(
|
|
294
|
+
document.body ? document.body.innerText : '',
|
|
295
|
+
document.documentElement.innerHTML,
|
|
296
|
+
);
|
|
297
|
+
if (result.errorHint) return result;
|
|
298
|
+
|
|
220
299
|
// Content processing
|
|
221
300
|
const contentEl = document.querySelector('#js_content');
|
|
222
301
|
if (!contentEl) return result;
|
|
@@ -268,6 +347,16 @@ cli({
|
|
|
268
347
|
})()
|
|
269
348
|
`);
|
|
270
349
|
|
|
350
|
+
if (data?.errorHint === 'environment verification required') {
|
|
351
|
+
return [{
|
|
352
|
+
title: 'Error',
|
|
353
|
+
author: '-',
|
|
354
|
+
publish_time: '-',
|
|
355
|
+
status: 'failed — verification required in WeChat browser page',
|
|
356
|
+
size: '-',
|
|
357
|
+
}];
|
|
358
|
+
}
|
|
359
|
+
|
|
271
360
|
return downloadArticle(
|
|
272
361
|
{
|
|
273
362
|
title: data?.title || '',
|
package/src/clis/weread/book.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { cli, Strategy } from '../../registry.js';
|
|
2
2
|
import { CliError } from '../../errors.js';
|
|
3
3
|
import type { IPage } from '../../types.js';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
fetchPrivateApi,
|
|
6
|
+
fetchWebApi,
|
|
7
|
+
resolveShelfReader,
|
|
8
|
+
WEREAD_UA,
|
|
9
|
+
WEREAD_WEB_ORIGIN,
|
|
10
|
+
} from './utils.js';
|
|
5
11
|
|
|
6
12
|
interface ReaderFallbackResult {
|
|
7
13
|
title: string;
|
|
@@ -13,6 +19,132 @@ interface ReaderFallbackResult {
|
|
|
13
19
|
metadataReady: boolean;
|
|
14
20
|
}
|
|
15
21
|
|
|
22
|
+
interface SearchHtmlEntry {
|
|
23
|
+
title: string;
|
|
24
|
+
author: string;
|
|
25
|
+
url: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function decodeHtmlText(value: string): string {
|
|
29
|
+
return value
|
|
30
|
+
.replace(/<[^>]+>/g, '')
|
|
31
|
+
.replace(/&#x([0-9a-fA-F]+);/gi, (_, n) => String.fromCharCode(parseInt(n, 16)))
|
|
32
|
+
.replace(/&#(\d+);/g, (_, n) => String.fromCharCode(Number(n)))
|
|
33
|
+
.replace(/ /g, ' ')
|
|
34
|
+
.replace(/&/g, '&')
|
|
35
|
+
.replace(/"/g, '"')
|
|
36
|
+
.trim();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeSearchText(value: string): string {
|
|
40
|
+
return value.replace(/\s+/g, ' ').trim();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function buildSearchIdentity(title: string, author: string): string {
|
|
44
|
+
return `${normalizeSearchText(title)}\u0000${normalizeSearchText(author)}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function countSearchTitles(entries: Array<{ title: string }>): Map<string, number> {
|
|
48
|
+
const counts = new Map<string, number>();
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
const key = normalizeSearchText(entry.title);
|
|
51
|
+
if (!key) continue;
|
|
52
|
+
counts.set(key, (counts.get(key) || 0) + 1);
|
|
53
|
+
}
|
|
54
|
+
return counts;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function countSearchIdentities(entries: Array<{ title: string; author: string }>): Map<string, number> {
|
|
58
|
+
const counts = new Map<string, number>();
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
const key = buildSearchIdentity(entry.title, entry.author);
|
|
61
|
+
if (!normalizeSearchText(entry.title) || !normalizeSearchText(entry.author)) continue;
|
|
62
|
+
counts.set(key, (counts.get(key) || 0) + 1);
|
|
63
|
+
}
|
|
64
|
+
return counts;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Reuse the public search page as a last-resort reader URL source when the
|
|
69
|
+
* cached shelf page cannot provide a trustworthy bookId-to-reader mapping.
|
|
70
|
+
*/
|
|
71
|
+
async function resolveSearchReaderUrl(title: string, author: string): Promise<string> {
|
|
72
|
+
const normalizedTitle = normalizeSearchText(title);
|
|
73
|
+
const normalizedAuthor = normalizeSearchText(author);
|
|
74
|
+
if (!normalizedTitle) return '';
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const [data, htmlEntries] = await Promise.all([
|
|
78
|
+
fetchWebApi('/search/global', { keyword: normalizedTitle }),
|
|
79
|
+
(async (): Promise<SearchHtmlEntry[]> => {
|
|
80
|
+
const url = new URL('/web/search/books', WEREAD_WEB_ORIGIN);
|
|
81
|
+
url.searchParams.set('keyword', normalizedTitle);
|
|
82
|
+
|
|
83
|
+
const resp = await fetch(url.toString(), {
|
|
84
|
+
headers: { 'User-Agent': WEREAD_UA },
|
|
85
|
+
});
|
|
86
|
+
if (!resp.ok) return [];
|
|
87
|
+
|
|
88
|
+
const html = await resp.text();
|
|
89
|
+
const items = Array.from(
|
|
90
|
+
html.matchAll(/<li[^>]*class="wr_bookList_item"[^>]*>([\s\S]*?)<\/li>/g),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
return items.map((match) => {
|
|
94
|
+
const chunk = match[1];
|
|
95
|
+
const hrefMatch = chunk.match(/<a[^>]*href="([^"]+)"[^>]*class="wr_bookList_item_link"[^>]*>|<a[^>]*class="wr_bookList_item_link"[^>]*href="([^"]+)"[^>]*>/);
|
|
96
|
+
const titleMatch = chunk.match(/<p[^>]*class="wr_bookList_item_title"[^>]*>([\s\S]*?)<\/p>/);
|
|
97
|
+
const authorMatch = chunk.match(/<p[^>]*class="wr_bookList_item_author"[^>]*>([\s\S]*?)<\/p>/);
|
|
98
|
+
const href = hrefMatch?.[1] || hrefMatch?.[2] || '';
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
title: decodeHtmlText(titleMatch?.[1] || ''),
|
|
102
|
+
author: decodeHtmlText(authorMatch?.[1] || ''),
|
|
103
|
+
url: href ? new URL(href, WEREAD_WEB_ORIGIN).toString() : '',
|
|
104
|
+
};
|
|
105
|
+
}).filter((entry) => entry.title && entry.url);
|
|
106
|
+
})(),
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
const books: any[] = Array.isArray(data?.books) ? data.books : [];
|
|
110
|
+
const apiIdentityCounts = countSearchIdentities(
|
|
111
|
+
books.map((item: any) => ({
|
|
112
|
+
title: item.bookInfo?.title ?? '',
|
|
113
|
+
author: item.bookInfo?.author ?? '',
|
|
114
|
+
})),
|
|
115
|
+
);
|
|
116
|
+
const htmlIdentityCounts = countSearchIdentities(
|
|
117
|
+
htmlEntries.filter((entry) => entry.author),
|
|
118
|
+
);
|
|
119
|
+
const identityKey = buildSearchIdentity(normalizedTitle, normalizedAuthor);
|
|
120
|
+
if (
|
|
121
|
+
normalizedAuthor &&
|
|
122
|
+
(apiIdentityCounts.get(identityKey) || 0) === 1 &&
|
|
123
|
+
(htmlIdentityCounts.get(identityKey) || 0) === 1
|
|
124
|
+
) {
|
|
125
|
+
const exactMatch = htmlEntries.find((entry) => buildSearchIdentity(entry.title, entry.author) === identityKey);
|
|
126
|
+
if (exactMatch?.url) return exactMatch.url;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const sameTitleHtmlEntries = htmlEntries.filter((entry) => normalizeSearchText(entry.title) === normalizedTitle);
|
|
130
|
+
if (normalizedAuthor && sameTitleHtmlEntries.some((entry) => normalizeSearchText(entry.author))) {
|
|
131
|
+
return '';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const apiTitleCounts = countSearchTitles(
|
|
135
|
+
books.map((item: any) => ({ title: item.bookInfo?.title ?? '' })),
|
|
136
|
+
);
|
|
137
|
+
const htmlTitleCounts = countSearchTitles(htmlEntries);
|
|
138
|
+
if ((apiTitleCounts.get(normalizedTitle) || 0) !== 1 || (htmlTitleCounts.get(normalizedTitle) || 0) !== 1) {
|
|
139
|
+
return '';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return htmlEntries.find((entry) => normalizeSearchText(entry.title) === normalizedTitle)?.url || '';
|
|
143
|
+
} catch {
|
|
144
|
+
return '';
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
16
148
|
/**
|
|
17
149
|
* Read visible book metadata from the web reader cover/flyleaf page.
|
|
18
150
|
* This path is used as a fallback when the private API session has expired.
|
|
@@ -108,7 +240,15 @@ cli({
|
|
|
108
240
|
throw error;
|
|
109
241
|
}
|
|
110
242
|
|
|
111
|
-
const readerUrl = await
|
|
243
|
+
const { readerUrl: resolvedReaderUrl, snapshot } = await resolveShelfReader(page, bookId);
|
|
244
|
+
let readerUrl = resolvedReaderUrl;
|
|
245
|
+
if (!readerUrl) {
|
|
246
|
+
const cachedBook = snapshot.rawBooks.find((book) => String(book?.bookId || '').trim() === bookId);
|
|
247
|
+
readerUrl = await resolveSearchReaderUrl(
|
|
248
|
+
String(cachedBook?.title || ''),
|
|
249
|
+
String(cachedBook?.author || ''),
|
|
250
|
+
);
|
|
251
|
+
}
|
|
112
252
|
if (!readerUrl) {
|
|
113
253
|
throw error;
|
|
114
254
|
}
|