@jackwener/opencli 1.5.5 → 1.5.6
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 +27 -2
- package/README.zh-CN.md +36 -4
- package/dist/browser/daemon-client.d.ts +5 -1
- package/dist/browser/page.d.ts +6 -0
- package/dist/browser/page.js +15 -0
- package/dist/cli-manifest.json +1229 -67
- package/dist/clis/band/bands.d.ts +1 -0
- package/dist/clis/band/bands.js +72 -0
- package/dist/clis/band/mentions.d.ts +1 -0
- package/dist/clis/band/mentions.js +127 -0
- package/dist/clis/band/post.d.ts +1 -0
- package/dist/clis/band/post.js +175 -0
- package/dist/clis/band/posts.d.ts +1 -0
- package/dist/clis/band/posts.js +94 -0
- package/dist/clis/doubao/detail.d.ts +1 -0
- package/dist/clis/doubao/detail.js +33 -0
- package/dist/clis/doubao/detail.test.d.ts +1 -0
- package/dist/clis/doubao/detail.test.js +42 -0
- package/dist/clis/doubao/history.d.ts +1 -0
- package/dist/clis/doubao/history.js +28 -0
- package/dist/clis/doubao/history.test.d.ts +1 -0
- package/dist/clis/doubao/history.test.js +37 -0
- package/dist/clis/doubao/meeting-summary.d.ts +1 -0
- package/dist/clis/doubao/meeting-summary.js +39 -0
- package/dist/clis/doubao/meeting-transcript.d.ts +1 -0
- package/dist/clis/doubao/meeting-transcript.js +36 -0
- package/dist/clis/doubao/utils.d.ts +27 -0
- package/dist/clis/doubao/utils.js +317 -0
- package/dist/clis/doubao/utils.test.d.ts +1 -0
- package/dist/clis/doubao/utils.test.js +24 -0
- package/dist/clis/douyin/_shared/public-api.d.ts +33 -0
- package/dist/clis/douyin/_shared/public-api.js +29 -0
- package/dist/clis/douyin/user-videos.d.ts +5 -0
- package/dist/clis/douyin/user-videos.js +74 -0
- package/dist/clis/douyin/user-videos.test.d.ts +1 -0
- package/dist/clis/douyin/user-videos.test.js +108 -0
- package/dist/clis/ones/common.d.ts +32 -0
- package/dist/clis/ones/common.js +144 -0
- package/dist/clis/ones/enrich-tasks.d.ts +5 -0
- package/dist/clis/ones/enrich-tasks.js +37 -0
- package/dist/clis/ones/login.d.ts +1 -0
- package/dist/clis/ones/login.js +80 -0
- package/dist/clis/ones/logout.d.ts +1 -0
- package/dist/clis/ones/logout.js +17 -0
- package/dist/clis/ones/me.d.ts +1 -0
- package/dist/clis/ones/me.js +30 -0
- package/dist/clis/ones/my-tasks.d.ts +1 -0
- package/dist/clis/ones/my-tasks.js +120 -0
- package/dist/clis/ones/resolve-labels.d.ts +10 -0
- package/dist/clis/ones/resolve-labels.js +64 -0
- package/dist/clis/ones/task-helpers.d.ts +29 -0
- package/dist/clis/ones/task-helpers.js +212 -0
- package/dist/clis/ones/task-helpers.test.d.ts +1 -0
- package/dist/clis/ones/task-helpers.test.js +12 -0
- package/dist/clis/ones/task.d.ts +1 -0
- package/dist/clis/ones/task.js +66 -0
- package/dist/clis/ones/tasks.d.ts +1 -0
- package/dist/clis/ones/tasks.js +79 -0
- package/dist/clis/ones/token-info.d.ts +1 -0
- package/dist/clis/ones/token-info.js +42 -0
- package/dist/clis/ones/worklog.d.ts +11 -0
- package/dist/clis/ones/worklog.js +267 -0
- package/dist/clis/ones/worklog.test.d.ts +1 -0
- package/dist/clis/ones/worklog.test.js +20 -0
- package/dist/clis/spotify/spotify.d.ts +1 -0
- package/dist/clis/spotify/spotify.js +316 -0
- package/dist/clis/spotify/utils.d.ts +21 -0
- package/dist/clis/spotify/utils.js +66 -0
- package/dist/clis/spotify/utils.test.d.ts +1 -0
- package/dist/clis/spotify/utils.test.js +67 -0
- package/dist/clis/tieba/commands.test.d.ts +4 -0
- package/dist/clis/tieba/commands.test.js +79 -0
- package/dist/clis/tieba/hot.d.ts +1 -0
- package/dist/clis/tieba/hot.js +48 -0
- package/dist/clis/tieba/posts.d.ts +1 -0
- package/dist/clis/tieba/posts.js +85 -0
- package/dist/clis/tieba/read.d.ts +1 -0
- package/dist/clis/tieba/read.js +140 -0
- package/dist/clis/tieba/search.d.ts +1 -0
- package/dist/clis/tieba/search.js +108 -0
- package/dist/clis/tieba/utils.d.ts +101 -0
- package/dist/clis/tieba/utils.js +240 -0
- package/dist/clis/tieba/utils.test.d.ts +1 -0
- package/dist/clis/tieba/utils.test.js +290 -0
- package/dist/clis/weread/book.js +100 -13
- package/dist/clis/weread/commands.test.js +221 -0
- package/dist/clis/weread/private-api-regression.test.d.ts +1 -0
- package/dist/{weread-private-api-regression.test.js → clis/weread/private-api-regression.test.js} +92 -30
- package/dist/clis/weread/search-regression.test.d.ts +1 -0
- package/dist/clis/weread/search-regression.test.js +407 -0
- package/dist/clis/weread/search.js +143 -7
- package/dist/clis/weread/shelf.js +13 -95
- package/dist/clis/weread/utils.d.ts +46 -0
- package/dist/clis/weread/utils.js +214 -7
- package/dist/clis/weread/utils.test.js +71 -1
- package/dist/clis/xiaohongshu/publish.d.ts +1 -1
- package/dist/clis/xiaohongshu/publish.js +78 -31
- package/dist/clis/xiaohongshu/publish.test.js +66 -1
- package/dist/clis/xiaohongshu/user-helpers.d.ts +1 -0
- package/dist/clis/xiaohongshu/user-helpers.js +2 -0
- package/dist/clis/xiaohongshu/user-helpers.test.js +18 -0
- package/dist/clis/xueqiu/comments.d.ts +118 -0
- package/dist/clis/xueqiu/comments.js +354 -0
- package/dist/clis/xueqiu/comments.test.d.ts +1 -0
- package/dist/clis/xueqiu/comments.test.js +696 -0
- package/dist/clis/youtube/transcript.js +2 -4
- package/dist/clis/youtube/utils.d.ts +9 -0
- package/dist/clis/youtube/utils.js +67 -3
- package/dist/clis/youtube/utils.test.d.ts +1 -0
- package/dist/clis/youtube/utils.test.js +37 -0
- package/dist/clis/youtube/video.js +16 -15
- package/dist/clis/zsxq/dynamics.d.ts +1 -0
- package/dist/clis/zsxq/dynamics.js +47 -0
- package/dist/clis/zsxq/groups.d.ts +1 -0
- package/dist/clis/zsxq/groups.js +32 -0
- package/dist/clis/zsxq/search.d.ts +1 -0
- package/dist/clis/zsxq/search.js +43 -0
- package/dist/clis/zsxq/search.test.d.ts +1 -0
- package/dist/clis/zsxq/search.test.js +24 -0
- package/dist/clis/zsxq/topic.d.ts +1 -0
- package/dist/clis/zsxq/topic.js +47 -0
- package/dist/clis/zsxq/topic.test.d.ts +1 -0
- package/dist/clis/zsxq/topic.test.js +29 -0
- package/dist/clis/zsxq/topics.d.ts +1 -0
- package/dist/clis/zsxq/topics.js +25 -0
- package/dist/clis/zsxq/topics.test.d.ts +1 -0
- package/dist/clis/zsxq/topics.test.js +24 -0
- package/dist/clis/zsxq/utils.d.ts +97 -0
- package/dist/clis/zsxq/utils.js +230 -0
- package/dist/commanderAdapter.js +1 -1
- package/dist/commanderAdapter.test.js +39 -0
- package/dist/external-clis.yaml +17 -0
- package/dist/types.d.ts +5 -0
- package/docs/.vitepress/config.mts +3 -0
- package/docs/adapters/browser/band.md +63 -0
- package/docs/adapters/browser/ones.md +59 -0
- package/docs/adapters/browser/spotify.md +62 -0
- package/docs/adapters/browser/tieba.md +45 -0
- package/docs/adapters/browser/xueqiu.md +5 -0
- package/docs/adapters/browser/zsxq.md +49 -0
- package/docs/adapters/index.md +5 -2
- package/docs/adapters-doc/ones.md +32 -0
- package/extension/src/background.ts +15 -0
- package/extension/src/cdp.ts +42 -0
- package/extension/src/protocol.ts +5 -1
- package/package.json +1 -1
- package/scripts/postinstall.js +16 -0
- package/src/browser/daemon-client.ts +5 -1
- package/src/browser/page.ts +16 -0
- package/src/clis/band/bands.ts +76 -0
- package/src/clis/band/mentions.ts +134 -0
- package/src/clis/band/post.ts +187 -0
- package/src/clis/band/posts.ts +106 -0
- package/src/clis/doubao/detail.test.ts +53 -0
- package/src/clis/doubao/detail.ts +41 -0
- package/src/clis/doubao/history.test.ts +45 -0
- package/src/clis/doubao/history.ts +32 -0
- package/src/clis/doubao/meeting-summary.ts +53 -0
- package/src/clis/doubao/meeting-transcript.ts +48 -0
- package/src/clis/doubao/utils.test.ts +45 -0
- package/src/clis/doubao/utils.ts +371 -0
- package/src/clis/douyin/_shared/public-api.ts +84 -0
- package/src/clis/douyin/user-videos.test.ts +122 -0
- package/src/clis/douyin/user-videos.ts +101 -0
- package/src/clis/ones/common.ts +187 -0
- package/src/clis/ones/enrich-tasks.ts +47 -0
- package/src/clis/ones/login.ts +103 -0
- package/src/clis/ones/logout.ts +19 -0
- package/src/clis/ones/me.ts +34 -0
- package/src/clis/ones/my-tasks.ts +148 -0
- package/src/clis/ones/resolve-labels.ts +80 -0
- package/src/clis/ones/task-helpers.test.ts +14 -0
- package/src/clis/ones/task-helpers.ts +214 -0
- package/src/clis/ones/task.ts +79 -0
- package/src/clis/ones/tasks.ts +92 -0
- package/src/clis/ones/token-info.ts +46 -0
- package/src/clis/ones/worklog.test.ts +24 -0
- package/src/clis/ones/worklog.ts +306 -0
- package/src/clis/spotify/spotify.ts +328 -0
- package/src/clis/spotify/utils.test.ts +87 -0
- package/src/clis/spotify/utils.ts +92 -0
- package/src/clis/tieba/commands.test.ts +86 -0
- package/src/clis/tieba/hot.ts +52 -0
- package/src/clis/tieba/posts.ts +108 -0
- package/src/clis/tieba/read.ts +158 -0
- package/src/clis/tieba/search.ts +119 -0
- package/src/clis/tieba/utils.test.ts +322 -0
- package/src/clis/tieba/utils.ts +348 -0
- package/src/clis/weread/book.ts +116 -13
- package/src/clis/weread/commands.test.ts +249 -0
- package/src/{weread-private-api-regression.test.ts → clis/weread/private-api-regression.test.ts} +108 -30
- package/src/clis/weread/search-regression.test.ts +440 -0
- package/src/clis/weread/search.ts +189 -9
- package/src/clis/weread/shelf.ts +20 -122
- package/src/clis/weread/utils.test.ts +81 -1
- package/src/clis/weread/utils.ts +264 -7
- package/src/clis/xiaohongshu/publish.test.ts +79 -1
- package/src/clis/xiaohongshu/publish.ts +84 -30
- package/src/clis/xiaohongshu/user-helpers.test.ts +23 -0
- package/src/clis/xiaohongshu/user-helpers.ts +4 -0
- package/src/clis/xueqiu/comments.test.ts +823 -0
- package/src/clis/xueqiu/comments.ts +461 -0
- package/src/clis/youtube/transcript.ts +2 -4
- package/src/clis/youtube/utils.test.ts +43 -0
- package/src/clis/youtube/utils.ts +69 -0
- package/src/clis/youtube/video.ts +16 -15
- package/src/clis/zsxq/dynamics.ts +60 -0
- package/src/clis/zsxq/groups.ts +41 -0
- package/src/clis/zsxq/search.test.ts +29 -0
- package/src/clis/zsxq/search.ts +54 -0
- package/src/clis/zsxq/topic.test.ts +34 -0
- package/src/clis/zsxq/topic.ts +68 -0
- package/src/clis/zsxq/topics.test.ts +29 -0
- package/src/clis/zsxq/topics.ts +36 -0
- package/src/clis/zsxq/utils.ts +351 -0
- package/src/commanderAdapter.test.ts +47 -0
- package/src/commanderAdapter.ts +1 -1
- package/src/external-clis.yaml +17 -0
- package/src/types.ts +5 -0
- package/tests/e2e/band-auth.test.ts +20 -0
- package/tests/e2e/browser-auth-helpers.ts +18 -0
- package/tests/e2e/browser-auth.test.ts +35 -47
- package/tests/e2e/browser-public.test.ts +288 -0
- package/tests/e2e/management.test.ts +1 -1
- package/tests/e2e/plugin-management.test.ts +1 -1
- package/vitest.config.ts +1 -0
- package/SKILL.md +0 -879
- package/dist/weread-private-api-regression.test.d.ts +0 -1
- package/dist/weread-search-regression.test.d.ts +0 -1
- package/dist/weread-search-regression.test.js +0 -39
- package/src/weread-search-regression.test.ts +0 -44
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Flow:
|
|
5
5
|
* 1. Navigate to creator publish page
|
|
6
|
-
* 2. Upload images via
|
|
6
|
+
* 2. Upload images via CDP DOM.setFileInputFiles (with base64 fallback)
|
|
7
7
|
* 3. Fill title and body text
|
|
8
8
|
* 4. Add topic hashtags
|
|
9
9
|
* 5. Publish (or save as draft)
|
|
@@ -37,43 +37,90 @@ const TITLE_SELECTORS = [
|
|
|
37
37
|
'.note-title input',
|
|
38
38
|
'input[maxlength]',
|
|
39
39
|
];
|
|
40
|
+
const SUPPORTED_EXTENSIONS = {
|
|
41
|
+
'.jpg': 'image/jpeg',
|
|
42
|
+
'.jpeg': 'image/jpeg',
|
|
43
|
+
'.png': 'image/png',
|
|
44
|
+
'.gif': 'image/gif',
|
|
45
|
+
'.webp': 'image/webp',
|
|
46
|
+
};
|
|
40
47
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
48
|
+
* Validate image paths: check existence and extension.
|
|
49
|
+
* Returns resolved absolute paths.
|
|
43
50
|
*/
|
|
44
|
-
function
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
};
|
|
56
|
-
const mimeType = mimeMap[ext];
|
|
57
|
-
if (!mimeType)
|
|
58
|
-
throw new Error(`Unsupported image format "${ext}". Supported: jpg, png, gif, webp`);
|
|
59
|
-
const base64 = fs.readFileSync(absPath).toString('base64');
|
|
60
|
-
return { name: path.basename(absPath), mimeType, base64 };
|
|
51
|
+
function validateImagePaths(filePaths) {
|
|
52
|
+
return filePaths.map((filePath) => {
|
|
53
|
+
const absPath = path.resolve(filePath);
|
|
54
|
+
if (!fs.existsSync(absPath))
|
|
55
|
+
throw new Error(`Image file not found: ${absPath}`);
|
|
56
|
+
const ext = path.extname(absPath).toLowerCase();
|
|
57
|
+
if (!SUPPORTED_EXTENSIONS[ext]) {
|
|
58
|
+
throw new Error(`Unsupported image format "${ext}". Supported: jpg, png, gif, webp`);
|
|
59
|
+
}
|
|
60
|
+
return absPath;
|
|
61
|
+
});
|
|
61
62
|
}
|
|
63
|
+
/** CSS selector for image-accepting file inputs. */
|
|
64
|
+
const IMAGE_INPUT_SELECTOR = 'input[type="file"][accept*="image"],'
|
|
65
|
+
+ 'input[type="file"][accept*=".jpg"],'
|
|
66
|
+
+ 'input[type="file"][accept*=".jpeg"],'
|
|
67
|
+
+ 'input[type="file"][accept*=".png"],'
|
|
68
|
+
+ 'input[type="file"][accept*=".gif"],'
|
|
69
|
+
+ 'input[type="file"][accept*=".webp"]';
|
|
62
70
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* a synthetic 'change' event on the input element.
|
|
71
|
+
* Upload images via CDP DOM.setFileInputFiles — Chrome reads files directly
|
|
72
|
+
* from the local filesystem, avoiding base64 payload size limits.
|
|
66
73
|
*
|
|
67
|
-
*
|
|
74
|
+
* Falls back to the legacy base64 DataTransfer approach if the extension
|
|
75
|
+
* does not support set-file-input (e.g. older extension version).
|
|
68
76
|
*/
|
|
69
|
-
async function
|
|
77
|
+
async function uploadImages(page, absPaths) {
|
|
78
|
+
// ── Primary: CDP DOM.setFileInputFiles ──────────────────────────────
|
|
79
|
+
if (page.setFileInput) {
|
|
80
|
+
try {
|
|
81
|
+
// Find image-accepting file input on the page
|
|
82
|
+
const selector = await page.evaluate(`
|
|
83
|
+
(() => {
|
|
84
|
+
const sels = ${JSON.stringify(IMAGE_INPUT_SELECTOR)};
|
|
85
|
+
const el = document.querySelector(sels);
|
|
86
|
+
return el ? sels : null;
|
|
87
|
+
})()
|
|
88
|
+
`);
|
|
89
|
+
if (!selector) {
|
|
90
|
+
return { ok: false, count: 0, error: 'No file input found on page' };
|
|
91
|
+
}
|
|
92
|
+
await page.setFileInput(absPaths, selector);
|
|
93
|
+
return { ok: true, count: absPaths.length };
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
// If set-file-input action is not supported by extension, fall through to legacy
|
|
97
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
98
|
+
if (msg.includes('Unknown action') || msg.includes('not supported')) {
|
|
99
|
+
// Extension too old — fall through to legacy base64 method
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
return { ok: false, count: 0, error: msg };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// ── Fallback: legacy base64 DataTransfer injection ─────────────────
|
|
107
|
+
const images = absPaths.map((absPath) => {
|
|
108
|
+
const base64 = fs.readFileSync(absPath).toString('base64');
|
|
109
|
+
const ext = path.extname(absPath).toLowerCase();
|
|
110
|
+
return { name: path.basename(absPath), mimeType: SUPPORTED_EXTENSIONS[ext], base64 };
|
|
111
|
+
});
|
|
112
|
+
// Warn if total payload is large — this may fail with older extensions
|
|
113
|
+
const totalBytes = images.reduce((sum, img) => sum + img.base64.length, 0);
|
|
114
|
+
if (totalBytes > 500_000) {
|
|
115
|
+
console.warn(`[warn] Total image payload is ${(totalBytes / 1024 / 1024).toFixed(1)}MB (base64). ` +
|
|
116
|
+
'This may fail with the browser bridge. Update the extension to v1.6+ for CDP-based upload, ' +
|
|
117
|
+
'or compress images before publishing.');
|
|
118
|
+
}
|
|
70
119
|
const payload = JSON.stringify(images);
|
|
71
120
|
return page.evaluate(`
|
|
72
121
|
(async () => {
|
|
73
122
|
const images = ${payload};
|
|
74
123
|
|
|
75
|
-
// Only use image-capable file inputs. Do not fall back to a generic uploader,
|
|
76
|
-
// otherwise we can accidentally feed images into the video upload flow.
|
|
77
124
|
const inputs = Array.from(document.querySelectorAll('input[type="file"]'));
|
|
78
125
|
const input = inputs.find(el => {
|
|
79
126
|
const accept = el.getAttribute('accept') || '';
|
|
@@ -317,8 +364,8 @@ cli({
|
|
|
317
364
|
throw new Error('At least one --images path is required. The creator center now requires images before showing the editor.');
|
|
318
365
|
if (imagePaths.length > MAX_IMAGES)
|
|
319
366
|
throw new Error(`Too many images: ${imagePaths.length} (max ${MAX_IMAGES})`);
|
|
320
|
-
//
|
|
321
|
-
const
|
|
367
|
+
// Validate image paths before navigating (fast-fail on bad paths / unsupported formats)
|
|
368
|
+
const absImagePaths = validateImagePaths(imagePaths);
|
|
322
369
|
// ── Step 1: Navigate to publish page ──────────────────────────────────────
|
|
323
370
|
await page.goto(PUBLISH_URL);
|
|
324
371
|
await page.wait({ time: 3 });
|
|
@@ -340,7 +387,7 @@ cli({
|
|
|
340
387
|
`Details: ${detail}. Debug screenshot: /tmp/xhs_publish_tab_debug.png`);
|
|
341
388
|
}
|
|
342
389
|
// ── Step 3: Upload images ──────────────────────────────────────────────────
|
|
343
|
-
const upload = await
|
|
390
|
+
const upload = await uploadImages(page, absImagePaths);
|
|
344
391
|
if (!upload.ok) {
|
|
345
392
|
await page.screenshot({ path: '/tmp/xhs_publish_upload_debug.png' });
|
|
346
393
|
throw new Error(`Image injection failed: ${upload.error ?? 'unknown'}. ` +
|
|
@@ -472,7 +519,7 @@ cli({
|
|
|
472
519
|
status: isSuccess ? `✅ ${verb}` : '⚠️ 操作完成,请在浏览器中确认',
|
|
473
520
|
detail: [
|
|
474
521
|
`"${title}"`,
|
|
475
|
-
`${
|
|
522
|
+
`${absImagePaths.length}张图片`,
|
|
476
523
|
topics.length ? `话题: ${topics.join(' ')}` : '',
|
|
477
524
|
successMsg || finalUrl || '',
|
|
478
525
|
]
|
|
@@ -4,7 +4,7 @@ import * as path from 'node:path';
|
|
|
4
4
|
import { describe, expect, it, vi } from 'vitest';
|
|
5
5
|
import { getRegistry } from '../../registry.js';
|
|
6
6
|
import './publish.js';
|
|
7
|
-
function createPageMock(evaluateResults) {
|
|
7
|
+
function createPageMock(evaluateResults, overrides = {}) {
|
|
8
8
|
const evaluate = vi.fn();
|
|
9
9
|
for (const result of evaluateResults) {
|
|
10
10
|
evaluate.mockResolvedValueOnce(result);
|
|
@@ -32,9 +32,74 @@ function createPageMock(evaluateResults) {
|
|
|
32
32
|
getCookies: vi.fn().mockResolvedValue([]),
|
|
33
33
|
screenshot: vi.fn().mockResolvedValue(''),
|
|
34
34
|
waitForCapture: vi.fn().mockResolvedValue(undefined),
|
|
35
|
+
...overrides,
|
|
35
36
|
};
|
|
36
37
|
}
|
|
37
38
|
describe('xiaohongshu publish', () => {
|
|
39
|
+
it('prefers CDP setFileInput upload when the page supports it', async () => {
|
|
40
|
+
const cmd = getRegistry().get('xiaohongshu/publish');
|
|
41
|
+
expect(cmd?.func).toBeTypeOf('function');
|
|
42
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-xhs-publish-'));
|
|
43
|
+
const imagePath = path.join(tempDir, 'demo.jpg');
|
|
44
|
+
fs.writeFileSync(imagePath, Buffer.from([0xff, 0xd8, 0xff, 0xd9]));
|
|
45
|
+
const setFileInput = vi.fn().mockResolvedValue(undefined);
|
|
46
|
+
const page = createPageMock([
|
|
47
|
+
'https://creator.xiaohongshu.com/publish/publish?from=menu_left',
|
|
48
|
+
{ ok: true, target: '上传图文', text: '上传图文' },
|
|
49
|
+
{ state: 'editor_ready', hasTitleInput: true, hasImageInput: true, hasVideoSurface: false },
|
|
50
|
+
'input[type="file"][accept*="image"],input[type="file"][accept*=".jpg"],input[type="file"][accept*=".jpeg"],input[type="file"][accept*=".png"],input[type="file"][accept*=".gif"],input[type="file"][accept*=".webp"]',
|
|
51
|
+
false,
|
|
52
|
+
true,
|
|
53
|
+
{ ok: true, sel: 'input[maxlength="20"]' },
|
|
54
|
+
{ ok: true, sel: '[contenteditable="true"][class*="content"]' },
|
|
55
|
+
true,
|
|
56
|
+
'https://creator.xiaohongshu.com/publish/success',
|
|
57
|
+
'发布成功',
|
|
58
|
+
], {
|
|
59
|
+
setFileInput,
|
|
60
|
+
});
|
|
61
|
+
const result = await cmd.func(page, {
|
|
62
|
+
title: 'CDP上传优先',
|
|
63
|
+
content: '优先走 setFileInput 主路径',
|
|
64
|
+
images: imagePath,
|
|
65
|
+
topics: '',
|
|
66
|
+
draft: false,
|
|
67
|
+
});
|
|
68
|
+
expect(setFileInput).toHaveBeenCalledWith([imagePath], expect.stringContaining('input[type="file"][accept*="image"]'));
|
|
69
|
+
const evaluateCalls = page.evaluate.mock.calls.map((args) => String(args[0]));
|
|
70
|
+
expect(evaluateCalls.some((code) => code.includes('atob(img.base64)'))).toBe(false);
|
|
71
|
+
expect(result).toEqual([
|
|
72
|
+
{
|
|
73
|
+
status: '✅ 发布成功',
|
|
74
|
+
detail: '"CDP上传优先" · 1张图片 · 发布成功',
|
|
75
|
+
},
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
it('fails fast when only a generic file input exists on the page', async () => {
|
|
79
|
+
const cmd = getRegistry().get('xiaohongshu/publish');
|
|
80
|
+
expect(cmd?.func).toBeTypeOf('function');
|
|
81
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-xhs-publish-'));
|
|
82
|
+
const imagePath = path.join(tempDir, 'demo.jpg');
|
|
83
|
+
fs.writeFileSync(imagePath, Buffer.from([0xff, 0xd8, 0xff, 0xd9]));
|
|
84
|
+
const setFileInput = vi.fn().mockResolvedValue(undefined);
|
|
85
|
+
const page = createPageMock([
|
|
86
|
+
'https://creator.xiaohongshu.com/publish/publish?from=menu_left',
|
|
87
|
+
{ ok: true, target: '上传图文', text: '上传图文' },
|
|
88
|
+
{ state: 'editor_ready', hasTitleInput: true, hasImageInput: true, hasVideoSurface: false },
|
|
89
|
+
null,
|
|
90
|
+
], {
|
|
91
|
+
setFileInput,
|
|
92
|
+
});
|
|
93
|
+
await expect(cmd.func(page, {
|
|
94
|
+
title: '不要走泛化上传',
|
|
95
|
+
content: 'generic file input 应该直接报错',
|
|
96
|
+
images: imagePath,
|
|
97
|
+
topics: '',
|
|
98
|
+
draft: false,
|
|
99
|
+
})).rejects.toThrow('Image injection failed: No file input found on page');
|
|
100
|
+
expect(setFileInput).not.toHaveBeenCalled();
|
|
101
|
+
expect(page.screenshot).toHaveBeenCalledWith({ path: '/tmp/xhs_publish_upload_debug.png' });
|
|
102
|
+
});
|
|
38
103
|
it('selects the image-text tab and publishes successfully', async () => {
|
|
39
104
|
const cmd = getRegistry().get('xiaohongshu/publish');
|
|
40
105
|
expect(cmd?.func).toBeTypeOf('function');
|
|
@@ -55,11 +55,13 @@ export function extractXhsUserNotes(snapshot, fallbackUserId) {
|
|
|
55
55
|
const userId = toCleanString(noteCard.user?.userId ?? noteCard.user?.user_id ?? fallbackUserId);
|
|
56
56
|
const xsecToken = toCleanString(entry?.xsecToken ?? entry?.xsec_token ?? noteCard.xsecToken ?? noteCard.xsec_token);
|
|
57
57
|
const likes = toCleanString(noteCard.interactInfo?.likedCount ?? noteCard.interact_info?.liked_count ?? 0) || '0';
|
|
58
|
+
const cover = toCleanString(noteCard.cover?.urlDefault ?? noteCard.cover?.urlPre ?? noteCard.cover?.url ?? '');
|
|
58
59
|
rows.push({
|
|
59
60
|
id: noteId,
|
|
60
61
|
title: toCleanString(noteCard.displayTitle ?? noteCard.display_title ?? noteCard.title),
|
|
61
62
|
type: toCleanString(noteCard.type),
|
|
62
63
|
likes,
|
|
64
|
+
cover,
|
|
63
65
|
url: buildXhsNoteUrl(userId || fallbackUserId, noteId, xsecToken),
|
|
64
66
|
});
|
|
65
67
|
}
|
|
@@ -55,6 +55,7 @@ describe('extractXhsUserNotes', () => {
|
|
|
55
55
|
title: 'First note',
|
|
56
56
|
type: 'video',
|
|
57
57
|
likes: '4.6万',
|
|
58
|
+
cover: '',
|
|
58
59
|
url: 'https://www.xiaohongshu.com/user/profile/user-1/note-1?xsec_token=abc&xsec_source=pc_user',
|
|
59
60
|
},
|
|
60
61
|
{
|
|
@@ -62,10 +63,27 @@ describe('extractXhsUserNotes', () => {
|
|
|
62
63
|
title: 'Second note',
|
|
63
64
|
type: 'normal',
|
|
64
65
|
likes: '42',
|
|
66
|
+
cover: '',
|
|
65
67
|
url: 'https://www.xiaohongshu.com/user/profile/fallback-user/note-2',
|
|
66
68
|
},
|
|
67
69
|
]);
|
|
68
70
|
});
|
|
71
|
+
it('extracts cover urls with fallback priority urlDefault -> urlPre -> url', () => {
|
|
72
|
+
const rows = extractXhsUserNotes({
|
|
73
|
+
noteGroups: [
|
|
74
|
+
[
|
|
75
|
+
{ noteCard: { noteId: 'cover-1', cover: { urlDefault: 'https://img.example/default.jpg', urlPre: 'https://img.example/pre.jpg', url: 'https://img.example/raw.jpg' } } },
|
|
76
|
+
{ noteCard: { noteId: 'cover-2', cover: { urlPre: 'https://img.example/pre-only.jpg', url: 'https://img.example/raw-only.jpg' } } },
|
|
77
|
+
{ noteCard: { noteId: 'cover-3', cover: { url: 'https://img.example/raw-fallback.jpg' } } },
|
|
78
|
+
],
|
|
79
|
+
],
|
|
80
|
+
}, 'fallback-user');
|
|
81
|
+
expect(rows.map(row => row.cover)).toEqual([
|
|
82
|
+
'https://img.example/default.jpg',
|
|
83
|
+
'https://img.example/pre-only.jpg',
|
|
84
|
+
'https://img.example/raw-fallback.jpg',
|
|
85
|
+
]);
|
|
86
|
+
});
|
|
69
87
|
it('deduplicates repeated notes by note id', () => {
|
|
70
88
|
const rows = extractXhsUserNotes({
|
|
71
89
|
noteGroups: [
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { IPage } from '../../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Minimal browser-response shape used by the classifier.
|
|
4
|
+
*/
|
|
5
|
+
export interface XueqiuCommentsResponse {
|
|
6
|
+
status: number;
|
|
7
|
+
contentType: string;
|
|
8
|
+
json: unknown;
|
|
9
|
+
textSnippet: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Minimal normalized row shape used during pagination and deduplication.
|
|
13
|
+
*/
|
|
14
|
+
export interface XueqiuCommentRow {
|
|
15
|
+
id: string;
|
|
16
|
+
author: string;
|
|
17
|
+
text?: string;
|
|
18
|
+
likes?: number;
|
|
19
|
+
replies?: number;
|
|
20
|
+
retweets?: number;
|
|
21
|
+
created_at?: string | null;
|
|
22
|
+
url?: string | null;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Public CLI row shape. This intentionally omits the internal stable ID used
|
|
26
|
+
* only for deduplication, so machine-readable output matches the command
|
|
27
|
+
* contract and table columns.
|
|
28
|
+
*/
|
|
29
|
+
export type XueqiuCommentOutputRow = Omit<XueqiuCommentRow, 'id'>;
|
|
30
|
+
/**
|
|
31
|
+
* Pagination options for collecting enough rows to satisfy `--limit`.
|
|
32
|
+
*/
|
|
33
|
+
export interface CollectCommentRowsOptions {
|
|
34
|
+
symbol: string;
|
|
35
|
+
limit: number;
|
|
36
|
+
pageSize: number;
|
|
37
|
+
maxRequests: number;
|
|
38
|
+
fetchPage: (pageNumber: number, pageSize: number) => Promise<XueqiuCommentsResponse>;
|
|
39
|
+
warn?: (message: string) => void;
|
|
40
|
+
}
|
|
41
|
+
type XueqiuCommentsKind = 'auth' | 'anti-bot' | 'argument' | 'empty' | 'incompatible' | 'unknown';
|
|
42
|
+
/**
|
|
43
|
+
* Extract the raw item list from one classified JSON payload.
|
|
44
|
+
*
|
|
45
|
+
* @param json Raw parsed JSON payload from browser fetch.
|
|
46
|
+
* @returns Discussion items when the response shape is usable.
|
|
47
|
+
*/
|
|
48
|
+
export declare function getCommentItems(json: unknown): Record<string, any>[];
|
|
49
|
+
/**
|
|
50
|
+
* Classify one raw browser response before command-level error handling.
|
|
51
|
+
*
|
|
52
|
+
* @param response Structured browser response payload.
|
|
53
|
+
* @returns Tagged result describing the response class.
|
|
54
|
+
*/
|
|
55
|
+
export declare function classifyXueqiuCommentsResponse(response: XueqiuCommentsResponse): {
|
|
56
|
+
kind: XueqiuCommentsKind;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Merge one new page of rows while preserving the first occurrence of each ID.
|
|
60
|
+
*
|
|
61
|
+
* @param current Rows already collected.
|
|
62
|
+
* @param incoming Rows from the next page.
|
|
63
|
+
* @returns Deduplicated merged rows.
|
|
64
|
+
*/
|
|
65
|
+
export declare function mergeUniqueCommentRows(current: XueqiuCommentRow[], incoming: XueqiuCommentRow[]): XueqiuCommentRow[];
|
|
66
|
+
/**
|
|
67
|
+
* Normalize one raw xueqiu discussion item into the CLI row shape.
|
|
68
|
+
*
|
|
69
|
+
* Returned rows represent stock-scoped discussion posts, not replies under
|
|
70
|
+
* one parent post.
|
|
71
|
+
*
|
|
72
|
+
* @param item Raw API item.
|
|
73
|
+
* @returns Cleaned CLI row.
|
|
74
|
+
*/
|
|
75
|
+
export declare function normalizeCommentItem(item: Record<string, any>): XueqiuCommentRow;
|
|
76
|
+
/**
|
|
77
|
+
* Remove internal-only fields before returning rows to the CLI renderer.
|
|
78
|
+
*
|
|
79
|
+
* @param row Internal row shape used during pagination.
|
|
80
|
+
* @returns Public output row that matches the documented command contract.
|
|
81
|
+
*/
|
|
82
|
+
export declare function toCommentOutputRow(row: XueqiuCommentRow): XueqiuCommentOutputRow;
|
|
83
|
+
/**
|
|
84
|
+
* Convert response classification into a compact warning phrase.
|
|
85
|
+
*
|
|
86
|
+
* @param kind Classifier result kind.
|
|
87
|
+
* @returns Human-readable reason fragment for stderr warnings.
|
|
88
|
+
*/
|
|
89
|
+
export declare function describeFailureKind(kind: XueqiuCommentsKind): string;
|
|
90
|
+
/**
|
|
91
|
+
* Fetch one discussion page from inside the browser context so cookies and
|
|
92
|
+
* any site-side request state stay attached to the request.
|
|
93
|
+
*
|
|
94
|
+
* @param page Active browser page.
|
|
95
|
+
* @param symbol Normalized stock symbol.
|
|
96
|
+
* @param pageNumber Internal page counter, starting from 1.
|
|
97
|
+
* @param pageSize Item count per internal request.
|
|
98
|
+
* @returns Structured response for command-side classification.
|
|
99
|
+
*/
|
|
100
|
+
export declare function fetchCommentsPage(page: IPage, symbol: string, pageNumber: number, pageSize: number): Promise<XueqiuCommentsResponse>;
|
|
101
|
+
/**
|
|
102
|
+
* Collect enough stock discussion rows to satisfy the requested limit.
|
|
103
|
+
*
|
|
104
|
+
* This helper owns the internal pagination policy so the public command
|
|
105
|
+
* contract can stay small and expose only `--limit`.
|
|
106
|
+
*
|
|
107
|
+
* @param options Pagination inputs and a page-fetch callback.
|
|
108
|
+
* @returns Deduplicated normalized rows, possibly partial with a warning.
|
|
109
|
+
*/
|
|
110
|
+
export declare function collectCommentRows(options: CollectCommentRowsOptions): Promise<XueqiuCommentRow[]>;
|
|
111
|
+
/**
|
|
112
|
+
* Convert raw CLI input into a normalized stock symbol.
|
|
113
|
+
*
|
|
114
|
+
* @param raw User-provided CLI argument.
|
|
115
|
+
* @returns Upper-cased symbol string.
|
|
116
|
+
*/
|
|
117
|
+
export declare function normalizeSymbolInput(raw: unknown): string;
|
|
118
|
+
export {};
|