jackwener/OpenCLI · error · CommandExecutionError

Appending images failed

Error message

Appending images failed: ${upload.error ?? 'unknown'}. Debug screenshot: /tmp/xhs_publish_append_debug.png

What it means

In 文字配图 (text-image) mode, after generated cards are applied, any additional --images files are appended via uploadImages. If that append upload fails, the CLI screenshots the page and aborts with the underlying upload error.

Solutions

  1. Read the upload.error detail and check /tmp/xhs_publish_append_debug.png.
  2. Verify each appended image path exists, is absolute, and is a supported format.
  3. Retry the command; transient injection races occur.
  4. If only generated cards are needed, drop the --images flag to bypass the append step.

Example fix

// before
opencli xiaohongshu publish --text-image --images missing.png
// after
ls -l missing.png   # verify first, or omit --images to use generated cards only
Defensive patterns

Strategy: validation

Validate before calling

const fs = require('fs');
const appendPaths = opts.images || [];
for (const p of appendPaths) {
  if (!fs.existsSync(p)) throw new Error(`Append image not found: ${p}`);
}

Try / catch

try {
  await publish({ textImage: true, images: extra });
} catch (e) {
  if (String(e.message).startsWith('Appending images failed')) {
    // fall back to generated cards only
    return publish({ textImage: true });
  }
  throw e;
}

Prevention

When it happens

Trigger: isTextImage is true and absImagePaths.length > 0, and the second uploadImages call returns { ok:false } — file missing/unreadable, input selector changed, or injection rejected.

Common situations: Same causes as the initial upload error: bad/relative image paths, unsupported format, deleted files, or composer DOM changes — but hit only when combining generated cards with extra uploaded images.

Related errors


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/0b85521602386630. Report an issue: GitHub.

Appendix: source

Thrown at clis/xiaohongshu/publish.js:1265

            await page.wait({ time: UPLOAD_SETTLE_MS / 1_000 });
            await waitForUploads(page);
        }
        // ── Step 3b: Wait for editor form to render ───────────────────────────────
        const formReady = await waitForEditForm(page);
        if (!formReady) {
            await page.screenshot({ path: '/tmp/xhs_publish_form_debug.png' });
            throw new CommandExecutionError('Editing form did not appear after image acquisition. The page layout may have changed. ' +
                'Debug screenshot: /tmp/xhs_publish_form_debug.png');
        }
        if (isTextImage) {
            await assertComposerMediaCount(page, cards.length, '文字配图 generated images');
        }
        // ── Step 3c: In text-image mode, optionally append uploaded images ────────
        if (isTextImage && absImagePaths.length > 0) {
            const upload = await uploadImages(page, absImagePaths);
            if (!upload.ok) {
                await page.screenshot({ path: '/tmp/xhs_publish_append_debug.png' });
                throw new CommandExecutionError(`Appending images failed: ${upload.error ?? 'unknown'}. ` +
                    'Debug screenshot: /tmp/xhs_publish_append_debug.png');
            }
            await page.wait({ time: UPLOAD_SETTLE_MS / 1_000 });
            await waitForUploads(page);
            await assertComposerMediaCount(page, cards.length + absImagePaths.length, '文字配图 appended images');
        }
        // ── Step 4: Fill title ─────────────────────────────────────────────────────
        await fillField(page, TITLE_SELECTORS, title, 'title');
        await page.wait({ time: 0.5 });
        // ── Step 5: Fill content / body ────────────────────────────────────────────
        await fillField(page, BODY_SELECTORS, content, 'content');
        await page.wait({ time: 0.5 });
        // ── Step 6: Add topic hashtags ─────────────────────────────────────────────
        // XHS converts a "#keyword" typed into the body editor into a real topic
        // entity only when the user picks an item from the inline suggestion
        // dropdown that pops up while typing. The previous implementation looked
        // for a standalone "添加话题" button + dedicated search <input>, which the
        // current creator-center editor no longer exposes — it left bare "#"

View on GitHub (pinned to 49907e53dc)