@jackwener/opencli 1.7.3 → 1.7.5
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/README.md +81 -59
- package/README.zh-CN.md +93 -67
- package/cli-manifest.json +5015 -2975
- package/clis/antigravity/serve.js +71 -25
- package/clis/baidu-scholar/search.js +87 -0
- package/clis/baidu-scholar/search.test.js +23 -0
- package/clis/bilibili/favorite.js +18 -13
- package/clis/binance/depth.js +3 -4
- package/clis/boss/utils.js +2 -3
- package/clis/chatgpt-app/ax.js +6 -3
- package/clis/deepseek/ask.js +74 -0
- package/clis/deepseek/history.js +25 -0
- package/clis/deepseek/new.js +20 -0
- package/clis/deepseek/read.js +22 -0
- package/clis/deepseek/status.js +24 -0
- package/clis/deepseek/utils.js +208 -0
- package/clis/douban/search.js +1 -0
- package/clis/douban/search.test.js +11 -0
- package/clis/douban/subject.js +20 -93
- package/clis/douban/subject.test.js +11 -0
- package/clis/douban/utils.js +250 -8
- package/clis/douban/utils.test.js +179 -4
- package/clis/doubao/utils.js +319 -130
- package/clis/doubao/utils.test.js +241 -2
- package/clis/eastmoney/_secid.js +78 -0
- package/clis/eastmoney/announcement.js +52 -0
- package/clis/eastmoney/convertible.js +73 -0
- package/clis/eastmoney/etf.js +65 -0
- package/clis/eastmoney/holders.js +78 -0
- package/clis/eastmoney/hot-rank.js +50 -0
- package/clis/eastmoney/hot-rank.test.js +59 -0
- package/clis/eastmoney/index-board.js +96 -0
- package/clis/eastmoney/kline.js +87 -0
- package/clis/eastmoney/kuaixun.js +54 -0
- package/clis/eastmoney/longhu.js +67 -0
- package/clis/eastmoney/money-flow.js +78 -0
- package/clis/eastmoney/northbound.js +57 -0
- package/clis/eastmoney/quote.js +107 -0
- package/clis/eastmoney/rank.js +94 -0
- package/clis/eastmoney/sectors.js +76 -0
- package/clis/google-scholar/search.js +58 -0
- package/clis/google-scholar/search.test.js +23 -0
- package/clis/gov-law/commands.test.js +39 -0
- package/clis/gov-law/recent.js +22 -0
- package/clis/gov-law/search.js +41 -0
- package/clis/gov-law/shared.js +51 -0
- package/clis/gov-policy/commands.test.js +27 -0
- package/clis/gov-policy/recent.js +47 -0
- package/clis/gov-policy/search.js +48 -0
- package/clis/grok/image.test.ts +107 -0
- package/clis/grok/image.ts +356 -0
- package/clis/nowcoder/companies.js +23 -0
- package/clis/nowcoder/creators.js +27 -0
- package/clis/nowcoder/detail.js +61 -0
- package/clis/nowcoder/experience.js +36 -0
- package/clis/nowcoder/hot.js +24 -0
- package/clis/nowcoder/jobs.js +21 -0
- package/clis/nowcoder/notifications.js +29 -0
- package/clis/nowcoder/papers.js +40 -0
- package/clis/nowcoder/practice.js +37 -0
- package/clis/nowcoder/recommend.js +30 -0
- package/clis/nowcoder/referral.js +39 -0
- package/clis/nowcoder/salary.js +40 -0
- package/clis/nowcoder/search.js +49 -0
- package/clis/nowcoder/suggest.js +33 -0
- package/clis/nowcoder/topics.js +27 -0
- package/clis/nowcoder/trending.js +25 -0
- package/clis/tdx/hot-rank.js +47 -0
- package/clis/tdx/hot-rank.test.js +59 -0
- package/clis/ths/hot-rank.js +49 -0
- package/clis/ths/hot-rank.test.js +64 -0
- package/clis/twitter/bookmarks.js +2 -1
- package/clis/twitter/list-add.js +337 -0
- package/clis/twitter/list-add.test.js +15 -0
- package/clis/twitter/list-remove.js +297 -0
- package/clis/twitter/list-remove.test.js +14 -0
- package/clis/twitter/list-tweets.js +185 -0
- package/clis/twitter/list-tweets.test.js +108 -0
- package/clis/twitter/lists.js +134 -47
- package/clis/twitter/lists.test.js +105 -38
- package/clis/uiverse/_shared.js +368 -0
- package/clis/uiverse/_shared.test.js +55 -0
- package/clis/uiverse/code.js +47 -0
- package/clis/uiverse/preview.js +71 -0
- package/clis/wanfang/search.js +66 -0
- package/clis/wanfang/search.test.js +23 -0
- package/clis/web/read.js +1 -1
- package/clis/weixin/download.js +3 -2
- package/clis/xiaohongshu/comments.js +2 -2
- package/clis/xiaohongshu/comments.test.js +46 -25
- package/clis/xiaohongshu/download.js +6 -7
- package/clis/xiaohongshu/download.test.js +17 -5
- package/clis/xiaohongshu/note-helpers.js +46 -12
- package/clis/xiaohongshu/note.js +3 -5
- package/clis/xiaohongshu/note.test.js +52 -25
- package/clis/xiaohongshu/publish.js +149 -28
- package/clis/xiaohongshu/publish.test.js +319 -6
- package/clis/xiaoyuzhou/auth.js +303 -0
- package/clis/xiaoyuzhou/auth.test.js +124 -0
- package/clis/xiaoyuzhou/download.js +53 -0
- package/clis/xiaoyuzhou/download.test.js +135 -0
- package/clis/xiaoyuzhou/episode.js +9 -4
- package/clis/xiaoyuzhou/podcast-episodes.js +15 -11
- package/clis/xiaoyuzhou/podcast.js +9 -4
- package/clis/xiaoyuzhou/transcript.js +76 -0
- package/clis/xiaoyuzhou/transcript.test.js +195 -0
- package/clis/xiaoyuzhou/utils.js +0 -40
- package/clis/xiaoyuzhou/utils.test.js +15 -75
- package/clis/youtube/feed.js +120 -0
- package/clis/youtube/history.js +118 -0
- package/clis/youtube/like.js +62 -0
- package/clis/youtube/playlist.js +97 -0
- package/clis/youtube/subscribe.js +71 -0
- package/clis/youtube/subscriptions.js +57 -0
- package/clis/youtube/unlike.js +62 -0
- package/clis/youtube/unsubscribe.js +71 -0
- package/clis/youtube/utils.js +122 -0
- package/clis/youtube/utils.test.js +32 -1
- package/clis/youtube/watch-later.js +76 -0
- package/clis/zsxq/dynamics.js +1 -1
- package/clis/zsxq/utils.js +6 -3
- package/clis/zsxq/utils.test.js +31 -0
- package/dist/src/browser/base-page.d.ts +1 -1
- package/dist/src/browser/base-page.js +25 -5
- package/dist/src/browser/bridge.d.ts +3 -0
- package/dist/src/browser/bridge.js +52 -15
- package/dist/src/browser/cdp.js +2 -1
- package/dist/src/browser/daemon-client.d.ts +7 -4
- package/dist/src/browser/daemon-client.js +6 -1
- package/dist/src/browser/daemon-client.test.js +40 -1
- package/dist/src/browser/dom-snapshot.js +20 -3
- package/dist/src/browser/page.d.ts +18 -5
- package/dist/src/browser/page.js +96 -15
- package/dist/src/browser/page.test.js +158 -1
- package/dist/src/browser/target-errors.d.ts +23 -0
- package/dist/src/browser/target-errors.js +29 -0
- package/dist/src/browser/target-errors.test.js +61 -0
- package/dist/src/browser/target-resolver.d.ts +57 -0
- package/dist/src/browser/target-resolver.js +298 -0
- package/dist/src/browser/target-resolver.test.js +43 -0
- package/dist/src/browser.test.js +38 -1
- package/dist/src/cli.js +272 -187
- package/dist/src/cli.test.js +167 -90
- package/dist/src/commanderAdapter.d.ts +0 -1
- package/dist/src/commanderAdapter.js +2 -16
- package/dist/src/commanderAdapter.test.js +1 -1
- package/dist/src/commands/daemon.d.ts +4 -2
- package/dist/src/commands/daemon.js +22 -2
- package/dist/src/commands/daemon.test.js +65 -2
- package/dist/src/completion-shared.js +2 -5
- package/dist/src/daemon.js +10 -0
- package/dist/src/doctor.d.ts +1 -0
- package/dist/src/doctor.js +32 -9
- package/dist/src/doctor.test.js +28 -12
- package/dist/src/download/article-download.d.ts +1 -0
- package/dist/src/download/article-download.js +3 -0
- package/dist/src/download/article-download.test.js +39 -0
- package/dist/src/external-clis.yaml +2 -2
- package/dist/src/logger.d.ts +2 -2
- package/dist/src/logger.js +3 -3
- package/dist/src/output.js +1 -5
- package/dist/src/output.test.js +0 -21
- package/dist/src/pipeline/steps/transform.js +1 -1
- package/dist/src/pipeline/template.d.ts +1 -0
- package/dist/src/pipeline/template.js +11 -3
- package/dist/src/pipeline/template.test.js +3 -0
- package/dist/src/pipeline/transform.test.js +14 -0
- package/dist/src/plugin.d.ts +8 -9
- package/dist/src/plugin.js +24 -28
- package/dist/src/plugin.test.js +16 -60
- package/dist/src/registry.d.ts +1 -0
- package/dist/src/registry.js +3 -2
- package/dist/src/registry.test.js +22 -0
- package/dist/src/types.d.ts +15 -6
- package/package.json +1 -1
- package/clis/twitter/lists-parser.js +0 -77
- package/clis/twitter/lists.d.ts +0 -5
- package/dist/src/cascade.d.ts +0 -46
- package/dist/src/cascade.js +0 -135
- package/dist/src/explore.d.ts +0 -99
- package/dist/src/explore.js +0 -402
- package/dist/src/generate-verified.d.ts +0 -105
- package/dist/src/generate-verified.js +0 -696
- package/dist/src/generate-verified.test.js +0 -925
- package/dist/src/generate.d.ts +0 -46
- package/dist/src/generate.js +0 -117
- package/dist/src/record.d.ts +0 -96
- package/dist/src/record.js +0 -657
- package/dist/src/record.test.js +0 -293
- package/dist/src/skill-generate.d.ts +0 -30
- package/dist/src/skill-generate.js +0 -75
- package/dist/src/skill-generate.test.js +0 -173
- package/dist/src/synthesize.d.ts +0 -97
- package/dist/src/synthesize.js +0 -208
- /package/dist/src/{generate-verified.test.d.ts → browser/target-errors.test.d.ts} +0 -0
- /package/dist/src/{record.test.d.ts → browser/target-resolver.test.d.ts} +0 -0
- /package/dist/src/{skill-generate.test.d.ts → download/article-download.test.d.ts} +0 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as os from 'node:os';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
|
|
5
|
+
export const UIVERSE_BASE_URL = 'https://uiverse.io';
|
|
6
|
+
|
|
7
|
+
const ROUTE_DATA_KEY = 'routes/$username.$friendlyId';
|
|
8
|
+
const CODE_DATA_KEY = 'routes/resource.post.code.$id';
|
|
9
|
+
const EXPORT_TARGET_BUTTON_LABELS = ['React', 'Vue', 'Svelte', 'Lit'];
|
|
10
|
+
|
|
11
|
+
function trimPathSegment(value) {
|
|
12
|
+
return String(value || '').trim().replace(/^\/+|\/+$/g, '');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function parseComponentInput(input) {
|
|
16
|
+
const raw = String(input || '').trim();
|
|
17
|
+
if (!raw) {
|
|
18
|
+
throw new Error('Missing component input. Pass a full Uiverse URL or an author/slug identifier.');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let pathname = raw;
|
|
22
|
+
if (/^https?:\/\//i.test(raw)) {
|
|
23
|
+
const url = new URL(raw);
|
|
24
|
+
if (url.hostname !== 'uiverse.io' && url.hostname !== 'www.uiverse.io') {
|
|
25
|
+
throw new Error(`Unsupported non-Uiverse URL: ${raw}`);
|
|
26
|
+
}
|
|
27
|
+
pathname = url.pathname;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const cleaned = trimPathSegment(pathname);
|
|
31
|
+
const segments = cleaned.split('/').filter(Boolean);
|
|
32
|
+
if (segments.length !== 2) {
|
|
33
|
+
throw new Error(`Could not parse author/slug from input: ${raw}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const [username, slug] = segments;
|
|
37
|
+
if (!username || !slug) {
|
|
38
|
+
throw new Error(`Invalid component identifier: ${raw}. Expected author/slug.`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
raw,
|
|
43
|
+
username,
|
|
44
|
+
slug,
|
|
45
|
+
url: `${UIVERSE_BASE_URL}/${username}/${slug}`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function fetchJsonInBrowser(page, url) {
|
|
50
|
+
const raw = await page.evaluate(`(async () => {
|
|
51
|
+
const url = ${JSON.stringify(url)};
|
|
52
|
+
const response = await fetch(url, {
|
|
53
|
+
credentials: 'include',
|
|
54
|
+
headers: {
|
|
55
|
+
accept: 'application/json, text/plain, */*',
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
const text = await response.text();
|
|
59
|
+
return JSON.stringify({
|
|
60
|
+
ok: response.ok,
|
|
61
|
+
status: response.status,
|
|
62
|
+
statusText: response.statusText,
|
|
63
|
+
text,
|
|
64
|
+
url,
|
|
65
|
+
});
|
|
66
|
+
})()`);
|
|
67
|
+
|
|
68
|
+
const result = JSON.parse(raw);
|
|
69
|
+
if (!result?.ok) {
|
|
70
|
+
throw new Error(`Request failed: ${result?.status} ${result?.statusText} (${result?.url || url})`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
return JSON.parse(result.text);
|
|
75
|
+
} catch {
|
|
76
|
+
throw new Error(`Response was not valid JSON: ${url}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function getPostDetails(page, input) {
|
|
81
|
+
const normalized = parseComponentInput(input);
|
|
82
|
+
await page.goto(normalized.url);
|
|
83
|
+
|
|
84
|
+
const raw = await page.evaluate(`(async () => {
|
|
85
|
+
const key = ${JSON.stringify(ROUTE_DATA_KEY)};
|
|
86
|
+
const loaderData = window.__remixContext?.state?.loaderData || {};
|
|
87
|
+
const routeData = loaderData[key];
|
|
88
|
+
return JSON.stringify({ routeData: routeData || null, keys: Object.keys(loaderData) });
|
|
89
|
+
})()`);
|
|
90
|
+
|
|
91
|
+
const parsed = JSON.parse(raw);
|
|
92
|
+
let routeData = parsed?.routeData;
|
|
93
|
+
if (!routeData?.post?.id) {
|
|
94
|
+
const routeUrl = `${normalized.url}?_data=${encodeURIComponent(ROUTE_DATA_KEY)}`;
|
|
95
|
+
routeData = await fetchJsonInBrowser(page, routeUrl);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!routeData?.post?.id) {
|
|
99
|
+
throw new Error(`Could not resolve post.id from the component page: ${normalized.url}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
...normalized,
|
|
104
|
+
post: routeData.post,
|
|
105
|
+
routeData,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function getRawCode(page, postId) {
|
|
110
|
+
const codeUrl = `${UIVERSE_BASE_URL}/resource/post/code/${postId}?v=1&_data=${encodeURIComponent(CODE_DATA_KEY)}`;
|
|
111
|
+
const payload = await fetchJsonInBrowser(page, codeUrl);
|
|
112
|
+
if (typeof payload?.html !== 'string' || typeof payload?.css !== 'string') {
|
|
113
|
+
throw new Error(`Unexpected code payload shape: ${codeUrl}`);
|
|
114
|
+
}
|
|
115
|
+
return payload;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function inferLanguage(target, post) {
|
|
119
|
+
if (target === 'react') return 'tsx';
|
|
120
|
+
if (target === 'vue') return 'vue';
|
|
121
|
+
if (target === 'html') return post?.isTailwind ? 'html+tailwind' : 'html';
|
|
122
|
+
if (target === 'css') return 'css';
|
|
123
|
+
return 'text';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function getCodeLength(code) {
|
|
127
|
+
return String(code || '').length;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function normalizeExportTarget(target) {
|
|
131
|
+
return String(target || '').trim().toLowerCase() === 'vue' ? 'Vue' : 'React';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function extractExportCode(page, target = 'react') {
|
|
135
|
+
const targetLabel = normalizeExportTarget(target);
|
|
136
|
+
const raw = await page.evaluate(`(async () => {
|
|
137
|
+
const targetLabel = ${JSON.stringify(targetLabel)};
|
|
138
|
+
const exportButtonLabel = 'Export';
|
|
139
|
+
const exportTargetButtonLabels = ${JSON.stringify(EXPORT_TARGET_BUTTON_LABELS)};
|
|
140
|
+
|
|
141
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
142
|
+
|
|
143
|
+
const triggerClick = (element) => {
|
|
144
|
+
if (!element) return;
|
|
145
|
+
element.focus?.();
|
|
146
|
+
const pointer = { bubbles: true, cancelable: true, composed: true, view: window };
|
|
147
|
+
const mouse = { bubbles: true, cancelable: true, composed: true, view: window, button: 0, buttons: 1 };
|
|
148
|
+
element.dispatchEvent(new PointerEvent('pointerdown', pointer));
|
|
149
|
+
element.dispatchEvent(new MouseEvent('mousedown', mouse));
|
|
150
|
+
element.dispatchEvent(new PointerEvent('pointerup', pointer));
|
|
151
|
+
element.dispatchEvent(new MouseEvent('mouseup', mouse));
|
|
152
|
+
element.dispatchEvent(new MouseEvent('click', mouse));
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const isCompleteExportCode = (code) => {
|
|
156
|
+
if (!code) return false;
|
|
157
|
+
if (targetLabel === 'Vue') {
|
|
158
|
+
return code.includes('<template>')
|
|
159
|
+
&& code.includes('</template>')
|
|
160
|
+
&& code.includes('<style')
|
|
161
|
+
&& code.includes('</style>');
|
|
162
|
+
}
|
|
163
|
+
return code.includes('export default')
|
|
164
|
+
&& (code.includes('styled-components') || code.includes('StyledWrapper') || code.includes('styled.'));
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const readCode = () => {
|
|
168
|
+
const dialog = document.querySelector('[role="dialog"]');
|
|
169
|
+
if (!dialog) return null;
|
|
170
|
+
const heading = dialog.querySelector('h1,h2,h3,h4,h5,h6');
|
|
171
|
+
if (heading && (heading.textContent || '').trim() !== targetLabel) return null;
|
|
172
|
+
const textarea = dialog.querySelector('textarea');
|
|
173
|
+
if (textarea && textarea.value) return textarea.value;
|
|
174
|
+
return null;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const exportButton = [...document.querySelectorAll('button')].find((element) => (element.textContent || '').trim() === exportButtonLabel);
|
|
178
|
+
const currentTargetButton = [...document.querySelectorAll('button')].find((element) => {
|
|
179
|
+
const text = (element.textContent || '').trim();
|
|
180
|
+
return exportTargetButtonLabels.includes(text);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const existing = readCode();
|
|
184
|
+
if (!existing && (!exportButton || !currentTargetButton)) {
|
|
185
|
+
return JSON.stringify({ ok: false, error: 'Could not find the export controls on the page.' });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!existing) {
|
|
189
|
+
const currentLabel = (currentTargetButton.textContent || '').trim();
|
|
190
|
+
if (currentLabel === targetLabel) {
|
|
191
|
+
triggerClick(exportButton);
|
|
192
|
+
} else {
|
|
193
|
+
triggerClick(currentTargetButton);
|
|
194
|
+
let menuItem = null;
|
|
195
|
+
for (let index = 0; index < 20; index += 1) {
|
|
196
|
+
menuItem = [...document.querySelectorAll('[role="menuitem"]')].find((element) => (element.textContent || '').trim() === targetLabel);
|
|
197
|
+
if (menuItem) break;
|
|
198
|
+
await sleep(100);
|
|
199
|
+
}
|
|
200
|
+
if (!menuItem) {
|
|
201
|
+
return JSON.stringify({ ok: false, error: 'Could not find target in export menu: ' + targetLabel });
|
|
202
|
+
}
|
|
203
|
+
triggerClick(menuItem);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let longest = existing || '';
|
|
208
|
+
let longestLooksComplete = isCompleteExportCode(longest);
|
|
209
|
+
let stableCount = 0;
|
|
210
|
+
|
|
211
|
+
for (let index = 0; index < 40; index += 1) {
|
|
212
|
+
await sleep(200);
|
|
213
|
+
const code = readCode();
|
|
214
|
+
if (!code) continue;
|
|
215
|
+
|
|
216
|
+
if (code.length > longest.length) {
|
|
217
|
+
longest = code;
|
|
218
|
+
longestLooksComplete = isCompleteExportCode(code);
|
|
219
|
+
stableCount = 0;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (code === longest) {
|
|
224
|
+
if (longestLooksComplete) {
|
|
225
|
+
stableCount += 1;
|
|
226
|
+
if (stableCount >= 2) {
|
|
227
|
+
return JSON.stringify({ ok: true, code: longest, length: longest.length });
|
|
228
|
+
}
|
|
229
|
+
} else {
|
|
230
|
+
stableCount = 0;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const dialog = document.querySelector('[role="dialog"]');
|
|
236
|
+
if (longest && longestLooksComplete) {
|
|
237
|
+
return JSON.stringify({ ok: true, code: longest, length: longest.length, fallback: true });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return JSON.stringify({
|
|
241
|
+
ok: false,
|
|
242
|
+
error: dialog
|
|
243
|
+
? (targetLabel + ' dialog appeared, but the exported code never reached a stable complete state.')
|
|
244
|
+
: (targetLabel + ' export dialog did not appear after clicking the export controls.'),
|
|
245
|
+
dialogFound: Boolean(dialog),
|
|
246
|
+
dialogText: dialog ? (dialog.innerText || '').slice(0, 200) : null,
|
|
247
|
+
longestLength: longest.length,
|
|
248
|
+
});
|
|
249
|
+
})()`);
|
|
250
|
+
|
|
251
|
+
const data = JSON.parse(raw);
|
|
252
|
+
if (!data?.ok || typeof data.code !== 'string') {
|
|
253
|
+
throw new Error(data?.error || `Failed to extract ${targetLabel} export code.`);
|
|
254
|
+
}
|
|
255
|
+
return data.code;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function parseHtmlRootSignature(html) {
|
|
259
|
+
const source = String(html || '').trim();
|
|
260
|
+
const match = source.match(/^<([a-zA-Z0-9-]+)([^>]*)>/);
|
|
261
|
+
if (!match) {
|
|
262
|
+
return { tag: null, id: null, classes: [] };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const [, tag, attrs] = match;
|
|
266
|
+
const idMatch = attrs.match(/\sid=["']([^"']+)["']/i);
|
|
267
|
+
const classMatch = attrs.match(/\sclass=["']([^"']+)["']/i);
|
|
268
|
+
const classes = classMatch ? classMatch[1].split(/\s+/).filter(Boolean) : [];
|
|
269
|
+
return {
|
|
270
|
+
tag: tag.toLowerCase(),
|
|
271
|
+
id: idMatch ? idMatch[1] : null,
|
|
272
|
+
classes,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export async function locatePreviewElement(page, html) {
|
|
277
|
+
const signature = parseHtmlRootSignature(html);
|
|
278
|
+
const raw = await page.evaluate(`(async () => {
|
|
279
|
+
const sig = ${JSON.stringify(signature)};
|
|
280
|
+
const viewportWidth = window.innerWidth;
|
|
281
|
+
const viewportHeight = window.innerHeight;
|
|
282
|
+
const fallbackTags = sig.tag ? [sig.tag] : ['label', 'button', 'a', 'div'];
|
|
283
|
+
|
|
284
|
+
const isVisible = (element) => {
|
|
285
|
+
if (!element || !(element instanceof Element)) return false;
|
|
286
|
+
if (element.closest('[role="dialog"]')) return false;
|
|
287
|
+
const style = window.getComputedStyle(element);
|
|
288
|
+
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
|
|
289
|
+
const rect = element.getBoundingClientRect();
|
|
290
|
+
return rect.width > 0 && rect.height > 0;
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const scoreCandidate = (element, source) => {
|
|
294
|
+
const rect = element.getBoundingClientRect();
|
|
295
|
+
const centerX = rect.x + rect.width / 2;
|
|
296
|
+
const centerY = rect.y + rect.height / 2;
|
|
297
|
+
const area = rect.width * rect.height;
|
|
298
|
+
let score = 0;
|
|
299
|
+
if (sig.tag && element.tagName.toLowerCase() === sig.tag) score += 30;
|
|
300
|
+
if (sig.id && element.id === sig.id) score += 120;
|
|
301
|
+
if (sig.classes.length && sig.classes.every((className) => element.classList.contains(className))) score += 120;
|
|
302
|
+
if (centerX <= viewportWidth * 0.65) score += 40;
|
|
303
|
+
if (centerY <= viewportHeight * 0.6) score += 40;
|
|
304
|
+
if (area <= viewportWidth * viewportHeight * 0.2) score += 30;
|
|
305
|
+
if (area <= viewportWidth * viewportHeight * 0.05) score += 20;
|
|
306
|
+
return {
|
|
307
|
+
source,
|
|
308
|
+
tag: element.tagName.toLowerCase(),
|
|
309
|
+
className: element.className || '',
|
|
310
|
+
id: element.id || '',
|
|
311
|
+
score,
|
|
312
|
+
rect: {
|
|
313
|
+
x: rect.x,
|
|
314
|
+
y: rect.y,
|
|
315
|
+
width: rect.width,
|
|
316
|
+
height: rect.height,
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const candidates = [];
|
|
322
|
+
const seen = new Set();
|
|
323
|
+
const collect = (element, source) => {
|
|
324
|
+
if (!isVisible(element)) return;
|
|
325
|
+
if (seen.has(element)) return;
|
|
326
|
+
seen.add(element);
|
|
327
|
+
candidates.push(scoreCandidate(element, source));
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
if (sig.id) collect(document.getElementById(sig.id), 'id');
|
|
331
|
+
if (sig.classes.length) collect(document.querySelector('.' + sig.classes.join('.')), 'classes');
|
|
332
|
+
|
|
333
|
+
for (const tagName of fallbackTags) {
|
|
334
|
+
const tagNodes = Array.from(document.querySelectorAll(tagName));
|
|
335
|
+
for (const node of tagNodes.slice(0, 200)) {
|
|
336
|
+
collect(node, 'tag:' + tagName);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
candidates.sort((left, right) => {
|
|
341
|
+
if (right.score !== left.score) return right.score - left.score;
|
|
342
|
+
if (left.rect.y !== right.rect.y) return left.rect.y - right.rect.y;
|
|
343
|
+
if (left.rect.x !== right.rect.x) return left.rect.x - right.rect.x;
|
|
344
|
+
return (left.rect.width * left.rect.height) - (right.rect.width * right.rect.height);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
return JSON.stringify({ signature: sig, best: candidates[0] || null, candidates: candidates.slice(0, 5) });
|
|
348
|
+
})()`);
|
|
349
|
+
|
|
350
|
+
const result = JSON.parse(raw);
|
|
351
|
+
if (!result?.best?.rect?.width || !result?.best?.rect?.height) {
|
|
352
|
+
throw new Error(`Could not locate a Uiverse preview element. Candidate data: ${JSON.stringify(result)}`);
|
|
353
|
+
}
|
|
354
|
+
return result;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export function getDefaultOutputPath({ username, slug, suffix, extension }) {
|
|
358
|
+
const safeUsername = trimPathSegment(username).replace(/[^a-zA-Z0-9-_]/g, '-');
|
|
359
|
+
const safeSlug = trimPathSegment(slug).replace(/[^a-zA-Z0-9-_]/g, '-');
|
|
360
|
+
return path.join(os.tmpdir(), `opencli-uiverse-${safeUsername}-${safeSlug}-${suffix}.${extension}`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export async function saveBase64File(base64, outputPath) {
|
|
364
|
+
const resolved = path.resolve(outputPath);
|
|
365
|
+
await fs.mkdir(path.dirname(resolved), { recursive: true });
|
|
366
|
+
await fs.writeFile(resolved, Buffer.from(base64, 'base64'));
|
|
367
|
+
return resolved;
|
|
368
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
UIVERSE_BASE_URL,
|
|
4
|
+
parseComponentInput,
|
|
5
|
+
parseHtmlRootSignature,
|
|
6
|
+
inferLanguage,
|
|
7
|
+
getCodeLength,
|
|
8
|
+
} from './_shared.js';
|
|
9
|
+
|
|
10
|
+
describe('uiverse shared helpers', () => {
|
|
11
|
+
it('parses full URLs and author/slug identifiers', () => {
|
|
12
|
+
expect(parseComponentInput('Galahhad/strong-squid-82')).toEqual({
|
|
13
|
+
raw: 'Galahhad/strong-squid-82',
|
|
14
|
+
username: 'Galahhad',
|
|
15
|
+
slug: 'strong-squid-82',
|
|
16
|
+
url: `${UIVERSE_BASE_URL}/Galahhad/strong-squid-82`,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
expect(parseComponentInput('https://uiverse.io/Galahhad/strong-squid-82')).toEqual({
|
|
20
|
+
raw: 'https://uiverse.io/Galahhad/strong-squid-82',
|
|
21
|
+
username: 'Galahhad',
|
|
22
|
+
slug: 'strong-squid-82',
|
|
23
|
+
url: `${UIVERSE_BASE_URL}/Galahhad/strong-squid-82`,
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('rejects unsupported hosts and malformed identifiers', () => {
|
|
28
|
+
expect(() => parseComponentInput('https://example.com/foo/bar')).toThrow('Unsupported non-Uiverse URL');
|
|
29
|
+
expect(() => parseComponentInput('only-author')).toThrow('Could not parse author/slug');
|
|
30
|
+
expect(() => parseComponentInput('a/b/c')).toThrow('Could not parse author/slug');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('parses the HTML root signature', () => {
|
|
34
|
+
expect(parseHtmlRootSignature('<label id="x" class="theme-switch primary"></label>')).toEqual({
|
|
35
|
+
tag: 'label',
|
|
36
|
+
id: 'x',
|
|
37
|
+
classes: ['theme-switch', 'primary'],
|
|
38
|
+
});
|
|
39
|
+
expect(parseHtmlRootSignature('')).toEqual({ tag: null, id: null, classes: [] });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('infers the language from target and metadata', () => {
|
|
43
|
+
expect(inferLanguage('react', {})).toBe('tsx');
|
|
44
|
+
expect(inferLanguage('vue', {})).toBe('vue');
|
|
45
|
+
expect(inferLanguage('html', { isTailwind: true })).toBe('html+tailwind');
|
|
46
|
+
expect(inferLanguage('css', {})).toBe('css');
|
|
47
|
+
expect(inferLanguage('unknown', {})).toBe('text');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('returns the code length safely', () => {
|
|
51
|
+
expect(getCodeLength('abc')).toBe(3);
|
|
52
|
+
expect(getCodeLength('')).toBe(0);
|
|
53
|
+
expect(getCodeLength(null)).toBe(0);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import {
|
|
3
|
+
getPostDetails,
|
|
4
|
+
getRawCode,
|
|
5
|
+
extractExportCode,
|
|
6
|
+
inferLanguage,
|
|
7
|
+
getCodeLength,
|
|
8
|
+
} from './_shared.js';
|
|
9
|
+
|
|
10
|
+
cli({
|
|
11
|
+
site: 'uiverse',
|
|
12
|
+
name: 'code',
|
|
13
|
+
description: 'Export Uiverse component code (HTML, CSS, React, or Vue)',
|
|
14
|
+
domain: 'uiverse.io',
|
|
15
|
+
strategy: Strategy.PUBLIC,
|
|
16
|
+
browser: true,
|
|
17
|
+
args: [
|
|
18
|
+
{ name: 'input', type: 'str', required: true, positional: true, help: 'Uiverse URL or author/slug identifier' },
|
|
19
|
+
{ name: 'target', type: 'str', required: true, choices: ['html', 'css', 'react', 'vue'], help: 'Code target to export' },
|
|
20
|
+
],
|
|
21
|
+
columns: ['target', 'username', 'slug', 'language', 'length'],
|
|
22
|
+
func: async (page, kwargs) => {
|
|
23
|
+
const detail = await getPostDetails(page, kwargs.input);
|
|
24
|
+
const target = String(kwargs.target).toLowerCase();
|
|
25
|
+
let code = '';
|
|
26
|
+
|
|
27
|
+
if (target === 'react' || target === 'vue') {
|
|
28
|
+
code = await extractExportCode(page, target);
|
|
29
|
+
} else {
|
|
30
|
+
const payload = await getRawCode(page, detail.post.id);
|
|
31
|
+
code = target === 'html' ? payload.html : payload.css;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
target,
|
|
36
|
+
username: detail.username,
|
|
37
|
+
slug: detail.slug,
|
|
38
|
+
url: detail.url,
|
|
39
|
+
language: inferLanguage(target, detail.post),
|
|
40
|
+
length: getCodeLength(code),
|
|
41
|
+
code,
|
|
42
|
+
postId: detail.post.id,
|
|
43
|
+
type: detail.post.type,
|
|
44
|
+
isTailwind: Boolean(detail.post.isTailwind),
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import {
|
|
3
|
+
getPostDetails,
|
|
4
|
+
getRawCode,
|
|
5
|
+
locatePreviewElement,
|
|
6
|
+
getDefaultOutputPath,
|
|
7
|
+
saveBase64File,
|
|
8
|
+
} from './_shared.js';
|
|
9
|
+
|
|
10
|
+
cli({
|
|
11
|
+
site: 'uiverse',
|
|
12
|
+
name: 'preview',
|
|
13
|
+
description: 'Capture a screenshot of the Uiverse preview element',
|
|
14
|
+
domain: 'uiverse.io',
|
|
15
|
+
strategy: Strategy.PUBLIC,
|
|
16
|
+
browser: true,
|
|
17
|
+
args: [
|
|
18
|
+
{ name: 'input', type: 'str', required: true, positional: true, help: 'Uiverse URL or author/slug identifier' },
|
|
19
|
+
{ name: 'output', type: 'str', required: false, help: 'Output image path (defaults to a temp file)' },
|
|
20
|
+
{ name: 'padding', type: 'int', required: false, default: 8, help: 'Extra padding around the captured preview in pixels' },
|
|
21
|
+
],
|
|
22
|
+
columns: ['username', 'slug', 'width', 'height', 'output'],
|
|
23
|
+
func: async (page, kwargs) => {
|
|
24
|
+
const detail = await getPostDetails(page, kwargs.input);
|
|
25
|
+
const payload = await getRawCode(page, detail.post.id);
|
|
26
|
+
|
|
27
|
+
const located = await locatePreviewElement(page, payload.html);
|
|
28
|
+
const rect = located.best.rect;
|
|
29
|
+
const padding = Math.max(0, Number(kwargs.padding ?? 8));
|
|
30
|
+
const clip = {
|
|
31
|
+
x: Math.max(0, rect.x - padding),
|
|
32
|
+
y: Math.max(0, rect.y - padding),
|
|
33
|
+
width: Math.max(1, rect.width + padding * 2),
|
|
34
|
+
height: Math.max(1, rect.height + padding * 2),
|
|
35
|
+
scale: 1,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const shot = await page.cdp('Page.captureScreenshot', {
|
|
39
|
+
format: 'png',
|
|
40
|
+
clip,
|
|
41
|
+
captureBeyondViewport: false,
|
|
42
|
+
});
|
|
43
|
+
const base64 = typeof shot === 'string' ? shot : shot?.data;
|
|
44
|
+
if (!base64) {
|
|
45
|
+
throw new Error('CDP screenshot failed: no image data was returned.');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const outputPath = kwargs.output || getDefaultOutputPath({
|
|
49
|
+
username: detail.username,
|
|
50
|
+
slug: detail.slug,
|
|
51
|
+
suffix: 'preview',
|
|
52
|
+
extension: 'png',
|
|
53
|
+
});
|
|
54
|
+
const savedPath = await saveBase64File(base64, outputPath);
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
username: detail.username,
|
|
58
|
+
slug: detail.slug,
|
|
59
|
+
url: detail.url,
|
|
60
|
+
output: savedPath,
|
|
61
|
+
width: Math.round(clip.width),
|
|
62
|
+
height: Math.round(clip.height),
|
|
63
|
+
x: Math.round(clip.x),
|
|
64
|
+
y: Math.round(clip.y),
|
|
65
|
+
selectorSource: located.best.source,
|
|
66
|
+
matchedTag: located.best.tag,
|
|
67
|
+
matchedClassName: located.best.className,
|
|
68
|
+
postId: detail.post.id,
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import { clampInt, requireNonEmptyQuery } from '../_shared/common.js';
|
|
3
|
+
|
|
4
|
+
cli({
|
|
5
|
+
site: 'wanfang',
|
|
6
|
+
name: 'search',
|
|
7
|
+
description: '万方数据论文搜索',
|
|
8
|
+
domain: 's.wanfangdata.com.cn',
|
|
9
|
+
strategy: Strategy.PUBLIC,
|
|
10
|
+
browser: true,
|
|
11
|
+
args: [
|
|
12
|
+
{ name: 'query', positional: true, required: true, help: '搜索关键词' },
|
|
13
|
+
{ name: 'limit', type: 'int', default: 10, help: '返回结果数量 (max 20)' },
|
|
14
|
+
],
|
|
15
|
+
columns: ['rank', 'title', 'authors', 'source', 'year', 'type', 'cited', 'url'],
|
|
16
|
+
navigateBefore: false,
|
|
17
|
+
func: async (page, kwargs) => {
|
|
18
|
+
const limit = clampInt(kwargs.limit, 10, 1, 20);
|
|
19
|
+
const query = requireNonEmptyQuery(kwargs.query);
|
|
20
|
+
await page.goto(`https://s.wanfangdata.com.cn/paper?q=${encodeURIComponent(query)}`);
|
|
21
|
+
await page.wait(5);
|
|
22
|
+
const data = await page.evaluate(`
|
|
23
|
+
(async () => {
|
|
24
|
+
const normalize = v => (v || '').replace(/\\s+/g, ' ').trim();
|
|
25
|
+
for (let i = 0; i < 30; i++) {
|
|
26
|
+
if (document.querySelectorAll('span.title').length > 0) break;
|
|
27
|
+
await new Promise(r => setTimeout(r, 500));
|
|
28
|
+
}
|
|
29
|
+
const results = [];
|
|
30
|
+
for (const titleSpan of document.querySelectorAll('span.title')) {
|
|
31
|
+
const title = normalize(titleSpan.textContent);
|
|
32
|
+
if (!title || title.length < 3) continue;
|
|
33
|
+
|
|
34
|
+
let container = titleSpan.parentElement;
|
|
35
|
+
for (let i = 0; i < 6; i++) {
|
|
36
|
+
if (!container?.parentElement || container.parentElement.tagName === 'BODY') break;
|
|
37
|
+
if (container.querySelectorAll('span.title').length >= 1 && container.querySelectorAll('span.authors').length >= 1) break;
|
|
38
|
+
container = container.parentElement;
|
|
39
|
+
}
|
|
40
|
+
if (!container) continue;
|
|
41
|
+
|
|
42
|
+
const id = normalize(container.querySelector('span.title-id-hidden')?.textContent);
|
|
43
|
+
const url = id ? 'https://d.wanfangdata.com.cn/' + id : '';
|
|
44
|
+
const authors = Array.from(container.querySelectorAll('span.authors'))
|
|
45
|
+
.map((item) => normalize(item.textContent))
|
|
46
|
+
.filter(Boolean)
|
|
47
|
+
.join(', ')
|
|
48
|
+
.slice(0, 80);
|
|
49
|
+
const type = normalize(container.querySelector('span.essay-type')?.textContent);
|
|
50
|
+
const source = normalize(container.querySelector('span.periodical, span.source')?.textContent);
|
|
51
|
+
|
|
52
|
+
let year = normalize(container.querySelector('span.year, span.date')?.textContent);
|
|
53
|
+
if (!year) year = (container.textContent || '').match(/(19|20)\\d{2}/)?.[0] || '';
|
|
54
|
+
|
|
55
|
+
const citedText = normalize(container.querySelector('.stat-item.quote, [class*=\"quote\"]')?.textContent);
|
|
56
|
+
const cited = citedText.match(/(\\d+)/)?.[1] || '0';
|
|
57
|
+
|
|
58
|
+
results.push({ rank: results.length + 1, title, authors, source, year, type, cited, url });
|
|
59
|
+
if (results.length >= ${limit}) break;
|
|
60
|
+
}
|
|
61
|
+
return results;
|
|
62
|
+
})()
|
|
63
|
+
`);
|
|
64
|
+
return Array.isArray(data) ? data : [];
|
|
65
|
+
},
|
|
66
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { getRegistry } from '@jackwener/opencli/registry';
|
|
3
|
+
import './search.js';
|
|
4
|
+
|
|
5
|
+
describe('wanfang search command', () => {
|
|
6
|
+
const command = getRegistry().get('wanfang/search');
|
|
7
|
+
|
|
8
|
+
it('registers as a public browser command', () => {
|
|
9
|
+
expect(command).toBeDefined();
|
|
10
|
+
expect(command.site).toBe('wanfang');
|
|
11
|
+
expect(command.strategy).toBe('public');
|
|
12
|
+
expect(command.browser).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('rejects empty queries before browser navigation', async () => {
|
|
16
|
+
const page = { goto: vi.fn() };
|
|
17
|
+
await expect(command.func(page, { query: ' ' })).rejects.toMatchObject({
|
|
18
|
+
name: 'ArgumentError',
|
|
19
|
+
code: 'ARGUMENT',
|
|
20
|
+
});
|
|
21
|
+
expect(page.goto).not.toHaveBeenCalled();
|
|
22
|
+
});
|
|
23
|
+
});
|
package/clis/web/read.js
CHANGED
|
@@ -27,7 +27,7 @@ cli({
|
|
|
27
27
|
{ name: 'download-images', type: 'boolean', default: true, help: 'Download images locally' },
|
|
28
28
|
{ name: 'wait', type: 'int', default: 3, help: 'Seconds to wait after page load' },
|
|
29
29
|
],
|
|
30
|
-
columns: ['title', 'author', 'publish_time', 'status', 'size'],
|
|
30
|
+
columns: ['title', 'author', 'publish_time', 'status', 'size', 'saved'],
|
|
31
31
|
func: async (page, kwargs) => {
|
|
32
32
|
const url = kwargs.url;
|
|
33
33
|
const waitSeconds = kwargs.wait ?? 3;
|
package/clis/weixin/download.js
CHANGED
|
@@ -179,12 +179,12 @@ cli({
|
|
|
179
179
|
{ name: 'output', default: './weixin-articles', help: 'Output directory' },
|
|
180
180
|
{ name: 'download-images', type: 'boolean', default: true, help: 'Download images locally' },
|
|
181
181
|
],
|
|
182
|
-
columns: ['title', 'author', 'publish_time', 'status', 'size'],
|
|
182
|
+
columns: ['title', 'author', 'publish_time', 'status', 'size', 'saved'],
|
|
183
183
|
func: async (page, kwargs) => {
|
|
184
184
|
const rawUrl = kwargs.url;
|
|
185
185
|
const url = normalizeWechatUrl(rawUrl);
|
|
186
186
|
if (!url.startsWith('https://mp.weixin.qq.com/')) {
|
|
187
|
-
return [{ title: 'Error', author: '-', publish_time: '-', status: 'invalid URL', size: '-' }];
|
|
187
|
+
return [{ title: 'Error', author: '-', publish_time: '-', status: 'invalid URL', size: '-', saved: '-' }];
|
|
188
188
|
}
|
|
189
189
|
// Navigate and wait for content to load
|
|
190
190
|
await page.goto(url);
|
|
@@ -297,6 +297,7 @@ cli({
|
|
|
297
297
|
publish_time: '-',
|
|
298
298
|
status: 'failed — verification required in WeChat browser page',
|
|
299
299
|
size: '-',
|
|
300
|
+
saved: '-',
|
|
300
301
|
}];
|
|
301
302
|
}
|
|
302
303
|
return downloadArticle({
|
|
@@ -22,7 +22,7 @@ cli({
|
|
|
22
22
|
strategy: Strategy.COOKIE,
|
|
23
23
|
navigateBefore: false,
|
|
24
24
|
args: [
|
|
25
|
-
{ name: 'note-id', required: true, positional: true, help: '
|
|
25
|
+
{ name: 'note-id', required: true, positional: true, help: 'Full Xiaohongshu note URL with xsec_token' },
|
|
26
26
|
{ name: 'limit', type: 'int', default: 20, help: 'Number of top-level comments (max 50)' },
|
|
27
27
|
{ name: 'with-replies', type: 'boolean', default: false, help: 'Include nested replies (楼中楼)' },
|
|
28
28
|
],
|
|
@@ -32,7 +32,7 @@ cli({
|
|
|
32
32
|
const withReplies = Boolean(kwargs['with-replies']);
|
|
33
33
|
const raw = String(kwargs['note-id']);
|
|
34
34
|
const noteId = parseNoteId(raw);
|
|
35
|
-
await page.goto(buildNoteUrl(raw));
|
|
35
|
+
await page.goto(buildNoteUrl(raw, { commandName: 'xiaohongshu comments' }));
|
|
36
36
|
await page.wait({ time: 2 + Math.random() * 3 });
|
|
37
37
|
const data = await page.evaluate(`
|
|
38
38
|
(async () => {
|