@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
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Xiaohongshu note — read full note content from a public note page.
|
|
3
|
+
*
|
|
4
|
+
* Extracts title, author, description text, and engagement metrics
|
|
5
|
+
* (likes, collects, comment count) via DOM extraction.
|
|
6
|
+
*/
|
|
7
|
+
import { cli, Strategy } from '../../registry.js';
|
|
8
|
+
import { AuthRequiredError, EmptyResultError } from '../../errors.js';
|
|
9
|
+
import { parseNoteId, buildNoteUrl } from './note-helpers.js';
|
|
10
|
+
cli({
|
|
11
|
+
site: 'xiaohongshu',
|
|
12
|
+
name: 'note',
|
|
13
|
+
description: '获取小红书笔记正文和互动数据',
|
|
14
|
+
domain: 'www.xiaohongshu.com',
|
|
15
|
+
strategy: Strategy.COOKIE,
|
|
16
|
+
args: [
|
|
17
|
+
{ name: 'note-id', required: true, positional: true, help: 'Note ID or full URL (preserves xsec_token for access)' },
|
|
18
|
+
],
|
|
19
|
+
columns: ['field', 'value'],
|
|
20
|
+
func: async (page, kwargs) => {
|
|
21
|
+
const raw = String(kwargs['note-id']);
|
|
22
|
+
const noteId = parseNoteId(raw);
|
|
23
|
+
const url = buildNoteUrl(raw);
|
|
24
|
+
await page.goto(url);
|
|
25
|
+
await page.wait(3);
|
|
26
|
+
const data = await page.evaluate(`
|
|
27
|
+
(() => {
|
|
28
|
+
const loginWall = /登录后查看|请登录/.test(document.body.innerText || '')
|
|
29
|
+
const notFound = /页面不见了|笔记不存在|无法浏览/.test(document.body.innerText || '')
|
|
30
|
+
|
|
31
|
+
const clean = (el) => (el?.textContent || '').replace(/\\s+/g, ' ').trim()
|
|
32
|
+
|
|
33
|
+
const title = clean(document.querySelector('#detail-title, .title'))
|
|
34
|
+
const desc = clean(document.querySelector('#detail-desc, .desc, .note-text'))
|
|
35
|
+
const author = clean(document.querySelector('.username, .author-wrapper .name'))
|
|
36
|
+
const likes = clean(document.querySelector('.like-wrapper .count'))
|
|
37
|
+
const collects = clean(document.querySelector('.collect-wrapper .count'))
|
|
38
|
+
const comments = clean(document.querySelector('.chat-wrapper .count'))
|
|
39
|
+
|
|
40
|
+
// Try to extract tags/topics
|
|
41
|
+
const tags = []
|
|
42
|
+
document.querySelectorAll('#detail-desc a.tag, #detail-desc a[href*="search_result"]').forEach(el => {
|
|
43
|
+
const t = (el.textContent || '').trim()
|
|
44
|
+
if (t) tags.push(t)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
return { loginWall, notFound, title, desc, author, likes, collects, comments, tags }
|
|
48
|
+
})()
|
|
49
|
+
`);
|
|
50
|
+
if (!data || typeof data !== 'object') {
|
|
51
|
+
throw new EmptyResultError('xiaohongshu/note', 'Unexpected evaluate response');
|
|
52
|
+
}
|
|
53
|
+
if (data.loginWall) {
|
|
54
|
+
throw new AuthRequiredError('www.xiaohongshu.com', 'Note content requires login');
|
|
55
|
+
}
|
|
56
|
+
if (data.notFound) {
|
|
57
|
+
throw new EmptyResultError('xiaohongshu/note', `Note ${noteId} not found or unavailable — it may have been deleted or restricted`);
|
|
58
|
+
}
|
|
59
|
+
const d = data;
|
|
60
|
+
// XHS renders placeholder text like "赞"/"收藏"/"评论" when count is 0;
|
|
61
|
+
// normalize to '0' unless the value looks numeric.
|
|
62
|
+
const numOrZero = (v) => /^\d+/.test(v) ? v : '0';
|
|
63
|
+
const rows = [
|
|
64
|
+
{ field: 'title', value: d.title || '' },
|
|
65
|
+
{ field: 'author', value: d.author || '' },
|
|
66
|
+
{ field: 'content', value: d.desc || '' },
|
|
67
|
+
{ field: 'likes', value: numOrZero(d.likes || '') },
|
|
68
|
+
{ field: 'collects', value: numOrZero(d.collects || '') },
|
|
69
|
+
{ field: 'comments', value: numOrZero(d.comments || '') },
|
|
70
|
+
];
|
|
71
|
+
if (d.tags?.length) {
|
|
72
|
+
rows.push({ field: 'tags', value: d.tags.join(', ') });
|
|
73
|
+
}
|
|
74
|
+
return rows;
|
|
75
|
+
},
|
|
76
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './note.js';
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { getRegistry } from '../../registry.js';
|
|
3
|
+
import { parseNoteId, buildNoteUrl } from './note-helpers.js';
|
|
4
|
+
import './note.js';
|
|
5
|
+
function createPageMock(evaluateResult) {
|
|
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({ forms: [], orphanFields: [] }),
|
|
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
|
+
describe('parseNoteId', () => {
|
|
32
|
+
it('extracts ID from /explore/ URL', () => {
|
|
33
|
+
expect(parseNoteId('https://www.xiaohongshu.com/explore/69c131c9000000002800be4c')).toBe('69c131c9000000002800be4c');
|
|
34
|
+
});
|
|
35
|
+
it('extracts ID from /search_result/ URL with query params', () => {
|
|
36
|
+
expect(parseNoteId('https://www.xiaohongshu.com/search_result/69c131c9000000002800be4c?xsec_token=abc')).toBe('69c131c9000000002800be4c');
|
|
37
|
+
});
|
|
38
|
+
it('extracts ID from /note/ URL', () => {
|
|
39
|
+
expect(parseNoteId('https://www.xiaohongshu.com/note/69c131c9000000002800be4c')).toBe('69c131c9000000002800be4c');
|
|
40
|
+
});
|
|
41
|
+
it('returns raw string when no URL pattern matches', () => {
|
|
42
|
+
expect(parseNoteId('69c131c9000000002800be4c')).toBe('69c131c9000000002800be4c');
|
|
43
|
+
});
|
|
44
|
+
it('trims whitespace', () => {
|
|
45
|
+
expect(parseNoteId(' 69c131c9000000002800be4c ')).toBe('69c131c9000000002800be4c');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
describe('buildNoteUrl', () => {
|
|
49
|
+
it('returns full URL as-is when given https URL', () => {
|
|
50
|
+
const url = 'https://www.xiaohongshu.com/search_result/abc123?xsec_token=tok';
|
|
51
|
+
expect(buildNoteUrl(url)).toBe(url);
|
|
52
|
+
});
|
|
53
|
+
it('constructs /explore/ URL for bare note ID', () => {
|
|
54
|
+
expect(buildNoteUrl('abc123')).toBe('https://www.xiaohongshu.com/explore/abc123');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
describe('xiaohongshu note', () => {
|
|
58
|
+
const command = getRegistry().get('xiaohongshu/note');
|
|
59
|
+
it('is registered', () => {
|
|
60
|
+
expect(command).toBeDefined();
|
|
61
|
+
expect(command.func).toBeTypeOf('function');
|
|
62
|
+
});
|
|
63
|
+
it('returns note content as field/value rows', async () => {
|
|
64
|
+
const page = createPageMock({
|
|
65
|
+
loginWall: false,
|
|
66
|
+
notFound: false,
|
|
67
|
+
title: '尚界Z7实车体验',
|
|
68
|
+
desc: '今天去看了实车,外观很帅',
|
|
69
|
+
author: '小红薯用户',
|
|
70
|
+
likes: '257',
|
|
71
|
+
collects: '98',
|
|
72
|
+
comments: '45',
|
|
73
|
+
tags: ['#尚界Z7', '#鸿蒙智行'],
|
|
74
|
+
});
|
|
75
|
+
const result = (await command.func(page, { 'note-id': '69c131c9000000002800be4c' }));
|
|
76
|
+
expect(page.goto.mock.calls[0][0]).toContain('/explore/69c131c9000000002800be4c');
|
|
77
|
+
expect(result).toEqual([
|
|
78
|
+
{ field: 'title', value: '尚界Z7实车体验' },
|
|
79
|
+
{ field: 'author', value: '小红薯用户' },
|
|
80
|
+
{ field: 'content', value: '今天去看了实车,外观很帅' },
|
|
81
|
+
{ field: 'likes', value: '257' },
|
|
82
|
+
{ field: 'collects', value: '98' },
|
|
83
|
+
{ field: 'comments', value: '45' },
|
|
84
|
+
{ field: 'tags', value: '#尚界Z7, #鸿蒙智行' },
|
|
85
|
+
]);
|
|
86
|
+
});
|
|
87
|
+
it('parses note ID from full /explore/ URL', async () => {
|
|
88
|
+
const page = createPageMock({
|
|
89
|
+
loginWall: false, notFound: false,
|
|
90
|
+
title: 'Test', desc: '', author: '', likes: '0', collects: '0', comments: '0', tags: [],
|
|
91
|
+
});
|
|
92
|
+
await command.func(page, {
|
|
93
|
+
'note-id': 'https://www.xiaohongshu.com/explore/69c131c9000000002800be4c?xsec_token=abc',
|
|
94
|
+
});
|
|
95
|
+
expect(page.goto.mock.calls[0][0]).toContain('/explore/69c131c9000000002800be4c');
|
|
96
|
+
});
|
|
97
|
+
it('preserves full search_result URL with xsec_token for navigation', async () => {
|
|
98
|
+
const page = createPageMock({
|
|
99
|
+
loginWall: false, notFound: false,
|
|
100
|
+
title: 'Test', desc: '', author: '', likes: '0', collects: '0', comments: '0', tags: [],
|
|
101
|
+
});
|
|
102
|
+
const fullUrl = 'https://www.xiaohongshu.com/search_result/69c131c9000000002800be4c?xsec_token=abc';
|
|
103
|
+
await command.func(page, { 'note-id': fullUrl });
|
|
104
|
+
// Should navigate to the full URL as-is, not strip the token
|
|
105
|
+
expect(page.goto.mock.calls[0][0]).toBe(fullUrl);
|
|
106
|
+
});
|
|
107
|
+
it('throws AuthRequiredError on login wall', async () => {
|
|
108
|
+
const page = createPageMock({ loginWall: true, notFound: false });
|
|
109
|
+
await expect(command.func(page, { 'note-id': 'abc123' })).rejects.toThrow('Note content requires login');
|
|
110
|
+
});
|
|
111
|
+
it('throws EmptyResultError when note is not found', async () => {
|
|
112
|
+
const page = createPageMock({ loginWall: false, notFound: true });
|
|
113
|
+
await expect(command.func(page, { 'note-id': 'abc123' })).rejects.toThrow('returned no data');
|
|
114
|
+
});
|
|
115
|
+
it('normalizes placeholder text to 0 for zero-count metrics', async () => {
|
|
116
|
+
const page = createPageMock({
|
|
117
|
+
loginWall: false, notFound: false,
|
|
118
|
+
title: 'New note', desc: 'Just posted', author: 'Author',
|
|
119
|
+
likes: '赞', collects: '收藏', comments: '评论', tags: [],
|
|
120
|
+
});
|
|
121
|
+
const result = (await command.func(page, { 'note-id': 'abc123' }));
|
|
122
|
+
expect(result.find((r) => r.field === 'likes').value).toBe('0');
|
|
123
|
+
expect(result.find((r) => r.field === 'collects').value).toBe('0');
|
|
124
|
+
expect(result.find((r) => r.field === 'comments').value).toBe('0');
|
|
125
|
+
});
|
|
126
|
+
it('omits tags row when no tags present', async () => {
|
|
127
|
+
const page = createPageMock({
|
|
128
|
+
loginWall: false, notFound: false,
|
|
129
|
+
title: 'No tags', desc: 'Content', author: 'Author',
|
|
130
|
+
likes: '1', collects: '2', comments: '3', tags: [],
|
|
131
|
+
});
|
|
132
|
+
const result = (await command.func(page, { 'note-id': 'abc123' }));
|
|
133
|
+
expect(result.find((r) => r.field === 'tags')).toBeUndefined();
|
|
134
|
+
expect(result).toHaveLength(6);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -40,6 +40,15 @@ cli({
|
|
|
40
40
|
const keyword = encodeURIComponent(kwargs.query);
|
|
41
41
|
await page.goto(`https://www.xiaohongshu.com/search_result?keyword=${keyword}&source=web_search_result_notes`);
|
|
42
42
|
await page.wait(3);
|
|
43
|
+
// Early login-wall detection: XHS may show a login gate instead of
|
|
44
|
+
// results. Check *before* autoScroll to avoid crashing on a page
|
|
45
|
+
// that has no meaningful content to scroll through.
|
|
46
|
+
const loginCheck = await page.evaluate(`
|
|
47
|
+
(() => /登录后查看搜索结果/.test(document.body?.innerText || ''))()
|
|
48
|
+
`);
|
|
49
|
+
if (loginCheck) {
|
|
50
|
+
throw new AuthRequiredError('www.xiaohongshu.com', 'Xiaohongshu search results are blocked behind a login wall');
|
|
51
|
+
}
|
|
43
52
|
// Scroll a couple of times to load more results
|
|
44
53
|
await page.autoScroll({ times: 2 });
|
|
45
54
|
const payload = await page.evaluate(`
|
|
@@ -36,12 +36,12 @@ describe('xiaohongshu search', () => {
|
|
|
36
36
|
const cmd = getRegistry().get('xiaohongshu/search');
|
|
37
37
|
expect(cmd?.func).toBeTypeOf('function');
|
|
38
38
|
const page = createPageMock([
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
results: [],
|
|
42
|
-
},
|
|
39
|
+
// First evaluate: early login-wall check (returns true)
|
|
40
|
+
true,
|
|
43
41
|
]);
|
|
44
42
|
await expect(cmd.func(page, { query: '特斯拉', limit: 5 })).rejects.toThrow('Xiaohongshu search results are blocked behind a login wall');
|
|
43
|
+
// autoScroll must NOT be called when a login wall is detected early
|
|
44
|
+
expect(page.autoScroll).not.toHaveBeenCalled();
|
|
45
45
|
});
|
|
46
46
|
it('returns ranked results with search_result url and author_url preserved', async () => {
|
|
47
47
|
const cmd = getRegistry().get('xiaohongshu/search');
|
|
@@ -49,6 +49,9 @@ describe('xiaohongshu search', () => {
|
|
|
49
49
|
const detailUrl = 'https://www.xiaohongshu.com/search_result/68e90be80000000004022e66?xsec_token=test-token&xsec_source=';
|
|
50
50
|
const authorUrl = 'https://www.xiaohongshu.com/user/profile/635a9c720000000018028b40?xsec_token=user-token&xsec_source=pc_search';
|
|
51
51
|
const page = createPageMock([
|
|
52
|
+
// First evaluate: early login-wall check (returns false → no wall)
|
|
53
|
+
false,
|
|
54
|
+
// Second evaluate: main DOM extraction
|
|
52
55
|
{
|
|
53
56
|
loginWall: false,
|
|
54
57
|
results: [
|
|
@@ -81,6 +84,9 @@ describe('xiaohongshu search', () => {
|
|
|
81
84
|
const cmd = getRegistry().get('xiaohongshu/search');
|
|
82
85
|
expect(cmd?.func).toBeTypeOf('function');
|
|
83
86
|
const page = createPageMock([
|
|
87
|
+
// First evaluate: early login-wall check (returns false → no wall)
|
|
88
|
+
false,
|
|
89
|
+
// Second evaluate: main DOM extraction
|
|
84
90
|
{
|
|
85
91
|
loginWall: false,
|
|
86
92
|
results: [
|
|
@@ -11,32 +11,60 @@ cli({
|
|
|
11
11
|
args: [
|
|
12
12
|
{ name: 'query', required: true, positional: true, help: 'Search query' },
|
|
13
13
|
{ name: 'limit', type: 'int', default: 20, help: 'Max results (max 50)' },
|
|
14
|
+
{ name: 'type', default: '', help: 'Filter type: shorts, video, channel, playlist' },
|
|
15
|
+
{ name: 'upload', default: '', help: 'Upload date: hour, today, week, month, year' },
|
|
16
|
+
{ name: 'sort', default: '', help: 'Sort by: relevance, date, views, rating' },
|
|
14
17
|
],
|
|
15
|
-
columns: ['rank', 'title', 'channel', 'views', 'duration', 'url'],
|
|
18
|
+
columns: ['rank', 'title', 'channel', 'views', 'duration', 'published', 'url'],
|
|
16
19
|
func: async (page, kwargs) => {
|
|
17
20
|
const limit = Math.min(kwargs.limit || 20, 50);
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
const query = encodeURIComponent(kwargs.query);
|
|
22
|
+
// Build search URL with filter params
|
|
23
|
+
// YouTube uses sp= parameter for filters — we use the URL approach for reliability
|
|
24
|
+
const spMap = {
|
|
25
|
+
// type filters
|
|
26
|
+
'shorts': 'EgIQCQ%3D%3D', // Shorts (type=9)
|
|
27
|
+
'video': 'EgIQAQ%3D%3D',
|
|
28
|
+
'channel': 'EgIQAg%3D%3D',
|
|
29
|
+
'playlist': 'EgIQAw%3D%3D',
|
|
30
|
+
// upload date filters (can be combined with type via URL)
|
|
31
|
+
'hour': 'EgIIAQ%3D%3D',
|
|
32
|
+
'today': 'EgIIAg%3D%3D',
|
|
33
|
+
'week': 'EgIIAw%3D%3D',
|
|
34
|
+
'month': 'EgIIBA%3D%3D',
|
|
35
|
+
'year': 'EgIIBQ%3D%3D',
|
|
36
|
+
};
|
|
37
|
+
const sortMap = {
|
|
38
|
+
'date': 'CAI%3D',
|
|
39
|
+
'views': 'CAM%3D',
|
|
40
|
+
'rating': 'CAE%3D',
|
|
41
|
+
};
|
|
42
|
+
// YouTube only supports a single sp= parameter — pick the most specific filter.
|
|
43
|
+
// Priority: type > upload > sort (type is the most common use case)
|
|
44
|
+
let sp = '';
|
|
45
|
+
if (kwargs.type && spMap[kwargs.type])
|
|
46
|
+
sp = spMap[kwargs.type];
|
|
47
|
+
else if (kwargs.upload && spMap[kwargs.upload])
|
|
48
|
+
sp = spMap[kwargs.upload];
|
|
49
|
+
else if (kwargs.sort && sortMap[kwargs.sort])
|
|
50
|
+
sp = sortMap[kwargs.sort];
|
|
51
|
+
let url = `https://www.youtube.com/results?search_query=${query}`;
|
|
52
|
+
if (sp)
|
|
53
|
+
url += `&sp=${sp}`;
|
|
54
|
+
await page.goto(url);
|
|
55
|
+
await page.wait(3);
|
|
20
56
|
const data = await page.evaluate(`
|
|
21
57
|
(async () => {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
const context = cfg.INNERTUBE_CONTEXT;
|
|
25
|
-
if (!apiKey || !context) return {error: 'YouTube config not found'};
|
|
58
|
+
const data = window.ytInitialData;
|
|
59
|
+
if (!data) return {error: 'YouTube data not found'};
|
|
26
60
|
|
|
27
|
-
const resp = await fetch('/youtubei/v1/search?key=' + apiKey + '&prettyPrint=false', {
|
|
28
|
-
method: 'POST', credentials: 'include',
|
|
29
|
-
headers: {'Content-Type': 'application/json'},
|
|
30
|
-
body: JSON.stringify({context, query: '${kwargs.query.replace(/'/g, "\\'")}'})
|
|
31
|
-
});
|
|
32
|
-
if (!resp.ok) return {error: 'HTTP ' + resp.status};
|
|
33
|
-
|
|
34
|
-
const data = await resp.json();
|
|
35
61
|
const contents = data.contents?.twoColumnSearchResultsRenderer?.primaryContents?.sectionListRenderer?.contents || [];
|
|
36
62
|
const videos = [];
|
|
37
63
|
for (const section of contents) {
|
|
38
|
-
|
|
39
|
-
|
|
64
|
+
const items = section.itemSectionRenderer?.contents || section.reelShelfRenderer?.items || [];
|
|
65
|
+
for (const item of items) {
|
|
66
|
+
if (videos.length >= ${limit}) break;
|
|
67
|
+
if (item.videoRenderer) {
|
|
40
68
|
const v = item.videoRenderer;
|
|
41
69
|
videos.push({
|
|
42
70
|
rank: videos.length + 1,
|
|
@@ -44,8 +72,20 @@ cli({
|
|
|
44
72
|
channel: v.ownerText?.runs?.[0]?.text || '',
|
|
45
73
|
views: v.viewCountText?.simpleText || v.shortViewCountText?.simpleText || '',
|
|
46
74
|
duration: v.lengthText?.simpleText || 'LIVE',
|
|
75
|
+
published: v.publishedTimeText?.simpleText || '',
|
|
47
76
|
url: 'https://www.youtube.com/watch?v=' + v.videoId
|
|
48
77
|
});
|
|
78
|
+
} else if (item.reelItemRenderer) {
|
|
79
|
+
const r = item.reelItemRenderer;
|
|
80
|
+
videos.push({
|
|
81
|
+
rank: videos.length + 1,
|
|
82
|
+
title: r.headline?.simpleText || '',
|
|
83
|
+
channel: r.navigationEndpoint?.reelWatchEndpoint?.overlay?.reelPlayerOverlayRenderer?.reelPlayerHeaderSupportedRenderers?.reelPlayerHeaderRenderer?.channelTitleText?.runs?.[0]?.text || '',
|
|
84
|
+
views: r.viewCountText?.simpleText || '',
|
|
85
|
+
duration: 'SHORT',
|
|
86
|
+
published: r.publishedTimeText?.simpleText || '',
|
|
87
|
+
url: 'https://www.youtube.com/shorts/' + r.videoId
|
|
88
|
+
});
|
|
49
89
|
}
|
|
50
90
|
}
|
|
51
91
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { cli, Strategy } from '../../registry.js';
|
|
2
|
-
import { AuthRequiredError } from '../../errors.js';
|
|
2
|
+
import { AuthRequiredError, CliError } from '../../errors.js';
|
|
3
3
|
cli({
|
|
4
4
|
site: 'zhihu',
|
|
5
5
|
name: 'question',
|
|
@@ -13,23 +13,25 @@ cli({
|
|
|
13
13
|
columns: ['rank', 'author', 'votes', 'content'],
|
|
14
14
|
func: async (page, kwargs) => {
|
|
15
15
|
const { id, limit = 5 } = kwargs;
|
|
16
|
+
const answerLimit = Number(limit);
|
|
16
17
|
const stripHtml = (html) => (html || '').replace(/<[^>]+>/g, '').replace(/ /g, ' ').replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&').trim();
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
throw new
|
|
32
|
-
|
|
18
|
+
// Only fetch answers here. The question detail endpoint is not used by the
|
|
19
|
+
// current CLI output and can fail independently, which would incorrectly
|
|
20
|
+
// turn a successful answers response into a login error.
|
|
21
|
+
const result = await page.evaluate(async ({ questionId, answerLimit }) => {
|
|
22
|
+
const aResp = await fetch(`https://www.zhihu.com/api/v4/questions/${questionId}/answers?limit=${answerLimit}&offset=0&sort_by=default&include=data[*].content,voteup_count,comment_count,author`, { credentials: 'include' });
|
|
23
|
+
if (!aResp.ok)
|
|
24
|
+
return { ok: false, status: aResp.status };
|
|
25
|
+
const a = await aResp.json();
|
|
26
|
+
return { ok: true, answers: Array.isArray(a?.data) ? a.data : [] };
|
|
27
|
+
}, { questionId: String(id), answerLimit });
|
|
28
|
+
if (!result?.ok) {
|
|
29
|
+
if (result?.status === 401 || result?.status === 403) {
|
|
30
|
+
throw new AuthRequiredError('www.zhihu.com', 'Failed to fetch question data from Zhihu');
|
|
31
|
+
}
|
|
32
|
+
throw new CliError('FETCH_ERROR', `Zhihu question answers request failed with HTTP ${result?.status ?? 'unknown'}`, 'Try again later or rerun with -v for more detail');
|
|
33
|
+
}
|
|
34
|
+
const answers = result.answers.slice(0, answerLimit).map((a, i) => ({
|
|
33
35
|
rank: i + 1,
|
|
34
36
|
author: a.author?.name ?? 'anonymous',
|
|
35
37
|
votes: a.voteup_count ?? 0,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './question.js';
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { getRegistry } from '../../registry.js';
|
|
3
|
+
import { AuthRequiredError } from '../../errors.js';
|
|
4
|
+
import './question.js';
|
|
5
|
+
describe('zhihu question', () => {
|
|
6
|
+
it('returns answers even when the unused question detail request fails', async () => {
|
|
7
|
+
const cmd = getRegistry().get('zhihu/question');
|
|
8
|
+
expect(cmd?.func).toBeTypeOf('function');
|
|
9
|
+
const evaluate = vi.fn().mockImplementation(async (_fn, args) => {
|
|
10
|
+
expect(args).toEqual({ questionId: '2021881398772981878', answerLimit: 3 });
|
|
11
|
+
return {
|
|
12
|
+
ok: true,
|
|
13
|
+
answers: [
|
|
14
|
+
{
|
|
15
|
+
author: { name: 'alice' },
|
|
16
|
+
voteup_count: 12,
|
|
17
|
+
content: '<p>Hello <b>Zhihu</b></p>',
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
const page = {
|
|
23
|
+
evaluate,
|
|
24
|
+
};
|
|
25
|
+
await expect(cmd.func(page, { id: '2021881398772981878', limit: 3 })).resolves.toEqual([
|
|
26
|
+
{
|
|
27
|
+
rank: 1,
|
|
28
|
+
author: 'alice',
|
|
29
|
+
votes: 12,
|
|
30
|
+
content: 'Hello Zhihu',
|
|
31
|
+
},
|
|
32
|
+
]);
|
|
33
|
+
expect(evaluate).toHaveBeenCalledTimes(1);
|
|
34
|
+
});
|
|
35
|
+
it('maps auth-like answer failures to AuthRequiredError', async () => {
|
|
36
|
+
const cmd = getRegistry().get('zhihu/question');
|
|
37
|
+
expect(cmd?.func).toBeTypeOf('function');
|
|
38
|
+
const page = {
|
|
39
|
+
evaluate: vi.fn().mockResolvedValue({ ok: false, status: 403 }),
|
|
40
|
+
};
|
|
41
|
+
await expect(cmd.func(page, { id: '2021881398772981878', limit: 3 })).rejects.toBeInstanceOf(AuthRequiredError);
|
|
42
|
+
});
|
|
43
|
+
it('preserves non-auth fetch failures as CliError instead of login errors', async () => {
|
|
44
|
+
const cmd = getRegistry().get('zhihu/question');
|
|
45
|
+
expect(cmd?.func).toBeTypeOf('function');
|
|
46
|
+
const page = {
|
|
47
|
+
evaluate: vi.fn().mockResolvedValue({ ok: false, status: 500 }),
|
|
48
|
+
};
|
|
49
|
+
await expect(cmd.func(page, { id: '2021881398772981878', limit: 3 })).rejects.toMatchObject({
|
|
50
|
+
code: 'FETCH_ERROR',
|
|
51
|
+
message: 'Zhihu question answers request failed with HTTP 500',
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
});
|
package/dist/commanderAdapter.js
CHANGED
|
@@ -38,6 +38,8 @@ export function registerCommandToProgram(siteCmd, cmd) {
|
|
|
38
38
|
return;
|
|
39
39
|
const deprecatedSuffix = cmd.deprecated ? ' [deprecated]' : '';
|
|
40
40
|
const subCmd = siteCmd.command(cmd.name).description(`${cmd.description}${deprecatedSuffix}`);
|
|
41
|
+
if (cmd.aliases?.length)
|
|
42
|
+
subCmd.aliases(cmd.aliases);
|
|
41
43
|
// Register positional args first, then named options
|
|
42
44
|
const positionalArgs = [];
|
|
43
45
|
for (const arg of cmd.args) {
|
|
@@ -91,6 +93,9 @@ export function registerCommandToProgram(siteCmd, cmd) {
|
|
|
91
93
|
console.error(chalk.yellow(`Deprecated: ${message}${replacement}`));
|
|
92
94
|
}
|
|
93
95
|
const result = await executeCommand(cmd, kwargs, verbose);
|
|
96
|
+
if (result === null || result === undefined) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
94
99
|
if (verbose && (!result || (Array.isArray(result) && result.length === 0))) {
|
|
95
100
|
console.error(chalk.yellow('[Verbose] Warning: Command returned an empty result.'));
|
|
96
101
|
}
|
|
@@ -274,7 +279,11 @@ async function renderError(err, cmdName, verbose) {
|
|
|
274
279
|
* Register all commands from the registry onto a Commander program.
|
|
275
280
|
*/
|
|
276
281
|
export function registerAllCommands(program, siteGroups) {
|
|
282
|
+
const seen = new Set();
|
|
277
283
|
for (const [, cmd] of getRegistry()) {
|
|
284
|
+
if (seen.has(cmd))
|
|
285
|
+
continue;
|
|
286
|
+
seen.add(cmd);
|
|
278
287
|
let siteCmd = siteGroups.get(cmd.site);
|
|
279
288
|
if (!siteCmd) {
|
|
280
289
|
siteCmd = program.command(cmd.site).description(`${cmd.site} commands`);
|
|
@@ -99,3 +99,28 @@ describe('commanderAdapter boolean alias support', () => {
|
|
|
99
99
|
expect(kwargs.undo).toBe(false);
|
|
100
100
|
});
|
|
101
101
|
});
|
|
102
|
+
describe('commanderAdapter command aliases', () => {
|
|
103
|
+
const cmd = {
|
|
104
|
+
site: 'notebooklm',
|
|
105
|
+
name: 'get',
|
|
106
|
+
aliases: ['metadata'],
|
|
107
|
+
description: 'Get notebook metadata',
|
|
108
|
+
browser: false,
|
|
109
|
+
args: [],
|
|
110
|
+
func: vi.fn(),
|
|
111
|
+
};
|
|
112
|
+
beforeEach(() => {
|
|
113
|
+
mockExecuteCommand.mockReset();
|
|
114
|
+
mockExecuteCommand.mockResolvedValue([]);
|
|
115
|
+
mockRenderOutput.mockReset();
|
|
116
|
+
delete process.env.OPENCLI_VERBOSE;
|
|
117
|
+
process.exitCode = undefined;
|
|
118
|
+
});
|
|
119
|
+
it('registers aliases with Commander so compatibility names execute the same command', async () => {
|
|
120
|
+
const program = new Command();
|
|
121
|
+
const siteCmd = program.command('notebooklm');
|
|
122
|
+
registerCommandToProgram(siteCmd, cmd);
|
|
123
|
+
await program.parseAsync(['node', 'opencli', 'notebooklm', 'metadata']);
|
|
124
|
+
expect(mockExecuteCommand).toHaveBeenCalledWith(cmd, {}, false);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI commands for daemon lifecycle management:
|
|
3
|
+
* opencli daemon status — show daemon state
|
|
4
|
+
* opencli daemon stop — graceful shutdown
|
|
5
|
+
* opencli daemon restart — stop + respawn
|
|
6
|
+
*/
|
|
7
|
+
export declare function daemonStatus(): Promise<void>;
|
|
8
|
+
export declare function daemonStop(): Promise<void>;
|
|
9
|
+
export declare function daemonRestart(): Promise<void>;
|