@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
|
@@ -22,10 +22,10 @@ const PUBLISH_URL = 'https://creator.xiaohongshu.com/publish/publish?from=menu_l
|
|
|
22
22
|
const MAX_IMAGES = 9;
|
|
23
23
|
const MAX_TITLE_LEN = 20;
|
|
24
24
|
const UPLOAD_SETTLE_MS = 3000;
|
|
25
|
-
/** Selectors for the title field, ordered by priority
|
|
25
|
+
/** Selectors for the title field, ordered by priority across current UI variants. */
|
|
26
26
|
const TITLE_SELECTORS = [
|
|
27
|
-
//
|
|
28
|
-
//
|
|
27
|
+
// Some creator-center variants expose the title as contenteditable,
|
|
28
|
+
// others use a normal <input> with the same placeholder.
|
|
29
29
|
'[contenteditable="true"][placeholder*="标题"]',
|
|
30
30
|
'[contenteditable="true"][placeholder*="赞"]',
|
|
31
31
|
'[contenteditable="true"][class*="title"]',
|
|
@@ -180,36 +180,157 @@ async function waitForUploads(page, maxWaitMs = 30_000) {
|
|
|
180
180
|
* Returns { ok, sel }.
|
|
181
181
|
*/
|
|
182
182
|
async function fillField(page, selectors, text, fieldName) {
|
|
183
|
-
const
|
|
184
|
-
(function(selectors
|
|
183
|
+
const located = await page.evaluate(`
|
|
184
|
+
(function(selectors) {
|
|
185
|
+
const __opencli_xhs_fill_phase = "locate";
|
|
185
186
|
for (const sel of selectors) {
|
|
186
187
|
const candidates = document.querySelectorAll(sel);
|
|
187
188
|
for (const el of candidates) {
|
|
188
189
|
if (!el || el.offsetParent === null) continue;
|
|
189
|
-
el.
|
|
190
|
-
|
|
191
|
-
el.
|
|
192
|
-
|
|
193
|
-
document.execCommand('insertText', false, text);
|
|
194
|
-
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
195
|
-
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
196
|
-
} else {
|
|
197
|
-
// contenteditable
|
|
198
|
-
el.textContent = '';
|
|
199
|
-
document.execCommand('selectAll', false);
|
|
200
|
-
document.execCommand('insertText', false, text);
|
|
201
|
-
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
202
|
-
}
|
|
203
|
-
return { ok: true, sel };
|
|
190
|
+
const kind = el.isContentEditable
|
|
191
|
+
? 'contenteditable'
|
|
192
|
+
: (el.tagName === 'TEXTAREA' ? 'textarea' : 'input');
|
|
193
|
+
return { ok: true, sel, kind };
|
|
204
194
|
}
|
|
205
195
|
}
|
|
206
196
|
return { ok: false };
|
|
207
|
-
})(${JSON.stringify(selectors)}
|
|
197
|
+
})(${JSON.stringify(selectors)})
|
|
208
198
|
`);
|
|
209
|
-
if (!
|
|
199
|
+
if (!located.ok) {
|
|
210
200
|
await page.screenshot({ path: `/tmp/xhs_publish_${fieldName}_debug.png` });
|
|
211
201
|
throw new Error(`Could not find ${fieldName} input. Debug screenshot: /tmp/xhs_publish_${fieldName}_debug.png`);
|
|
212
202
|
}
|
|
203
|
+
const applyInPage = () => page.evaluate(`
|
|
204
|
+
((selector, expectedText) => {
|
|
205
|
+
const __opencli_xhs_fill_phase = "apply";
|
|
206
|
+
const normalize = (value) => (value || '').replace(/\\s+/g, ' ').trim();
|
|
207
|
+
const fireBeforeInput = (el, value) => {
|
|
208
|
+
try {
|
|
209
|
+
el.dispatchEvent(new InputEvent('beforeinput', {
|
|
210
|
+
bubbles: true,
|
|
211
|
+
data: value,
|
|
212
|
+
inputType: 'insertText',
|
|
213
|
+
}));
|
|
214
|
+
} catch {
|
|
215
|
+
el.dispatchEvent(new Event('beforeinput', { bubbles: true }));
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
const fireInput = (el, value) => {
|
|
219
|
+
try {
|
|
220
|
+
el.dispatchEvent(new InputEvent('input', {
|
|
221
|
+
bubbles: true,
|
|
222
|
+
data: value,
|
|
223
|
+
inputType: 'insertText',
|
|
224
|
+
}));
|
|
225
|
+
} catch {
|
|
226
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
const el = Array.from(document.querySelectorAll(selector)).find(node => node && node.offsetParent !== null);
|
|
230
|
+
if (!el) return { ok: false, actual: '' };
|
|
231
|
+
el.focus();
|
|
232
|
+
fireBeforeInput(el, expectedText);
|
|
233
|
+
if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
|
|
234
|
+
const proto = el.tagName === 'TEXTAREA'
|
|
235
|
+
? HTMLTextAreaElement.prototype
|
|
236
|
+
: HTMLInputElement.prototype;
|
|
237
|
+
const nativeSetter = Object.getOwnPropertyDescriptor(proto, 'value')?.set;
|
|
238
|
+
if (nativeSetter) nativeSetter.call(el, expectedText);
|
|
239
|
+
else el.value = expectedText;
|
|
240
|
+
fireInput(el, expectedText);
|
|
241
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
242
|
+
el.blur();
|
|
243
|
+
return { ok: el.value === expectedText, actual: el.value || '' };
|
|
244
|
+
}
|
|
245
|
+
el.textContent = '';
|
|
246
|
+
const selection = window.getSelection();
|
|
247
|
+
const range = document.createRange();
|
|
248
|
+
range.selectNodeContents(el);
|
|
249
|
+
range.collapse(false);
|
|
250
|
+
selection?.removeAllRanges();
|
|
251
|
+
selection?.addRange(range);
|
|
252
|
+
const inserted = document.execCommand('insertText', false, expectedText);
|
|
253
|
+
if (!inserted) el.textContent = expectedText;
|
|
254
|
+
fireInput(el, expectedText);
|
|
255
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
256
|
+
el.blur();
|
|
257
|
+
const actual = normalize(el.innerText || el.textContent || '');
|
|
258
|
+
return { ok: actual === normalize(expectedText), actual };
|
|
259
|
+
})(${JSON.stringify(located.sel)}, ${JSON.stringify(text)})
|
|
260
|
+
`);
|
|
261
|
+
let result;
|
|
262
|
+
if (located.kind === 'contenteditable' && page.insertText) {
|
|
263
|
+
const prepared = await page.evaluate(`
|
|
264
|
+
((selector, nextText) => {
|
|
265
|
+
const __opencli_xhs_fill_phase = "prepare";
|
|
266
|
+
const fireBeforeInput = (el, value) => {
|
|
267
|
+
try {
|
|
268
|
+
el.dispatchEvent(new InputEvent('beforeinput', {
|
|
269
|
+
bubbles: true,
|
|
270
|
+
data: value,
|
|
271
|
+
inputType: 'insertText',
|
|
272
|
+
}));
|
|
273
|
+
} catch {
|
|
274
|
+
el.dispatchEvent(new Event('beforeinput', { bubbles: true }));
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
const el = Array.from(document.querySelectorAll(selector)).find(node => node && node.offsetParent !== null);
|
|
278
|
+
if (!el) return { ok: false };
|
|
279
|
+
el.focus();
|
|
280
|
+
el.textContent = '';
|
|
281
|
+
const selection = window.getSelection();
|
|
282
|
+
const range = document.createRange();
|
|
283
|
+
range.selectNodeContents(el);
|
|
284
|
+
range.collapse(false);
|
|
285
|
+
selection?.removeAllRanges();
|
|
286
|
+
selection?.addRange(range);
|
|
287
|
+
fireBeforeInput(el, nextText);
|
|
288
|
+
return { ok: true };
|
|
289
|
+
})(${JSON.stringify(located.sel)}, ${JSON.stringify(text)})
|
|
290
|
+
`);
|
|
291
|
+
if (!prepared?.ok) {
|
|
292
|
+
await page.screenshot({ path: `/tmp/xhs_publish_${fieldName}_debug.png` });
|
|
293
|
+
throw new Error(`Could not prepare ${fieldName} input. Debug screenshot: /tmp/xhs_publish_${fieldName}_debug.png`);
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
await page.insertText(text);
|
|
297
|
+
result = await page.evaluate(`
|
|
298
|
+
((selector, expectedText) => {
|
|
299
|
+
const __opencli_xhs_fill_phase = "verify";
|
|
300
|
+
const normalize = (value) => (value || '').replace(/\\s+/g, ' ').trim();
|
|
301
|
+
const fireInput = (el, value) => {
|
|
302
|
+
try {
|
|
303
|
+
el.dispatchEvent(new InputEvent('input', {
|
|
304
|
+
bubbles: true,
|
|
305
|
+
data: value,
|
|
306
|
+
inputType: 'insertText',
|
|
307
|
+
}));
|
|
308
|
+
} catch {
|
|
309
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
const el = Array.from(document.querySelectorAll(selector)).find(node => node && node.offsetParent !== null);
|
|
313
|
+
if (!el) return { ok: false, actual: '' };
|
|
314
|
+
fireInput(el, expectedText);
|
|
315
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
316
|
+
el.blur();
|
|
317
|
+
const actual = normalize(el.innerText || el.textContent || '');
|
|
318
|
+
return { ok: actual === normalize(expectedText), actual };
|
|
319
|
+
})(${JSON.stringify(located.sel)}, ${JSON.stringify(text)})
|
|
320
|
+
`);
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
result = await applyInPage();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
result = await applyInPage();
|
|
328
|
+
}
|
|
329
|
+
if (!result?.ok) {
|
|
330
|
+
await page.screenshot({ path: `/tmp/xhs_publish_${fieldName}_debug.png` });
|
|
331
|
+
const actual = typeof result?.actual === 'string' ? result.actual : '';
|
|
332
|
+
throw new Error(`Failed to set ${fieldName}. Expected "${text}", got "${actual}". Debug screenshot: /tmp/xhs_publish_${fieldName}_debug.png`);
|
|
333
|
+
}
|
|
213
334
|
}
|
|
214
335
|
async function selectImageTextTab(page) {
|
|
215
336
|
const result = await page.evaluate(`
|
|
@@ -500,17 +621,17 @@ cli({
|
|
|
500
621
|
// ── Step 8: Verify success ─────────────────────────────────────────────────
|
|
501
622
|
await page.wait({ time: 4 });
|
|
502
623
|
const finalUrl = await page.evaluate('() => location.href');
|
|
624
|
+
const successMarkers = isDraft
|
|
625
|
+
? ['草稿已保存', '暂存成功', '保存成功', '上传成功']
|
|
626
|
+
: ['发布成功', '上传成功'];
|
|
503
627
|
const successMsg = await page.evaluate(`
|
|
504
|
-
(
|
|
628
|
+
(markers => {
|
|
505
629
|
for (const el of document.querySelectorAll('*')) {
|
|
506
630
|
const text = (el.innerText || '').trim();
|
|
507
|
-
if (
|
|
508
|
-
el.children.length === 0 &&
|
|
509
|
-
(text.includes('发布成功') || text.includes('草稿已保存') || text.includes('暂存成功') || text.includes('上传成功'))
|
|
510
|
-
) return text;
|
|
631
|
+
if (el.children.length === 0 && markers.some(marker => text.includes(marker))) return text;
|
|
511
632
|
}
|
|
512
633
|
return '';
|
|
513
|
-
}
|
|
634
|
+
})(${JSON.stringify(successMarkers)})
|
|
514
635
|
`);
|
|
515
636
|
const navigatedAway = !finalUrl.includes('/publish/publish');
|
|
516
637
|
const isSuccess = successMsg.length > 0 || navigatedAway;
|
|
@@ -33,7 +33,214 @@ function createPageMock(evaluateResults, overrides = {}) {
|
|
|
33
33
|
...overrides,
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
|
+
function createConditionalPageMock(evaluateImpl, overrides = {}) {
|
|
37
|
+
const page = createPageMock([], overrides);
|
|
38
|
+
page.evaluate.mockImplementation(async (js) => evaluateImpl(String(js)));
|
|
39
|
+
return page;
|
|
40
|
+
}
|
|
36
41
|
describe('xiaohongshu publish', () => {
|
|
42
|
+
it('uses native insertText for contenteditable title fields when available', async () => {
|
|
43
|
+
const cmd = getRegistry().get('xiaohongshu/publish');
|
|
44
|
+
expect(cmd?.func).toBeTypeOf('function');
|
|
45
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-xhs-publish-'));
|
|
46
|
+
const imagePath = path.join(tempDir, 'demo.jpg');
|
|
47
|
+
fs.writeFileSync(imagePath, Buffer.from([0xff, 0xd8, 0xff, 0xd9]));
|
|
48
|
+
const insertText = vi.fn().mockResolvedValue(undefined);
|
|
49
|
+
const page = createConditionalPageMock((code) => {
|
|
50
|
+
if (code.includes('location.href'))
|
|
51
|
+
return 'https://creator.xiaohongshu.com/publish/publish?from=menu_left';
|
|
52
|
+
if (code.includes("const targets = ['上传图文', '图文', '图片']"))
|
|
53
|
+
return { ok: true, target: '上传图文', text: '上传图文' };
|
|
54
|
+
if (code.includes('hasTitleInput') && code.includes('hasVideoSurface'))
|
|
55
|
+
return { state: 'editor_ready', hasTitleInput: true, hasImageInput: true, hasVideoSurface: false };
|
|
56
|
+
if (code.includes('const images =') && code.includes('dt.items.add(new File'))
|
|
57
|
+
return { ok: true, count: 1 };
|
|
58
|
+
if (code.includes('[class*="upload"][class*="progress"]'))
|
|
59
|
+
return false;
|
|
60
|
+
if (code.includes('const sels =') && code.includes('for (const sel of sels)'))
|
|
61
|
+
return true;
|
|
62
|
+
if (code.includes('__opencli_xhs_fill_phase') && code.includes('"locate"')) {
|
|
63
|
+
return code.includes('[contenteditable="true"][placeholder*="标题"]')
|
|
64
|
+
? { ok: true, sel: '[contenteditable="true"][placeholder*="标题"]', kind: 'contenteditable' }
|
|
65
|
+
: { ok: true, sel: '[contenteditable="true"][class*="content"]', kind: 'contenteditable' };
|
|
66
|
+
}
|
|
67
|
+
if (code.includes('__opencli_xhs_fill_phase') && code.includes('"prepare"'))
|
|
68
|
+
return { ok: true };
|
|
69
|
+
if (code.includes('__opencli_xhs_fill_phase') && code.includes('"verify"')) {
|
|
70
|
+
return code.includes('[contenteditable="true"][placeholder*="标题"]')
|
|
71
|
+
? { ok: true, actual: '标题走原生输入' }
|
|
72
|
+
: { ok: true, actual: '正文也走原生输入' };
|
|
73
|
+
}
|
|
74
|
+
if (code.includes('(function(selectors, text)')) {
|
|
75
|
+
return code.includes('[contenteditable="true"][placeholder*="标题"]')
|
|
76
|
+
? { ok: true, sel: '[contenteditable="true"][placeholder*="标题"]', kind: 'contenteditable', actual: '标题走原生输入' }
|
|
77
|
+
: { ok: true, sel: '[contenteditable="true"][class*="content"]', kind: 'contenteditable', actual: '正文也走原生输入' };
|
|
78
|
+
}
|
|
79
|
+
if (code.includes('labels.some'))
|
|
80
|
+
return true;
|
|
81
|
+
if (code.includes('for (const el of document.querySelectorAll'))
|
|
82
|
+
return '发布成功';
|
|
83
|
+
throw new Error(`Unhandled evaluate call: ${code.slice(0, 120)}`);
|
|
84
|
+
}, {
|
|
85
|
+
insertText,
|
|
86
|
+
});
|
|
87
|
+
const result = await cmd.func(page, {
|
|
88
|
+
title: '标题走原生输入',
|
|
89
|
+
content: '正文也走原生输入',
|
|
90
|
+
images: imagePath,
|
|
91
|
+
topics: '',
|
|
92
|
+
draft: false,
|
|
93
|
+
});
|
|
94
|
+
expect(insertText).toHaveBeenNthCalledWith(1, '标题走原生输入');
|
|
95
|
+
expect(insertText).toHaveBeenNthCalledWith(2, '正文也走原生输入');
|
|
96
|
+
expect(result).toEqual([
|
|
97
|
+
{
|
|
98
|
+
status: '✅ 发布成功',
|
|
99
|
+
detail: '"标题走原生输入" · 1张图片 · 发布成功',
|
|
100
|
+
},
|
|
101
|
+
]);
|
|
102
|
+
});
|
|
103
|
+
it('aborts when the title does not stick after filling', async () => {
|
|
104
|
+
const cmd = getRegistry().get('xiaohongshu/publish');
|
|
105
|
+
expect(cmd?.func).toBeTypeOf('function');
|
|
106
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-xhs-publish-'));
|
|
107
|
+
const imagePath = path.join(tempDir, 'demo.jpg');
|
|
108
|
+
fs.writeFileSync(imagePath, Buffer.from([0xff, 0xd8, 0xff, 0xd9]));
|
|
109
|
+
const insertText = vi.fn().mockResolvedValue(undefined);
|
|
110
|
+
const page = createConditionalPageMock((code) => {
|
|
111
|
+
if (code.includes('location.href'))
|
|
112
|
+
return 'https://creator.xiaohongshu.com/publish/publish?from=menu_left';
|
|
113
|
+
if (code.includes("const targets = ['上传图文', '图文', '图片']"))
|
|
114
|
+
return { ok: true, target: '上传图文', text: '上传图文' };
|
|
115
|
+
if (code.includes('hasTitleInput') && code.includes('hasVideoSurface'))
|
|
116
|
+
return { state: 'editor_ready', hasTitleInput: true, hasImageInput: true, hasVideoSurface: false };
|
|
117
|
+
if (code.includes('const images =') && code.includes('dt.items.add(new File'))
|
|
118
|
+
return { ok: true, count: 1 };
|
|
119
|
+
if (code.includes('[class*="upload"][class*="progress"]'))
|
|
120
|
+
return false;
|
|
121
|
+
if (code.includes('const sels =') && code.includes('for (const sel of sels)'))
|
|
122
|
+
return true;
|
|
123
|
+
if (code.includes('__opencli_xhs_fill_phase') && code.includes('"locate"'))
|
|
124
|
+
return { ok: true, sel: '[contenteditable="true"][placeholder*="标题"]', kind: 'contenteditable' };
|
|
125
|
+
if (code.includes('__opencli_xhs_fill_phase') && code.includes('"prepare"'))
|
|
126
|
+
return { ok: true };
|
|
127
|
+
if (code.includes('__opencli_xhs_fill_phase') && code.includes('"verify"'))
|
|
128
|
+
return { ok: false, actual: '' };
|
|
129
|
+
if (code.includes('(function(selectors, text)'))
|
|
130
|
+
return { ok: true, sel: '[contenteditable="true"][placeholder*="标题"]', kind: 'contenteditable', actual: '' };
|
|
131
|
+
if (code.includes('labels.some'))
|
|
132
|
+
return true;
|
|
133
|
+
if (code.includes('for (const el of document.querySelectorAll'))
|
|
134
|
+
return '发布成功';
|
|
135
|
+
throw new Error(`Unhandled evaluate call: ${code.slice(0, 120)}`);
|
|
136
|
+
}, {
|
|
137
|
+
insertText,
|
|
138
|
+
});
|
|
139
|
+
await expect(cmd.func(page, {
|
|
140
|
+
title: '标题没写进去',
|
|
141
|
+
content: '正文',
|
|
142
|
+
images: imagePath,
|
|
143
|
+
topics: '',
|
|
144
|
+
draft: false,
|
|
145
|
+
})).rejects.toThrow('Failed to set title');
|
|
146
|
+
expect(insertText).toHaveBeenCalledWith('标题没写进去');
|
|
147
|
+
});
|
|
148
|
+
it('falls back to in-page insertion when contenteditable native insertText fails', async () => {
|
|
149
|
+
const cmd = getRegistry().get('xiaohongshu/publish');
|
|
150
|
+
expect(cmd?.func).toBeTypeOf('function');
|
|
151
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-xhs-publish-'));
|
|
152
|
+
const imagePath = path.join(tempDir, 'demo.jpg');
|
|
153
|
+
fs.writeFileSync(imagePath, Buffer.from([0xff, 0xd8, 0xff, 0xd9]));
|
|
154
|
+
const insertText = vi.fn().mockRejectedValue(new Error('insertText returned no inserted flag'));
|
|
155
|
+
const page = createConditionalPageMock((code) => {
|
|
156
|
+
if (code.includes('location.href'))
|
|
157
|
+
return 'https://creator.xiaohongshu.com/publish/publish?from=menu_left';
|
|
158
|
+
if (code.includes("const targets = ['上传图文', '图文', '图片']"))
|
|
159
|
+
return { ok: true, target: '上传图文', text: '上传图文' };
|
|
160
|
+
if (code.includes('hasTitleInput') && code.includes('hasVideoSurface'))
|
|
161
|
+
return { state: 'editor_ready', hasTitleInput: true, hasImageInput: true, hasVideoSurface: false };
|
|
162
|
+
if (code.includes('const images =') && code.includes('dt.items.add(new File'))
|
|
163
|
+
return { ok: true, count: 1 };
|
|
164
|
+
if (code.includes('[class*="upload"][class*="progress"]'))
|
|
165
|
+
return false;
|
|
166
|
+
if (code.includes('const sels =') && code.includes('for (const sel of sels)'))
|
|
167
|
+
return true;
|
|
168
|
+
if (code.includes('__opencli_xhs_fill_phase') && code.includes('"locate"')) {
|
|
169
|
+
return code.includes('[contenteditable="true"][placeholder*="标题"]')
|
|
170
|
+
? { ok: true, sel: '[contenteditable="true"][placeholder*="标题"]', kind: 'contenteditable' }
|
|
171
|
+
: { ok: true, sel: '[contenteditable="true"][class*="content"]', kind: 'contenteditable' };
|
|
172
|
+
}
|
|
173
|
+
if (code.includes('__opencli_xhs_fill_phase') && code.includes('"prepare"'))
|
|
174
|
+
return { ok: true };
|
|
175
|
+
if (code.includes('__opencli_xhs_fill_phase') && code.includes('"apply"')) {
|
|
176
|
+
return code.includes('[contenteditable="true"][placeholder*="标题"]')
|
|
177
|
+
? { ok: true, actual: '原生失败后回退' }
|
|
178
|
+
: { ok: true, actual: '正文也回退' };
|
|
179
|
+
}
|
|
180
|
+
if (code.includes('labels.some'))
|
|
181
|
+
return true;
|
|
182
|
+
if (code.includes('for (const el of document.querySelectorAll'))
|
|
183
|
+
return '发布成功';
|
|
184
|
+
throw new Error(`Unhandled evaluate call: ${code.slice(0, 120)}`);
|
|
185
|
+
}, {
|
|
186
|
+
insertText,
|
|
187
|
+
});
|
|
188
|
+
const result = await cmd.func(page, {
|
|
189
|
+
title: '原生失败后回退',
|
|
190
|
+
content: '正文也回退',
|
|
191
|
+
images: imagePath,
|
|
192
|
+
topics: '',
|
|
193
|
+
draft: false,
|
|
194
|
+
});
|
|
195
|
+
expect(insertText).toHaveBeenCalledWith('原生失败后回退');
|
|
196
|
+
expect(result).toEqual([
|
|
197
|
+
{
|
|
198
|
+
status: '✅ 发布成功',
|
|
199
|
+
detail: '"原生失败后回退" · 1张图片 · 发布成功',
|
|
200
|
+
},
|
|
201
|
+
]);
|
|
202
|
+
});
|
|
203
|
+
it('aborts when an input title does not stick after filling', async () => {
|
|
204
|
+
const cmd = getRegistry().get('xiaohongshu/publish');
|
|
205
|
+
expect(cmd?.func).toBeTypeOf('function');
|
|
206
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-xhs-publish-'));
|
|
207
|
+
const imagePath = path.join(tempDir, 'demo.jpg');
|
|
208
|
+
fs.writeFileSync(imagePath, Buffer.from([0xff, 0xd8, 0xff, 0xd9]));
|
|
209
|
+
const page = createConditionalPageMock((code) => {
|
|
210
|
+
if (code.includes('location.href'))
|
|
211
|
+
return 'https://creator.xiaohongshu.com/publish/publish?from=menu_left';
|
|
212
|
+
if (code.includes("const targets = ['上传图文', '图文', '图片']"))
|
|
213
|
+
return { ok: true, target: '上传图文', text: '上传图文' };
|
|
214
|
+
if (code.includes('hasTitleInput') && code.includes('hasVideoSurface'))
|
|
215
|
+
return { state: 'editor_ready', hasTitleInput: true, hasImageInput: true, hasVideoSurface: false };
|
|
216
|
+
if (code.includes('const images =') && code.includes('dt.items.add(new File'))
|
|
217
|
+
return { ok: true, count: 1 };
|
|
218
|
+
if (code.includes('[class*="upload"][class*="progress"]'))
|
|
219
|
+
return false;
|
|
220
|
+
if (code.includes('const sels =') && code.includes('for (const sel of sels)'))
|
|
221
|
+
return true;
|
|
222
|
+
if (code.includes('__opencli_xhs_fill_phase') && code.includes('"locate"'))
|
|
223
|
+
return code.includes('input[maxlength')
|
|
224
|
+
? { ok: true, sel: 'input[maxlength="20"]', kind: 'input' }
|
|
225
|
+
: { ok: true, sel: '[contenteditable="true"][class*="content"]', kind: 'contenteditable' };
|
|
226
|
+
if (code.includes('__opencli_xhs_fill_phase') && code.includes('"apply"'))
|
|
227
|
+
return code.includes('input[maxlength')
|
|
228
|
+
? { ok: false, actual: '' }
|
|
229
|
+
: { ok: true, actual: '正文' };
|
|
230
|
+
if (code.includes('labels.some'))
|
|
231
|
+
return true;
|
|
232
|
+
if (code.includes('for (const el of document.querySelectorAll'))
|
|
233
|
+
return '发布成功';
|
|
234
|
+
throw new Error(`Unhandled evaluate call: ${code.slice(0, 120)}`);
|
|
235
|
+
});
|
|
236
|
+
await expect(cmd.func(page, {
|
|
237
|
+
title: '输入框标题没写进去',
|
|
238
|
+
content: '正文',
|
|
239
|
+
images: imagePath,
|
|
240
|
+
topics: '',
|
|
241
|
+
draft: false,
|
|
242
|
+
})).rejects.toThrow('Failed to set title');
|
|
243
|
+
});
|
|
37
244
|
it('prefers CDP setFileInput upload when the page supports it', async () => {
|
|
38
245
|
const cmd = getRegistry().get('xiaohongshu/publish');
|
|
39
246
|
expect(cmd?.func).toBeTypeOf('function');
|
|
@@ -48,8 +255,10 @@ describe('xiaohongshu publish', () => {
|
|
|
48
255
|
'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"]',
|
|
49
256
|
false,
|
|
50
257
|
true,
|
|
51
|
-
{ ok: true, sel: 'input[maxlength="20"]' },
|
|
52
|
-
{ ok: true,
|
|
258
|
+
{ ok: true, sel: 'input[maxlength="20"]', kind: 'input' },
|
|
259
|
+
{ ok: true, actual: 'CDP上传优先' },
|
|
260
|
+
{ ok: true, sel: '[contenteditable="true"][class*="content"]', kind: 'contenteditable' },
|
|
261
|
+
{ ok: true, actual: '优先走 setFileInput 主路径' },
|
|
53
262
|
true,
|
|
54
263
|
'https://creator.xiaohongshu.com/publish/success',
|
|
55
264
|
'发布成功',
|
|
@@ -111,8 +320,10 @@ describe('xiaohongshu publish', () => {
|
|
|
111
320
|
{ ok: true, count: 1 },
|
|
112
321
|
false,
|
|
113
322
|
true, // waitForEditForm: editor appeared
|
|
114
|
-
{ ok: true, sel: 'input[maxlength="20"]' },
|
|
115
|
-
{ ok: true,
|
|
323
|
+
{ ok: true, sel: 'input[maxlength="20"]', kind: 'input' },
|
|
324
|
+
{ ok: true, actual: 'DeepSeek别乱问' },
|
|
325
|
+
{ ok: true, sel: '[contenteditable="true"][class*="content"]', kind: 'contenteditable' },
|
|
326
|
+
{ ok: true, actual: '一篇真实一点的小红书正文' },
|
|
116
327
|
true,
|
|
117
328
|
'https://creator.xiaohongshu.com/publish/success',
|
|
118
329
|
'发布成功',
|
|
@@ -171,8 +382,10 @@ describe('xiaohongshu publish', () => {
|
|
|
171
382
|
{ ok: true, count: 1 }, // injectImages
|
|
172
383
|
false, // waitForUploads: no progress indicator
|
|
173
384
|
true, // waitForEditForm: editor appeared
|
|
174
|
-
{ ok: true, sel: 'input[maxlength="20"]' },
|
|
175
|
-
{ ok: true,
|
|
385
|
+
{ ok: true, sel: 'input[maxlength="20"]', kind: 'input' },
|
|
386
|
+
{ ok: true, actual: '延迟切换也能过' },
|
|
387
|
+
{ ok: true, sel: '[contenteditable="true"][class*="content"]', kind: 'contenteditable' },
|
|
388
|
+
{ ok: true, actual: '图文页切换慢一点也继续等' },
|
|
176
389
|
true,
|
|
177
390
|
'https://creator.xiaohongshu.com/publish/success',
|
|
178
391
|
'发布成功',
|
|
@@ -192,4 +405,104 @@ describe('xiaohongshu publish', () => {
|
|
|
192
405
|
},
|
|
193
406
|
]);
|
|
194
407
|
});
|
|
408
|
+
it('treats 保存成功 on the draft list as a successful draft save', async () => {
|
|
409
|
+
const cmd = getRegistry().get('xiaohongshu/publish');
|
|
410
|
+
expect(cmd?.func).toBeTypeOf('function');
|
|
411
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-xhs-publish-'));
|
|
412
|
+
const imagePath = path.join(tempDir, 'demo.jpg');
|
|
413
|
+
fs.writeFileSync(imagePath, Buffer.from([0xff, 0xd8, 0xff, 0xd9]));
|
|
414
|
+
const page = createConditionalPageMock((code) => {
|
|
415
|
+
if (code.includes('location.href'))
|
|
416
|
+
return 'https://creator.xiaohongshu.com/publish/publish?from=menu_left&target=image';
|
|
417
|
+
if (code.includes("const targets = ['上传图文', '图文', '图片']"))
|
|
418
|
+
return { ok: true, target: '上传图文', text: '上传图文' };
|
|
419
|
+
if (code.includes('hasTitleInput') && code.includes('hasVideoSurface'))
|
|
420
|
+
return { state: 'editor_ready', hasTitleInput: true, hasImageInput: true, hasVideoSurface: false };
|
|
421
|
+
if (code.includes('const images =') && code.includes('dt.items.add(new File'))
|
|
422
|
+
return { ok: true, count: 1 };
|
|
423
|
+
if (code.includes('[class*="upload"][class*="progress"]'))
|
|
424
|
+
return false;
|
|
425
|
+
if (code.includes('const sels =') && code.includes('for (const sel of sels)'))
|
|
426
|
+
return true;
|
|
427
|
+
if (code.includes('__opencli_xhs_fill_phase') && code.includes('"locate"')) {
|
|
428
|
+
return code.includes('[contenteditable="true"][class*="content"]')
|
|
429
|
+
? { ok: true, sel: '[contenteditable="true"][class*="content"]', kind: 'contenteditable' }
|
|
430
|
+
: { ok: true, sel: 'input[placeholder*="标题"]', kind: 'input' };
|
|
431
|
+
}
|
|
432
|
+
if (code.includes('__opencli_xhs_fill_phase') && code.includes('"apply"')) {
|
|
433
|
+
return code.includes('[contenteditable="true"][class*="content"]')
|
|
434
|
+
? { ok: true, actual: '停留在发布页也算成功' }
|
|
435
|
+
: { ok: true, actual: '草稿成功提示' };
|
|
436
|
+
}
|
|
437
|
+
if (code.includes('labels.some'))
|
|
438
|
+
return true;
|
|
439
|
+
if (code.includes('for (const el of document.querySelectorAll')) {
|
|
440
|
+
return code.includes('保存成功') ? '保存成功' : '';
|
|
441
|
+
}
|
|
442
|
+
throw new Error(`Unhandled evaluate call: ${code.slice(0, 120)}`);
|
|
443
|
+
});
|
|
444
|
+
const result = await cmd.func(page, {
|
|
445
|
+
title: '草稿成功提示',
|
|
446
|
+
content: '停留在发布页也算成功',
|
|
447
|
+
images: imagePath,
|
|
448
|
+
topics: '',
|
|
449
|
+
draft: true,
|
|
450
|
+
});
|
|
451
|
+
expect(result).toEqual([
|
|
452
|
+
{
|
|
453
|
+
status: '✅ 暂存成功',
|
|
454
|
+
detail: '"草稿成功提示" · 1张图片 · 保存成功',
|
|
455
|
+
},
|
|
456
|
+
]);
|
|
457
|
+
});
|
|
458
|
+
it('does not treat 保存成功 alone as a publish success signal', async () => {
|
|
459
|
+
const cmd = getRegistry().get('xiaohongshu/publish');
|
|
460
|
+
expect(cmd?.func).toBeTypeOf('function');
|
|
461
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-xhs-publish-'));
|
|
462
|
+
const imagePath = path.join(tempDir, 'demo.jpg');
|
|
463
|
+
fs.writeFileSync(imagePath, Buffer.from([0xff, 0xd8, 0xff, 0xd9]));
|
|
464
|
+
const page = createConditionalPageMock((code) => {
|
|
465
|
+
if (code.includes('location.href'))
|
|
466
|
+
return 'https://creator.xiaohongshu.com/publish/publish?from=menu_left&target=image';
|
|
467
|
+
if (code.includes("const targets = ['上传图文', '图文', '图片']"))
|
|
468
|
+
return { ok: true, target: '上传图文', text: '上传图文' };
|
|
469
|
+
if (code.includes('hasTitleInput') && code.includes('hasVideoSurface'))
|
|
470
|
+
return { state: 'editor_ready', hasTitleInput: true, hasImageInput: true, hasVideoSurface: false };
|
|
471
|
+
if (code.includes('const images =') && code.includes('dt.items.add(new File'))
|
|
472
|
+
return { ok: true, count: 1 };
|
|
473
|
+
if (code.includes('[class*="upload"][class*="progress"]'))
|
|
474
|
+
return false;
|
|
475
|
+
if (code.includes('const sels =') && code.includes('for (const sel of sels)'))
|
|
476
|
+
return true;
|
|
477
|
+
if (code.includes('__opencli_xhs_fill_phase') && code.includes('"locate"')) {
|
|
478
|
+
return code.includes('[contenteditable="true"][class*="content"]')
|
|
479
|
+
? { ok: true, sel: '[contenteditable="true"][class*="content"]', kind: 'contenteditable' }
|
|
480
|
+
: { ok: true, sel: 'input[placeholder*="标题"]', kind: 'input' };
|
|
481
|
+
}
|
|
482
|
+
if (code.includes('__opencli_xhs_fill_phase') && code.includes('"apply"')) {
|
|
483
|
+
return code.includes('[contenteditable="true"][class*="content"]')
|
|
484
|
+
? { ok: true, actual: '发布提示不该复用草稿成功' }
|
|
485
|
+
: { ok: true, actual: '发布成功提示' };
|
|
486
|
+
}
|
|
487
|
+
if (code.includes('labels.some'))
|
|
488
|
+
return true;
|
|
489
|
+
if (code.includes('for (const el of document.querySelectorAll')) {
|
|
490
|
+
return code.includes('保存成功') ? '保存成功' : '';
|
|
491
|
+
}
|
|
492
|
+
throw new Error(`Unhandled evaluate call: ${code.slice(0, 120)}`);
|
|
493
|
+
});
|
|
494
|
+
const result = await cmd.func(page, {
|
|
495
|
+
title: '发布成功提示',
|
|
496
|
+
content: '发布提示不该复用草稿成功',
|
|
497
|
+
images: imagePath,
|
|
498
|
+
topics: '',
|
|
499
|
+
draft: false,
|
|
500
|
+
});
|
|
501
|
+
expect(result).toEqual([
|
|
502
|
+
{
|
|
503
|
+
status: '⚠️ 操作完成,请在浏览器中确认',
|
|
504
|
+
detail: '"发布成功提示" · 1张图片 · https://creator.xiaohongshu.com/publish/publish?from=menu_left&target=image',
|
|
505
|
+
},
|
|
506
|
+
]);
|
|
507
|
+
});
|
|
195
508
|
});
|