@jackwener/opencli 1.4.1 → 1.5.1
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/.github/workflows/build-extension.yml +2 -6
- package/.github/workflows/ci.yml +21 -1
- package/README.md +35 -6
- package/README.zh-CN.md +12 -5
- package/SKILL.md +2 -0
- package/dist/browser/cdp.d.ts +2 -1
- package/dist/browser/cdp.js +5 -0
- package/dist/browser/discover.d.ts +4 -1
- package/dist/browser/discover.js +6 -2
- package/dist/browser/errors.d.ts +2 -2
- package/dist/browser/errors.js +4 -12
- package/dist/browser/mcp.d.ts +2 -1
- package/dist/browser/page.d.ts +3 -0
- package/dist/browser/page.js +24 -1
- package/dist/build-manifest.d.ts +2 -0
- package/dist/build-manifest.js +39 -14
- package/dist/build-manifest.test.js +21 -0
- package/dist/capabilityRouting.d.ts +2 -0
- package/dist/capabilityRouting.js +2 -1
- package/dist/cli-manifest.json +1567 -108
- package/dist/cli.js +68 -6
- package/dist/clis/36kr/article.d.ts +1 -0
- package/dist/clis/36kr/article.js +62 -0
- package/dist/clis/36kr/hot.d.ts +3 -0
- package/dist/clis/36kr/hot.js +80 -0
- package/dist/clis/36kr/hot.test.d.ts +1 -0
- package/dist/clis/36kr/hot.test.js +15 -0
- package/dist/clis/36kr/news.d.ts +1 -0
- package/dist/clis/36kr/news.js +51 -0
- package/dist/clis/36kr/news.test.d.ts +1 -0
- package/dist/clis/36kr/news.test.js +85 -0
- package/dist/clis/36kr/search.d.ts +1 -0
- package/dist/clis/36kr/search.js +72 -0
- package/dist/clis/bilibili/comments.d.ts +5 -0
- package/dist/clis/bilibili/comments.js +40 -0
- package/dist/clis/bilibili/comments.test.d.ts +1 -0
- package/dist/clis/bilibili/comments.test.js +82 -0
- package/dist/clis/bluesky/feeds.yaml +29 -0
- package/dist/clis/bluesky/followers.yaml +33 -0
- package/dist/clis/bluesky/following.yaml +33 -0
- package/dist/clis/bluesky/profile.yaml +27 -0
- package/dist/clis/bluesky/search.yaml +34 -0
- package/dist/clis/bluesky/starter-packs.yaml +34 -0
- package/dist/clis/bluesky/thread.yaml +32 -0
- package/dist/clis/bluesky/trending.yaml +27 -0
- package/dist/clis/bluesky/user.yaml +34 -0
- package/dist/clis/chatgpt/ask.js +29 -14
- package/dist/clis/chatgpt/ax.d.ts +6 -0
- package/dist/clis/chatgpt/ax.js +172 -1
- package/dist/clis/chatgpt/model.d.ts +1 -0
- package/dist/clis/chatgpt/model.js +24 -0
- package/dist/clis/chatgpt/send.js +12 -3
- package/dist/clis/douban/download.d.ts +1 -0
- package/dist/clis/douban/download.js +67 -0
- package/dist/clis/douban/download.test.d.ts +1 -0
- package/dist/clis/douban/download.test.js +170 -0
- package/dist/clis/douban/photos.d.ts +1 -0
- package/dist/clis/douban/photos.js +34 -0
- package/dist/clis/douban/utils.d.ts +25 -0
- package/dist/clis/douban/utils.js +190 -1
- package/dist/clis/douban/utils.test.d.ts +1 -0
- package/dist/clis/douban/utils.test.js +64 -0
- package/dist/clis/imdb/person.d.ts +1 -0
- package/dist/clis/imdb/person.js +203 -0
- package/dist/clis/imdb/reviews.d.ts +1 -0
- package/dist/clis/imdb/reviews.js +88 -0
- package/dist/clis/imdb/search.d.ts +1 -0
- package/dist/clis/imdb/search.js +161 -0
- package/dist/clis/imdb/title.d.ts +1 -0
- package/dist/clis/imdb/title.js +93 -0
- package/dist/clis/imdb/top.d.ts +1 -0
- package/dist/clis/imdb/top.js +53 -0
- package/dist/clis/imdb/trending.d.ts +1 -0
- package/dist/clis/imdb/trending.js +52 -0
- package/dist/clis/imdb/utils.d.ts +46 -0
- package/dist/clis/imdb/utils.js +285 -0
- package/dist/clis/imdb/utils.test.d.ts +1 -0
- package/dist/clis/imdb/utils.test.js +88 -0
- package/dist/clis/jd/item.d.ts +4 -0
- package/dist/clis/jd/item.js +16 -15
- package/dist/clis/jd/item.test.js +16 -1
- package/dist/clis/linux-do/categories.yaml +38 -9
- package/dist/clis/linux-do/category.d.ts +1 -0
- package/dist/clis/linux-do/category.js +36 -0
- package/dist/clis/linux-do/feed.d.ts +45 -0
- package/dist/clis/linux-do/feed.js +397 -0
- package/dist/clis/linux-do/feed.test.d.ts +1 -0
- package/dist/clis/linux-do/feed.test.js +118 -0
- package/dist/clis/linux-do/hot.d.ts +1 -0
- package/dist/clis/linux-do/hot.js +25 -0
- package/dist/clis/linux-do/latest.d.ts +1 -0
- package/dist/clis/linux-do/latest.js +18 -0
- package/dist/clis/linux-do/tags.yaml +41 -0
- package/dist/clis/linux-do/topic.yaml +41 -3
- package/dist/clis/linux-do/user-posts.yaml +67 -0
- package/dist/clis/linux-do/user-topics.yaml +54 -0
- package/dist/clis/paperreview/commands.test.d.ts +3 -0
- package/dist/clis/paperreview/commands.test.js +243 -0
- package/dist/clis/paperreview/feedback.d.ts +1 -0
- package/dist/clis/paperreview/feedback.js +52 -0
- package/dist/clis/paperreview/review.d.ts +1 -0
- package/dist/clis/paperreview/review.js +37 -0
- package/dist/clis/paperreview/submit.d.ts +1 -0
- package/dist/clis/paperreview/submit.js +85 -0
- package/dist/clis/paperreview/utils.d.ts +46 -0
- package/dist/clis/paperreview/utils.js +197 -0
- package/dist/clis/paperreview/utils.test.d.ts +1 -0
- package/dist/clis/paperreview/utils.test.js +49 -0
- package/dist/clis/producthunt/browse.d.ts +1 -0
- package/dist/clis/producthunt/browse.js +99 -0
- package/dist/clis/producthunt/hot.d.ts +1 -0
- package/dist/clis/producthunt/hot.js +110 -0
- package/dist/clis/producthunt/posts.d.ts +1 -0
- package/dist/clis/producthunt/posts.js +28 -0
- package/dist/clis/producthunt/today.d.ts +1 -0
- package/dist/clis/producthunt/today.js +35 -0
- package/dist/clis/producthunt/utils.d.ts +29 -0
- package/dist/clis/producthunt/utils.js +99 -0
- package/dist/clis/producthunt/utils.test.d.ts +1 -0
- package/dist/clis/producthunt/utils.test.js +64 -0
- package/dist/clis/twitter/article.js +4 -28
- package/dist/clis/twitter/likes.d.ts +24 -0
- package/dist/clis/twitter/likes.js +217 -0
- package/dist/clis/twitter/likes.test.d.ts +1 -0
- package/dist/clis/twitter/likes.test.js +85 -0
- package/dist/clis/twitter/profile.js +4 -28
- package/dist/clis/twitter/search.js +2 -1
- package/dist/clis/twitter/search.test.js +2 -0
- package/dist/clis/twitter/shared.d.ts +6 -0
- package/dist/clis/twitter/shared.js +35 -0
- package/dist/clis/twitter/timeline.js +2 -13
- package/dist/clis/twitter/trending.js +29 -61
- package/dist/clis/v2ex/hot.yaml +17 -3
- package/dist/clis/weixin/download.d.ts +17 -0
- package/dist/clis/weixin/download.js +88 -20
- package/dist/clis/weread/book.js +2 -2
- package/dist/clis/weread/commands.test.d.ts +3 -0
- package/dist/clis/weread/commands.test.js +43 -0
- package/dist/clis/weread/highlights.js +2 -2
- package/dist/clis/weread/notebooks.js +2 -2
- package/dist/clis/weread/notes.js +3 -3
- package/dist/clis/weread/shelf.js +2 -2
- package/dist/clis/weread/utils.d.ts +4 -4
- package/dist/clis/weread/utils.js +32 -14
- package/dist/clis/weread/utils.test.js +1 -28
- package/dist/clis/xiaohongshu/comments.d.ts +5 -0
- package/dist/clis/xiaohongshu/comments.js +74 -0
- package/dist/clis/xiaohongshu/comments.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/comments.test.js +79 -0
- package/dist/clis/xiaohongshu/publish.js +179 -47
- package/dist/clis/xiaohongshu/publish.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/publish.test.js +131 -0
- package/dist/clis/xiaohongshu/search.d.ts +8 -1
- package/dist/clis/xiaohongshu/search.js +20 -1
- package/dist/clis/xiaohongshu/search.test.d.ts +1 -1
- package/dist/clis/xiaohongshu/search.test.js +32 -1
- package/dist/commanderAdapter.d.ts +1 -0
- package/dist/commanderAdapter.js +176 -29
- package/dist/commanderAdapter.test.d.ts +1 -0
- package/dist/commanderAdapter.test.js +62 -0
- package/dist/daemon.js +17 -1
- package/dist/discovery.js +48 -42
- package/dist/doctor.d.ts +2 -2
- package/dist/doctor.js +11 -4
- package/dist/download/index.js +63 -51
- package/dist/download/index.test.js +17 -4
- package/dist/engine.test.js +42 -0
- package/dist/errors.d.ts +4 -2
- package/dist/errors.js +17 -34
- package/dist/execution.d.ts +1 -3
- package/dist/execution.js +66 -8
- package/dist/execution.test.d.ts +1 -0
- package/dist/execution.test.js +40 -0
- package/dist/external.js +6 -1
- package/dist/hooks.js +2 -0
- package/dist/main.js +6 -0
- package/dist/output.js +5 -1
- package/dist/pipeline/executor.js +3 -4
- package/dist/plugin-manifest.d.ts +70 -0
- package/dist/plugin-manifest.js +160 -0
- package/dist/plugin-manifest.test.d.ts +4 -0
- package/dist/plugin-manifest.test.js +179 -0
- package/dist/plugin-scaffold.d.ts +28 -0
- package/dist/plugin-scaffold.js +142 -0
- package/dist/plugin-scaffold.test.d.ts +4 -0
- package/dist/plugin-scaffold.test.js +83 -0
- package/dist/plugin.d.ts +82 -11
- package/dist/plugin.js +870 -84
- package/dist/plugin.test.js +1032 -17
- package/dist/registry.d.ts +4 -0
- package/dist/registry.js +2 -0
- package/dist/runtime-detect.d.ts +21 -0
- package/dist/runtime-detect.js +32 -0
- package/dist/runtime-detect.test.d.ts +1 -0
- package/dist/runtime-detect.test.js +27 -0
- package/dist/runtime.d.ts +1 -0
- package/dist/runtime.js +2 -2
- package/dist/serialization.d.ts +2 -0
- package/dist/serialization.js +6 -0
- package/dist/types.d.ts +3 -0
- package/dist/update-check.d.ts +22 -0
- package/dist/update-check.js +112 -0
- package/dist/weixin-download.test.d.ts +1 -0
- package/dist/weixin-download.test.js +30 -0
- package/dist/weread-private-api-regression.test.d.ts +1 -0
- package/dist/weread-private-api-regression.test.js +122 -0
- package/dist/yaml-schema.d.ts +3 -0
- package/dist/yaml-schema.js +18 -1
- package/docs/.vitepress/config.mts +4 -0
- package/docs/adapters/browser/36kr.md +47 -0
- package/docs/adapters/browser/bluesky.md +53 -0
- package/docs/adapters/browser/douban.md +14 -0
- package/docs/adapters/browser/imdb.md +47 -0
- package/docs/adapters/browser/jd.md +2 -2
- package/docs/adapters/browser/linux-do.md +181 -20
- package/docs/adapters/browser/paperreview.md +43 -0
- package/docs/adapters/browser/producthunt.md +49 -0
- package/docs/adapters/desktop/chatgpt.md +5 -0
- package/docs/adapters/index.md +6 -2
- package/docs/advanced/download.md +4 -0
- package/docs/advanced/rate-limiter-plugin.md +99 -0
- package/docs/guide/electron-app-cli.md +200 -0
- package/docs/guide/getting-started.md +1 -0
- package/docs/guide/plugins.md +97 -0
- package/docs/zh/guide/electron-app-cli.md +188 -0
- package/docs/zh/guide/getting-started.md +1 -0
- package/docs/zh/guide/plugins.md +65 -0
- package/extension/package.json +1 -0
- package/extension/scripts/package-release.mjs +179 -0
- package/extension/src/background.ts +2 -0
- package/package.json +4 -1
- package/scripts/postinstall.js +10 -0
- package/src/browser/cdp.ts +8 -1
- package/src/browser/discover.ts +8 -3
- package/src/browser/errors.ts +13 -14
- package/src/browser/mcp.ts +2 -1
- package/src/browser/page.ts +24 -1
- package/src/build-manifest.test.ts +23 -0
- package/src/build-manifest.ts +40 -15
- package/src/capabilityRouting.ts +2 -1
- package/src/cli.ts +69 -6
- package/src/clis/36kr/article.ts +69 -0
- package/src/clis/36kr/hot.test.ts +19 -0
- package/src/clis/36kr/hot.ts +100 -0
- package/src/clis/36kr/news.test.ts +90 -0
- package/src/clis/36kr/news.ts +54 -0
- package/src/clis/36kr/search.ts +78 -0
- package/src/clis/bilibili/comments.test.ts +102 -0
- package/src/clis/bilibili/comments.ts +44 -0
- package/src/clis/bluesky/feeds.yaml +29 -0
- package/src/clis/bluesky/followers.yaml +33 -0
- package/src/clis/bluesky/following.yaml +33 -0
- package/src/clis/bluesky/profile.yaml +27 -0
- package/src/clis/bluesky/search.yaml +34 -0
- package/src/clis/bluesky/starter-packs.yaml +34 -0
- package/src/clis/bluesky/thread.yaml +32 -0
- package/src/clis/bluesky/trending.yaml +27 -0
- package/src/clis/bluesky/user.yaml +34 -0
- package/src/clis/chatgpt/ask.ts +28 -14
- package/src/clis/chatgpt/ax.ts +180 -1
- package/src/clis/chatgpt/model.ts +27 -0
- package/src/clis/chatgpt/send.ts +16 -6
- package/src/clis/douban/download.test.ts +196 -0
- package/src/clis/douban/download.ts +78 -0
- package/src/clis/douban/photos.ts +36 -0
- package/src/clis/douban/utils.test.ts +97 -0
- package/src/clis/douban/utils.ts +232 -1
- package/src/clis/imdb/person.ts +232 -0
- package/src/clis/imdb/reviews.ts +111 -0
- package/src/clis/imdb/search.ts +179 -0
- package/src/clis/imdb/title.ts +121 -0
- package/src/clis/imdb/top.ts +67 -0
- package/src/clis/imdb/trending.ts +66 -0
- package/src/clis/imdb/utils.test.ts +117 -0
- package/src/clis/imdb/utils.ts +305 -0
- package/src/clis/jd/item.test.ts +18 -1
- package/src/clis/jd/item.ts +18 -15
- package/src/clis/linux-do/categories.yaml +38 -9
- package/src/clis/linux-do/category.ts +37 -0
- package/src/clis/linux-do/feed.test.ts +132 -0
- package/src/clis/linux-do/feed.ts +501 -0
- package/src/clis/linux-do/hot.ts +26 -0
- package/src/clis/linux-do/latest.ts +19 -0
- package/src/clis/linux-do/tags.yaml +41 -0
- package/src/clis/linux-do/topic.yaml +41 -3
- package/src/clis/linux-do/user-posts.yaml +67 -0
- package/src/clis/linux-do/user-topics.yaml +54 -0
- package/src/clis/paperreview/commands.test.ts +283 -0
- package/src/clis/paperreview/feedback.ts +64 -0
- package/src/clis/paperreview/review.ts +47 -0
- package/src/clis/paperreview/submit.ts +119 -0
- package/src/clis/paperreview/utils.test.ts +68 -0
- package/src/clis/paperreview/utils.ts +276 -0
- package/src/clis/producthunt/browse.ts +109 -0
- package/src/clis/producthunt/hot.ts +127 -0
- package/src/clis/producthunt/posts.ts +29 -0
- package/src/clis/producthunt/today.ts +37 -0
- package/src/clis/producthunt/utils.test.ts +72 -0
- package/src/clis/producthunt/utils.ts +122 -0
- package/src/clis/twitter/article.ts +5 -28
- package/src/clis/twitter/likes.test.ts +91 -0
- package/src/clis/twitter/likes.ts +256 -0
- package/src/clis/twitter/profile.ts +5 -28
- package/src/clis/twitter/search.test.ts +2 -0
- package/src/clis/twitter/search.ts +3 -1
- package/src/clis/twitter/shared.ts +45 -0
- package/src/clis/twitter/timeline.ts +2 -13
- package/src/clis/twitter/trending.ts +29 -77
- package/src/clis/v2ex/hot.yaml +17 -3
- package/src/clis/weixin/download.ts +114 -20
- package/src/clis/weread/book.ts +2 -2
- package/src/clis/weread/commands.test.ts +57 -0
- package/src/clis/weread/highlights.ts +2 -2
- package/src/clis/weread/notebooks.ts +2 -2
- package/src/clis/weread/notes.ts +3 -3
- package/src/clis/weread/shelf.ts +2 -2
- package/src/clis/weread/utils.test.ts +1 -32
- package/src/clis/weread/utils.ts +41 -16
- package/src/clis/xiaohongshu/comments.test.ts +96 -0
- package/src/clis/xiaohongshu/comments.ts +81 -0
- package/src/clis/xiaohongshu/publish.test.ts +151 -0
- package/src/clis/xiaohongshu/publish.ts +206 -54
- package/src/clis/xiaohongshu/search.test.ts +39 -1
- package/src/clis/xiaohongshu/search.ts +19 -1
- package/src/commanderAdapter.test.ts +78 -0
- package/src/commanderAdapter.ts +188 -24
- package/src/daemon.ts +19 -1
- package/src/discovery.ts +49 -48
- package/src/doctor.ts +15 -5
- package/src/download/index.test.ts +14 -4
- package/src/download/index.ts +67 -55
- package/src/engine.test.ts +38 -0
- package/src/errors.ts +26 -63
- package/src/execution.test.ts +47 -0
- package/src/execution.ts +67 -9
- package/src/external.ts +6 -1
- package/src/hooks.ts +1 -0
- package/src/main.ts +7 -0
- package/src/output.ts +3 -1
- package/src/pipeline/executor.ts +4 -6
- package/src/plugin-manifest.test.ts +223 -0
- package/src/plugin-manifest.ts +206 -0
- package/src/plugin-scaffold.test.ts +98 -0
- package/src/plugin-scaffold.ts +170 -0
- package/src/plugin.test.ts +1104 -17
- package/src/plugin.ts +1101 -86
- package/src/registry.ts +6 -1
- package/src/runtime-detect.test.ts +30 -0
- package/src/runtime-detect.ts +36 -0
- package/src/runtime.ts +3 -3
- package/src/serialization.ts +4 -0
- package/src/types.ts +3 -0
- package/src/update-check.ts +114 -0
- package/src/weixin-download.test.ts +64 -0
- package/src/weread-private-api-regression.test.ts +150 -0
- package/src/yaml-schema.ts +20 -0
- package/tests/e2e/browser-auth.test.ts +13 -9
- package/tests/e2e/browser-public-extended.test.ts +1 -1
- package/tests/e2e/browser-public.test.ts +62 -4
- package/tests/e2e/helpers.ts +2 -1
- package/tests/e2e/public-commands.test.ts +37 -3
- package/tests/smoke/api-health.test.ts +1 -1
- package/vitest.config.ts +10 -0
- package/dist/clis/linux-do/category.yaml +0 -51
- package/dist/clis/linux-do/hot.yaml +0 -50
- package/dist/clis/linux-do/latest.yaml +0 -40
- package/src/clis/linux-do/category.yaml +0 -51
- package/src/clis/linux-do/hot.yaml +0 -50
- package/src/clis/linux-do/latest.yaml +0 -40
|
@@ -27,6 +27,22 @@ const MAX_IMAGES = 9;
|
|
|
27
27
|
const MAX_TITLE_LEN = 20;
|
|
28
28
|
const UPLOAD_SETTLE_MS = 3000;
|
|
29
29
|
|
|
30
|
+
/** Selectors for the title field, ordered by priority (new UI first). */
|
|
31
|
+
const TITLE_SELECTORS = [
|
|
32
|
+
// New creator center (2026-03) uses contenteditable for the title field.
|
|
33
|
+
// Placeholder observed: "填写标题会有更多赞哦"
|
|
34
|
+
'[contenteditable="true"][placeholder*="标题"]',
|
|
35
|
+
'[contenteditable="true"][placeholder*="赞"]',
|
|
36
|
+
'[contenteditable="true"][class*="title"]',
|
|
37
|
+
'input[maxlength="20"]',
|
|
38
|
+
'input[class*="title"]',
|
|
39
|
+
'input[placeholder*="标题"]',
|
|
40
|
+
'input[placeholder*="title" i]',
|
|
41
|
+
'.title-input input',
|
|
42
|
+
'.note-title input',
|
|
43
|
+
'input[maxlength]',
|
|
44
|
+
];
|
|
45
|
+
|
|
30
46
|
type ImagePayload = { name: string; mimeType: string; base64: string };
|
|
31
47
|
|
|
32
48
|
/**
|
|
@@ -63,14 +79,22 @@ async function injectImages(page: IPage, images: ImagePayload[]): Promise<{ ok:
|
|
|
63
79
|
(async () => {
|
|
64
80
|
const images = ${payload};
|
|
65
81
|
|
|
66
|
-
//
|
|
82
|
+
// Only use image-capable file inputs. Do not fall back to a generic uploader,
|
|
83
|
+
// otherwise we can accidentally feed images into the video upload flow.
|
|
67
84
|
const inputs = Array.from(document.querySelectorAll('input[type="file"]'));
|
|
68
85
|
const input = inputs.find(el => {
|
|
69
86
|
const accept = el.getAttribute('accept') || '';
|
|
70
|
-
return
|
|
71
|
-
|
|
87
|
+
return (
|
|
88
|
+
accept.includes('image') ||
|
|
89
|
+
accept.includes('.jpg') ||
|
|
90
|
+
accept.includes('.jpeg') ||
|
|
91
|
+
accept.includes('.png') ||
|
|
92
|
+
accept.includes('.gif') ||
|
|
93
|
+
accept.includes('.webp')
|
|
94
|
+
);
|
|
95
|
+
});
|
|
72
96
|
|
|
73
|
-
if (!input) return { ok: false, count: 0, error: 'No file input found on page' };
|
|
97
|
+
if (!input) return { ok: false, count: 0, error: 'No image file input found on page' };
|
|
74
98
|
|
|
75
99
|
const dt = new DataTransfer();
|
|
76
100
|
for (const img of images) {
|
|
@@ -151,6 +175,139 @@ async function fillField(page: IPage, selectors: string[], text: string, fieldNa
|
|
|
151
175
|
}
|
|
152
176
|
}
|
|
153
177
|
|
|
178
|
+
async function selectImageTextTab(
|
|
179
|
+
page: IPage,
|
|
180
|
+
): Promise<{ ok: boolean; target?: string; text?: string; visibleTexts?: string[] }> {
|
|
181
|
+
const result = await page.evaluate(`
|
|
182
|
+
() => {
|
|
183
|
+
const isVisible = (el) => {
|
|
184
|
+
if (!el || el.offsetParent === null) return false;
|
|
185
|
+
const rect = el.getBoundingClientRect();
|
|
186
|
+
return rect.width > 0 && rect.height > 0;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const normalize = (value) => (value || '').replace(/\\s+/g, ' ').trim();
|
|
190
|
+
const selector = 'button, [role="tab"], [role="button"], a, label, div, span, li';
|
|
191
|
+
const nodes = Array.from(document.querySelectorAll(selector));
|
|
192
|
+
const targets = ['上传图文', '图文', '图片'];
|
|
193
|
+
|
|
194
|
+
for (const target of targets) {
|
|
195
|
+
for (const node of nodes) {
|
|
196
|
+
if (!isVisible(node)) continue;
|
|
197
|
+
const text = normalize(node.innerText || node.textContent || '');
|
|
198
|
+
if (!text || text.includes('视频')) continue;
|
|
199
|
+
if (text === target || text.startsWith(target) || text.includes(target)) {
|
|
200
|
+
const clickable = node.closest('button, [role="tab"], [role="button"], a, label') || node;
|
|
201
|
+
clickable.click();
|
|
202
|
+
return { ok: true, target, text };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const visibleTexts = [];
|
|
208
|
+
for (const node of nodes) {
|
|
209
|
+
if (!isVisible(node)) continue;
|
|
210
|
+
const text = normalize(node.innerText || node.textContent || '');
|
|
211
|
+
if (!text || text.length > 20) continue;
|
|
212
|
+
visibleTexts.push(text);
|
|
213
|
+
if (visibleTexts.length >= 20) break;
|
|
214
|
+
}
|
|
215
|
+
return { ok: false, visibleTexts };
|
|
216
|
+
}
|
|
217
|
+
`);
|
|
218
|
+
if (result?.ok) {
|
|
219
|
+
await page.wait({ time: 1 });
|
|
220
|
+
}
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
type PublishSurfaceState = 'video_surface' | 'image_surface' | 'editor_ready';
|
|
225
|
+
|
|
226
|
+
type PublishSurfaceInspection = {
|
|
227
|
+
state: PublishSurfaceState;
|
|
228
|
+
hasTitleInput: boolean;
|
|
229
|
+
hasImageInput: boolean;
|
|
230
|
+
hasVideoSurface: boolean;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
async function inspectPublishSurfaceState(page: IPage): Promise<PublishSurfaceInspection> {
|
|
234
|
+
return page.evaluate(`
|
|
235
|
+
() => {
|
|
236
|
+
const text = (document.body?.innerText || '').replace(/\s+/g, ' ').trim();
|
|
237
|
+
const hasTitleInput = !!Array.from(document.querySelectorAll('input, textarea')).find((el) => {
|
|
238
|
+
if (!el || el.offsetParent === null) return false;
|
|
239
|
+
const placeholder = (el.getAttribute('placeholder') || '').trim();
|
|
240
|
+
const cls = el.className ? String(el.className) : '';
|
|
241
|
+
const maxLength = Number(el.getAttribute('maxlength') || 0);
|
|
242
|
+
return (
|
|
243
|
+
placeholder.includes('标题') ||
|
|
244
|
+
/title/i.test(placeholder) ||
|
|
245
|
+
/title/i.test(cls) ||
|
|
246
|
+
maxLength === 20
|
|
247
|
+
);
|
|
248
|
+
});
|
|
249
|
+
const hasImageInput = !!Array.from(document.querySelectorAll('input[type="file"]')).find((el) => {
|
|
250
|
+
const accept = el.getAttribute('accept') || '';
|
|
251
|
+
return (
|
|
252
|
+
accept.includes('image') ||
|
|
253
|
+
accept.includes('.jpg') ||
|
|
254
|
+
accept.includes('.jpeg') ||
|
|
255
|
+
accept.includes('.png') ||
|
|
256
|
+
accept.includes('.gif') ||
|
|
257
|
+
accept.includes('.webp')
|
|
258
|
+
);
|
|
259
|
+
});
|
|
260
|
+
const hasVideoSurface = text.includes('拖拽视频到此处点击上传') || text.includes('上传视频');
|
|
261
|
+
const state = hasTitleInput ? 'editor_ready' : hasImageInput || !hasVideoSurface ? 'image_surface' : 'video_surface';
|
|
262
|
+
return { state, hasTitleInput, hasImageInput, hasVideoSurface };
|
|
263
|
+
}
|
|
264
|
+
`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function waitForPublishSurfaceState(
|
|
268
|
+
page: IPage,
|
|
269
|
+
maxWaitMs = 5_000,
|
|
270
|
+
): Promise<PublishSurfaceInspection> {
|
|
271
|
+
const pollMs = 500;
|
|
272
|
+
const maxAttempts = Math.max(1, Math.ceil(maxWaitMs / pollMs));
|
|
273
|
+
let surface = await inspectPublishSurfaceState(page);
|
|
274
|
+
|
|
275
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
276
|
+
if (surface.state !== 'video_surface') {
|
|
277
|
+
return surface;
|
|
278
|
+
}
|
|
279
|
+
if (i < maxAttempts - 1) {
|
|
280
|
+
await page.wait({ time: pollMs / 1_000 });
|
|
281
|
+
surface = await inspectPublishSurfaceState(page);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return surface;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Poll until the title/content editing form appears on the page.
|
|
290
|
+
* The new creator center UI only renders the editor after images are uploaded.
|
|
291
|
+
*/
|
|
292
|
+
async function waitForEditForm(page: IPage, maxWaitMs = 10_000): Promise<boolean> {
|
|
293
|
+
const pollMs = 1_000;
|
|
294
|
+
const maxAttempts = Math.ceil(maxWaitMs / pollMs);
|
|
295
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
296
|
+
const found: boolean = await page.evaluate(`
|
|
297
|
+
(() => {
|
|
298
|
+
const sels = ${JSON.stringify(TITLE_SELECTORS)};
|
|
299
|
+
for (const sel of sels) {
|
|
300
|
+
const el = document.querySelector(sel);
|
|
301
|
+
if (el && el.offsetParent !== null) return true;
|
|
302
|
+
}
|
|
303
|
+
return false;
|
|
304
|
+
})()`);
|
|
305
|
+
if (found) return true;
|
|
306
|
+
if (i < maxAttempts - 1) await page.wait({ time: pollMs / 1_000 });
|
|
307
|
+
}
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
|
|
154
311
|
cli({
|
|
155
312
|
site: 'xiaohongshu',
|
|
156
313
|
name: 'publish',
|
|
@@ -161,7 +318,7 @@ cli({
|
|
|
161
318
|
args: [
|
|
162
319
|
{ name: 'title', required: true, help: '笔记标题 (最多20字)' },
|
|
163
320
|
{ name: 'content', required: true, positional: true, help: '笔记正文' },
|
|
164
|
-
{ name: 'images', required:
|
|
321
|
+
{ name: 'images', required: true, help: '图片路径,逗号分隔,最多9张 (jpg/png/gif/webp)' },
|
|
165
322
|
{ name: 'topics', required: false, help: '话题标签,逗号分隔,不含 # 号' },
|
|
166
323
|
{ name: 'draft', type: 'bool', default: false, help: '保存为草稿,不直接发布' },
|
|
167
324
|
],
|
|
@@ -184,6 +341,8 @@ cli({
|
|
|
184
341
|
if (title.length > MAX_TITLE_LEN)
|
|
185
342
|
throw new Error(`Title is ${title.length} chars — must be ≤ ${MAX_TITLE_LEN}`);
|
|
186
343
|
if (!content) throw new Error('Positional argument <content> is required');
|
|
344
|
+
if (imagePaths.length === 0)
|
|
345
|
+
throw new Error('At least one --images path is required. The creator center now requires images before showing the editor.');
|
|
187
346
|
if (imagePaths.length > MAX_IMAGES)
|
|
188
347
|
throw new Error(`Too many images: ${imagePaths.length} (max ${MAX_IMAGES})`);
|
|
189
348
|
|
|
@@ -204,51 +363,44 @@ cli({
|
|
|
204
363
|
}
|
|
205
364
|
|
|
206
365
|
// ── Step 2: Select 图文 (image+text) note type if tabs are present ─────────
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
`);
|
|
220
|
-
if (tabClicked) await page.wait({ time: 1 });
|
|
366
|
+
const tabResult = await selectImageTextTab(page);
|
|
367
|
+
const surface = await waitForPublishSurfaceState(page, tabResult?.ok ? 5_000 : 2_000);
|
|
368
|
+
if (surface.state === 'video_surface') {
|
|
369
|
+
await page.screenshot({ path: '/tmp/xhs_publish_tab_debug.png' });
|
|
370
|
+
const detail = tabResult?.ok
|
|
371
|
+
? `clicked "${tabResult.text}"`
|
|
372
|
+
: `visible candidates: ${(tabResult?.visibleTexts || []).join(' | ') || 'none'}`;
|
|
373
|
+
throw new Error(
|
|
374
|
+
'Still on the video publish page after trying to select 图文. ' +
|
|
375
|
+
`Details: ${detail}. Debug screenshot: /tmp/xhs_publish_tab_debug.png`
|
|
376
|
+
);
|
|
377
|
+
}
|
|
221
378
|
|
|
222
379
|
// ── Step 3: Upload images ──────────────────────────────────────────────────
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
380
|
+
const upload = await injectImages(page, imageData);
|
|
381
|
+
if (!upload.ok) {
|
|
382
|
+
await page.screenshot({ path: '/tmp/xhs_publish_upload_debug.png' });
|
|
383
|
+
throw new Error(
|
|
384
|
+
`Image injection failed: ${upload.error ?? 'unknown'}. ` +
|
|
385
|
+
'Debug screenshot: /tmp/xhs_publish_upload_debug.png'
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
// Allow XHS to process and upload images to its CDN
|
|
389
|
+
await page.wait({ time: UPLOAD_SETTLE_MS / 1_000 });
|
|
390
|
+
await waitForUploads(page);
|
|
391
|
+
|
|
392
|
+
// ── Step 3b: Wait for editor form to render ───────────────────────────────
|
|
393
|
+
const formReady = await waitForEditForm(page);
|
|
394
|
+
if (!formReady) {
|
|
395
|
+
await page.screenshot({ path: '/tmp/xhs_publish_form_debug.png' });
|
|
396
|
+
throw new Error(
|
|
397
|
+
'Editing form did not appear after image upload. The page layout may have changed. ' +
|
|
398
|
+
'Debug screenshot: /tmp/xhs_publish_form_debug.png'
|
|
399
|
+
);
|
|
235
400
|
}
|
|
236
401
|
|
|
237
402
|
// ── Step 4: Fill title ─────────────────────────────────────────────────────
|
|
238
|
-
await fillField(
|
|
239
|
-
page,
|
|
240
|
-
[
|
|
241
|
-
'input[maxlength="20"]',
|
|
242
|
-
'input[class*="title"]',
|
|
243
|
-
'input[placeholder*="标题"]',
|
|
244
|
-
'input[placeholder*="title" i]',
|
|
245
|
-
'.title-input input',
|
|
246
|
-
'.note-title input',
|
|
247
|
-
'input[maxlength]',
|
|
248
|
-
],
|
|
249
|
-
title,
|
|
250
|
-
'title'
|
|
251
|
-
);
|
|
403
|
+
await fillField(page, TITLE_SELECTORS, title, 'title');
|
|
252
404
|
await page.wait({ time: 0.5 });
|
|
253
405
|
|
|
254
406
|
// ── Step 5: Fill content / body ────────────────────────────────────────────
|
|
@@ -263,7 +415,7 @@ cli({
|
|
|
263
415
|
'.note-content [contenteditable="true"]',
|
|
264
416
|
'.editor-content [contenteditable="true"]',
|
|
265
417
|
// Broad fallback — last resort; filter out any title contenteditable
|
|
266
|
-
'[contenteditable="true"]:not([placeholder*="标题"]):not([placeholder*="title" i])',
|
|
418
|
+
'[contenteditable="true"]:not([placeholder*="标题"]):not([placeholder*="赞"]):not([placeholder*="title" i])',
|
|
267
419
|
],
|
|
268
420
|
content,
|
|
269
421
|
'content'
|
|
@@ -327,14 +479,14 @@ cli({
|
|
|
327
479
|
}
|
|
328
480
|
|
|
329
481
|
// ── Step 7: Publish or save draft ─────────────────────────────────────────
|
|
330
|
-
const
|
|
482
|
+
const actionLabels = isDraft ? ['暂存离开', '存草稿'] : ['发布', '发布笔记'];
|
|
331
483
|
const btnClicked: boolean = await page.evaluate(`
|
|
332
|
-
(
|
|
484
|
+
(labels => {
|
|
333
485
|
const buttons = document.querySelectorAll('button, [role="button"]');
|
|
334
486
|
for (const btn of buttons) {
|
|
335
487
|
const text = (btn.innerText || btn.textContent || '').trim();
|
|
336
488
|
if (
|
|
337
|
-
(text ===
|
|
489
|
+
labels.some(l => text === l || text.includes(l)) &&
|
|
338
490
|
btn.offsetParent !== null &&
|
|
339
491
|
!btn.disabled
|
|
340
492
|
) {
|
|
@@ -343,13 +495,13 @@ cli({
|
|
|
343
495
|
}
|
|
344
496
|
}
|
|
345
497
|
return false;
|
|
346
|
-
})(${JSON.stringify(
|
|
498
|
+
})(${JSON.stringify(actionLabels)})
|
|
347
499
|
`);
|
|
348
500
|
|
|
349
501
|
if (!btnClicked) {
|
|
350
502
|
await page.screenshot({ path: '/tmp/xhs_publish_submit_debug.png' });
|
|
351
503
|
throw new Error(
|
|
352
|
-
`Could not find "${
|
|
504
|
+
`Could not find "${actionLabels[0]}" button. ` +
|
|
353
505
|
'Debug screenshot: /tmp/xhs_publish_submit_debug.png'
|
|
354
506
|
);
|
|
355
507
|
}
|
|
@@ -364,7 +516,7 @@ cli({
|
|
|
364
516
|
const text = (el.innerText || '').trim();
|
|
365
517
|
if (
|
|
366
518
|
el.children.length === 0 &&
|
|
367
|
-
(text.includes('发布成功') || text.includes('草稿已保存') || text.includes('上传成功'))
|
|
519
|
+
(text.includes('发布成功') || text.includes('草稿已保存') || text.includes('暂存成功') || text.includes('上传成功'))
|
|
368
520
|
) return text;
|
|
369
521
|
}
|
|
370
522
|
return '';
|
|
@@ -373,14 +525,14 @@ cli({
|
|
|
373
525
|
|
|
374
526
|
const navigatedAway = !finalUrl.includes('/publish/publish');
|
|
375
527
|
const isSuccess = successMsg.length > 0 || navigatedAway;
|
|
376
|
-
const verb = isDraft ? '
|
|
528
|
+
const verb = isDraft ? '暂存成功' : '发布成功';
|
|
377
529
|
|
|
378
530
|
return [
|
|
379
531
|
{
|
|
380
532
|
status: isSuccess ? `✅ ${verb}` : '⚠️ 操作完成,请在浏览器中确认',
|
|
381
533
|
detail: [
|
|
382
534
|
`"${title}"`,
|
|
383
|
-
|
|
535
|
+
`${imageData.length}张图片`,
|
|
384
536
|
topics.length ? `话题: ${topics.join(' ')}` : '',
|
|
385
537
|
successMsg || finalUrl || '',
|
|
386
538
|
]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import type { IPage } from '../../types.js';
|
|
3
3
|
import { getRegistry } from '../../registry.js';
|
|
4
|
-
import './search.js';
|
|
4
|
+
import { noteIdToDate } from './search.js';
|
|
5
5
|
|
|
6
6
|
function createPageMock(evaluateResults: any[]): IPage {
|
|
7
7
|
const evaluate = vi.fn();
|
|
@@ -86,6 +86,7 @@ describe('xiaohongshu search', () => {
|
|
|
86
86
|
title: '某鱼买FSD被坑了4万',
|
|
87
87
|
author: '随风',
|
|
88
88
|
likes: '261',
|
|
89
|
+
published_at: '2025-10-10',
|
|
89
90
|
url: detailUrl,
|
|
90
91
|
author_url: authorUrl,
|
|
91
92
|
},
|
|
@@ -132,3 +133,40 @@ describe('xiaohongshu search', () => {
|
|
|
132
133
|
expect(result[0]).toMatchObject({ rank: 1, title: 'Result A' });
|
|
133
134
|
});
|
|
134
135
|
});
|
|
136
|
+
|
|
137
|
+
describe('noteIdToDate (ObjectID timestamp parsing)', () => {
|
|
138
|
+
it('parses a known note ID to the correct China-timezone date', () => {
|
|
139
|
+
// 0x697f6c74 = 1769958516 → 2026-02-01 in UTC+8
|
|
140
|
+
expect(noteIdToDate('https://www.xiaohongshu.com/search_result/697f6c74000000002103de17')).toBe('2026-02-01');
|
|
141
|
+
// 0x68e90be8 → 2025-10-10 in UTC+8
|
|
142
|
+
expect(noteIdToDate('https://www.xiaohongshu.com/explore/68e90be80000000004022e66')).toBe('2025-10-10');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('returns China date when UTC+8 crosses into the next day', () => {
|
|
146
|
+
// 0x69b739f0 = 2026-03-15 23:00 UTC = 2026-03-16 07:00 CST
|
|
147
|
+
// Without UTC+8 offset this would incorrectly return 2026-03-15
|
|
148
|
+
expect(noteIdToDate('https://www.xiaohongshu.com/search_result/69b739f00000000000000000')).toBe('2026-03-16');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('handles /note/ path variant', () => {
|
|
152
|
+
expect(noteIdToDate('https://www.xiaohongshu.com/note/697f6c74000000002103de17')).toBe('2026-02-01');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('handles URL with query parameters', () => {
|
|
156
|
+
expect(noteIdToDate('https://www.xiaohongshu.com/search_result/697f6c74000000002103de17?xsec_token=abc')).toBe('2026-02-01');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('returns empty string for non-matching URLs', () => {
|
|
160
|
+
expect(noteIdToDate('https://www.xiaohongshu.com/user/profile/635a9c720000000018028b40')).toBe('');
|
|
161
|
+
expect(noteIdToDate('https://www.xiaohongshu.com/')).toBe('');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('returns empty string for IDs shorter than 24 hex chars', () => {
|
|
165
|
+
expect(noteIdToDate('https://www.xiaohongshu.com/search_result/abcdef')).toBe('');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('returns empty string when timestamp is out of range', () => {
|
|
169
|
+
// All zeros → ts = 0
|
|
170
|
+
expect(noteIdToDate('https://www.xiaohongshu.com/search_result/000000000000000000000000')).toBe('');
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -9,6 +9,23 @@
|
|
|
9
9
|
import { cli, Strategy } from '../../registry.js';
|
|
10
10
|
import { AuthRequiredError } from '../../errors.js';
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Extract approximate publish date from a Xiaohongshu note URL.
|
|
14
|
+
* XHS note IDs follow MongoDB ObjectID format where the first 8 hex
|
|
15
|
+
* characters encode a Unix timestamp (the moment the ID was generated,
|
|
16
|
+
* which closely matches publish time but is not an official API field).
|
|
17
|
+
* e.g. "697f6c74..." → 0x697f6c74 = 1769958516 → 2026-02-01
|
|
18
|
+
*/
|
|
19
|
+
export function noteIdToDate(url: string): string {
|
|
20
|
+
const match = url.match(/\/(?:search_result|explore|note)\/([0-9a-f]{24})(?=[?#/]|$)/i);
|
|
21
|
+
if (!match) return '';
|
|
22
|
+
const hex = match[1].substring(0, 8);
|
|
23
|
+
const ts = parseInt(hex, 16);
|
|
24
|
+
if (!ts || ts < 1_000_000_000 || ts > 4_000_000_000) return '';
|
|
25
|
+
// Offset by UTC+8 (China Standard Time) so the date matches what XHS users see
|
|
26
|
+
return new Date((ts + 8 * 3600) * 1000).toISOString().slice(0, 10);
|
|
27
|
+
}
|
|
28
|
+
|
|
12
29
|
cli({
|
|
13
30
|
site: 'xiaohongshu',
|
|
14
31
|
name: 'search',
|
|
@@ -19,7 +36,7 @@ cli({
|
|
|
19
36
|
{ name: 'query', required: true, positional: true, help: 'Search keyword' },
|
|
20
37
|
{ name: 'limit', type: 'int', default: 20, help: 'Number of results' },
|
|
21
38
|
],
|
|
22
|
-
columns: ['rank', 'title', 'author', 'likes', 'url'],
|
|
39
|
+
columns: ['rank', 'title', 'author', 'likes', 'published_at', 'url'],
|
|
23
40
|
func: async (page, kwargs) => {
|
|
24
41
|
const keyword = encodeURIComponent(kwargs.query);
|
|
25
42
|
await page.goto(
|
|
@@ -97,6 +114,7 @@ cli({
|
|
|
97
114
|
.map((item: any, i: number) => ({
|
|
98
115
|
rank: i + 1,
|
|
99
116
|
...item,
|
|
117
|
+
published_at: noteIdToDate(item.url),
|
|
100
118
|
}));
|
|
101
119
|
},
|
|
102
120
|
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import type { CliCommand } from './registry.js';
|
|
4
|
+
|
|
5
|
+
const { mockExecuteCommand, mockRenderOutput } = vi.hoisted(() => ({
|
|
6
|
+
mockExecuteCommand: vi.fn(),
|
|
7
|
+
mockRenderOutput: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
vi.mock('./execution.js', () => ({
|
|
11
|
+
executeCommand: mockExecuteCommand,
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
vi.mock('./output.js', () => ({
|
|
15
|
+
render: mockRenderOutput,
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
import { registerCommandToProgram } from './commanderAdapter.js';
|
|
19
|
+
|
|
20
|
+
describe('commanderAdapter arg passing', () => {
|
|
21
|
+
const cmd: CliCommand = {
|
|
22
|
+
site: 'paperreview',
|
|
23
|
+
name: 'submit',
|
|
24
|
+
description: 'Submit a PDF',
|
|
25
|
+
browser: false,
|
|
26
|
+
args: [
|
|
27
|
+
{ name: 'pdf', positional: true, required: true, help: 'Path to the paper PDF' },
|
|
28
|
+
{ name: 'dry-run', type: 'bool', default: false, help: 'Validate only' },
|
|
29
|
+
{ name: 'prepare-only', type: 'bool', default: false, help: 'Prepare only' },
|
|
30
|
+
],
|
|
31
|
+
func: vi.fn(),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
mockExecuteCommand.mockReset();
|
|
36
|
+
mockExecuteCommand.mockResolvedValue([]);
|
|
37
|
+
mockRenderOutput.mockReset();
|
|
38
|
+
delete process.env.OPENCLI_VERBOSE;
|
|
39
|
+
process.exitCode = undefined;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('passes bool flag values through to executeCommand for coercion', async () => {
|
|
43
|
+
const program = new Command();
|
|
44
|
+
const siteCmd = program.command('paperreview');
|
|
45
|
+
registerCommandToProgram(siteCmd, cmd);
|
|
46
|
+
|
|
47
|
+
await program.parseAsync(['node', 'opencli', 'paperreview', 'submit', './paper.pdf', '--dry-run', 'false']);
|
|
48
|
+
|
|
49
|
+
expect(mockExecuteCommand).toHaveBeenCalled();
|
|
50
|
+
const kwargs = mockExecuteCommand.mock.calls[0][1];
|
|
51
|
+
expect(kwargs.pdf).toBe('./paper.pdf');
|
|
52
|
+
expect(kwargs).toHaveProperty('dry-run');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('passes valueless bool flags as true to executeCommand', async () => {
|
|
56
|
+
const program = new Command();
|
|
57
|
+
const siteCmd = program.command('paperreview');
|
|
58
|
+
registerCommandToProgram(siteCmd, cmd);
|
|
59
|
+
|
|
60
|
+
await program.parseAsync(['node', 'opencli', 'paperreview', 'submit', './paper.pdf', '--prepare-only']);
|
|
61
|
+
|
|
62
|
+
expect(mockExecuteCommand).toHaveBeenCalled();
|
|
63
|
+
const kwargs = mockExecuteCommand.mock.calls[0][1];
|
|
64
|
+
expect(kwargs.pdf).toBe('./paper.pdf');
|
|
65
|
+
expect(kwargs['prepare-only']).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('rejects invalid bool values before calling executeCommand', async () => {
|
|
69
|
+
const program = new Command();
|
|
70
|
+
const siteCmd = program.command('paperreview');
|
|
71
|
+
registerCommandToProgram(siteCmd, cmd);
|
|
72
|
+
|
|
73
|
+
await program.parseAsync(['node', 'opencli', 'paperreview', 'submit', './paper.pdf', '--dry-run', 'maybe']);
|
|
74
|
+
|
|
75
|
+
// normalizeArgValue validates bools eagerly; executeCommand should not be reached
|
|
76
|
+
expect(mockExecuteCommand).not.toHaveBeenCalled();
|
|
77
|
+
});
|
|
78
|
+
});
|