From 39c7e86a8dd393a2d2447ce6f7316d0be5eb8ebb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jim=20Liu=20=E5=AE=9D=E7=8E=89?= Date: Fri, 6 Mar 2026 20:27:18 -0600 Subject: [PATCH] feat(baoyu-post-to-weibo): add video support and improve upload reliability - Add --video flag for video uploads (max 18 files total) - Switch from clipboard paste to DOM.setFileInputFiles for uploads - Add Chrome health check with auto-restart for unresponsive instances - Add navigation check to ensure Weibo home page before posting --- README.md | 7 +- README.zh.md | 7 +- skills/baoyu-post-to-weibo/SKILL.md | 10 +- .../baoyu-post-to-weibo/scripts/weibo-post.ts | 148 +++++++++++------- .../scripts/weibo-utils.ts | 14 ++ 5 files changed, 120 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index 0128d64..ccfe2d3 100644 --- a/README.md +++ b/README.md @@ -560,9 +560,9 @@ To obtain credentials: #### baoyu-post-to-weibo -Post content to Weibo (微博). Supports regular posts with text and images, and headline articles (头条文章) with Markdown input. Uses real Chrome with CDP to bypass anti-automation. +Post content to Weibo (微博). Supports regular posts with text, images, and videos, and headline articles (头条文章) with Markdown input. Uses real Chrome with CDP to bypass anti-automation. -**Regular Posts** - Text + up to 9 images: +**Regular Posts** - Text + images/videos (max 18 files): ```bash # Post with text @@ -570,6 +570,9 @@ Post content to Weibo (微博). Supports regular posts with text and images, and # Post with images /baoyu-post-to-weibo "Check this out" --image photo.png + +# Post with video +/baoyu-post-to-weibo "Watch this" --video clip.mp4 ``` **Headline Articles (头条文章)** - Long-form Markdown: diff --git a/README.zh.md b/README.zh.md index d275214..c593550 100644 --- a/README.zh.md +++ b/README.zh.md @@ -560,9 +560,9 @@ WECHAT_APP_SECRET=你的AppSecret #### baoyu-post-to-weibo -发布内容到微博。支持带图文本发布和头条文章(长篇 Markdown)。使用真实 Chrome + CDP 绕过反自动化检测。 +发布内容到微博。支持文字、图片、视频发布和头条文章(长篇 Markdown)。使用真实 Chrome + CDP 绕过反自动化检测。 -**普通微博** - 文字 + 最多 9 张图片: +**普通微博** - 文字 + 图片/视频(最多 18 个文件): ```bash # 发布文字 @@ -570,6 +570,9 @@ WECHAT_APP_SECRET=你的AppSecret # 发布带图片 /baoyu-post-to-weibo "看看这个" --image photo.png + +# 发布带视频 +/baoyu-post-to-weibo "看这个" --video clip.mp4 ``` **头条文章** - 长篇 Markdown 文章: diff --git a/skills/baoyu-post-to-weibo/SKILL.md b/skills/baoyu-post-to-weibo/SKILL.md index 271cf04..fd53d7d 100644 --- a/skills/baoyu-post-to-weibo/SKILL.md +++ b/skills/baoyu-post-to-weibo/SKILL.md @@ -1,11 +1,11 @@ --- name: baoyu-post-to-weibo -description: Posts content to Weibo (微博). Supports regular posts with text and images, and headline articles (头条文章) with Markdown input via Chrome CDP. Use when user asks to "post to Weibo", "发微博", "发布微博", "publish to Weibo", "share on Weibo", "写微博", or "微博头条文章". +description: Posts content to Weibo (微博). Supports regular posts with text, images, and videos, and headline articles (头条文章) with Markdown input via Chrome CDP. Use when user asks to "post to Weibo", "发微博", "发布微博", "publish to Weibo", "share on Weibo", "写微博", or "微博头条文章". --- # Post to Weibo -Posts text, images, and long-form articles to Weibo via real Chrome browser (bypasses anti-bot detection). +Posts text, images, videos, and long-form articles to Weibo via real Chrome browser (bypasses anti-bot detection). ## Script Directory @@ -69,17 +69,19 @@ if (Test-Path "$HOME/.baoyu-skills/baoyu-post-to-weibo/EXTEND.md") { "user" } ## Regular Posts -Text + up to 9 images. Posted on Weibo homepage. +Text + images/videos (max 18 files total). Posted on Weibo homepage. ```bash ${BUN_X} ${SKILL_DIR}/scripts/weibo-post.ts "Hello Weibo!" --image ./photo.png +${BUN_X} ${SKILL_DIR}/scripts/weibo-post.ts "Watch this" --video ./clip.mp4 ``` **Parameters**: | Parameter | Description | |-----------|-------------| | `` | Post content (positional) | -| `--image ` | Image file (repeatable, max 9) | +| `--image ` | Image file (repeatable) | +| `--video ` | Video file (repeatable) | | `--profile ` | Custom Chrome profile | **Note**: Script opens browser with content filled in. User reviews and publishes manually. diff --git a/skills/baoyu-post-to-weibo/scripts/weibo-post.ts b/skills/baoyu-post-to-weibo/scripts/weibo-post.ts index 666ef71..2e8d6dc 100644 --- a/skills/baoyu-post-to-weibo/scripts/weibo-post.ts +++ b/skills/baoyu-post-to-weibo/scripts/weibo-post.ts @@ -1,47 +1,48 @@ import { spawn } from 'node:child_process'; import fs from 'node:fs'; import { mkdir } from 'node:fs/promises'; +import path from 'node:path'; import process from 'node:process'; import { CdpConnection, - copyImageToClipboard, findChromeExecutable, findExistingChromeDebugPort, getDefaultProfileDir, getFreePort, - pasteFromClipboard, + killChromeByProfile, sleep, waitForChromeDebugPort, } from './weibo-utils.js'; const WEIBO_HOME_URL = 'https://weibo.com/'; +const MAX_FILES = 18; + interface WeiboPostOptions { text?: string; images?: string[]; + videos?: string[]; timeoutMs?: number; profileDir?: string; chromePath?: string; } export async function postToWeibo(options: WeiboPostOptions): Promise { - const { text, images = [], timeoutMs = 120_000, profileDir = getDefaultProfileDir() } = options; + const { text, images = [], videos = [], timeoutMs = 120_000, profileDir = getDefaultProfileDir() } = options; + + const allFiles = [...images, ...videos]; + if (allFiles.length > MAX_FILES) { + throw new Error(`Too many files: ${allFiles.length} (max ${MAX_FILES})`); + } await mkdir(profileDir, { recursive: true }); - const existingPort = findExistingChromeDebugPort(profileDir); - let port: number; + const chromePath = options.chromePath ?? findChromeExecutable(); + if (!chromePath) throw new Error('Chrome not found. Set WEIBO_BROWSER_CHROME_PATH env var.'); - if (existingPort) { - console.log(`[weibo-post] Found existing Chrome on port ${existingPort}, reusing...`); - port = existingPort; - } else { - const chromePath = options.chromePath ?? findChromeExecutable(); - if (!chromePath) throw new Error('Chrome not found. Set WEIBO_BROWSER_CHROME_PATH env var.'); - - port = await getFreePort(); + const launchChrome = async (): Promise => { + const port = await getFreePort(); console.log(`[weibo-post] Launching Chrome (profile: ${profileDir})`); - const chromeArgs = [ `--remote-debugging-port=${port}`, `--user-data-dir=${profileDir}`, @@ -51,13 +52,35 @@ export async function postToWeibo(options: WeiboPostOptions): Promise { '--start-maximized', WEIBO_HOME_URL, ]; - if (process.platform === 'darwin') { const appPath = chromePath.replace(/\/Contents\/MacOS\/Google Chrome$/, ''); spawn('open', ['-na', appPath, '--args', ...chromeArgs], { stdio: 'ignore' }); } else { spawn(chromePath, chromeArgs, { stdio: 'ignore' }); } + return port; + }; + + let port: number; + const existingPort = findExistingChromeDebugPort(profileDir); + + if (existingPort) { + console.log(`[weibo-post] Found existing Chrome on port ${existingPort}, checking health...`); + try { + const wsUrl = await waitForChromeDebugPort(existingPort, 5_000); + const testCdp = await CdpConnection.connect(wsUrl, 5_000, { defaultTimeoutMs: 5_000 }); + await testCdp.send('Target.getTargets'); + testCdp.close(); + console.log('[weibo-post] Existing Chrome is responsive, reusing.'); + port = existingPort; + } catch { + console.log('[weibo-post] Existing Chrome unresponsive, restarting...'); + killChromeByProfile(profileDir); + await sleep(2000); + port = await launchChrome(); + } + } else { + port = await launchChrome(); } let cdp: CdpConnection | null = null; @@ -76,10 +99,23 @@ export async function postToWeibo(options: WeiboPostOptions): Promise { const { sessionId } = await cdp.send<{ sessionId: string }>('Target.attachToTarget', { targetId: pageTarget.targetId, flatten: true }); + await cdp.send('Target.activateTarget', { targetId: pageTarget.targetId }); + await cdp.send('Page.enable', {}, { sessionId }); await cdp.send('Runtime.enable', {}, { sessionId }); await cdp.send('Input.setIgnoreInputEvents', { ignore: false }, { sessionId }); + const currentUrl = await cdp.send<{ result: { value: string } }>('Runtime.evaluate', { + expression: `window.location.href`, + returnByValue: true, + }, { sessionId }); + + if (!currentUrl.result.value.includes('weibo.com/') || currentUrl.result.value.includes('card.weibo.com')) { + console.log('[weibo-post] Navigating to Weibo home...'); + await cdp.send('Page.navigate', { url: WEIBO_HOME_URL }, { sessionId }); + await sleep(3000); + } + console.log('[weibo-post] Waiting for Weibo editor...'); await sleep(3000); @@ -145,56 +181,45 @@ export async function postToWeibo(options: WeiboPostOptions): Promise { } } - for (const imagePath of images) { - if (!fs.existsSync(imagePath)) { - console.warn(`[weibo-post] Image not found: ${imagePath}`); - continue; + if (allFiles.length > 0) { + const missing = allFiles.filter((f) => !fs.existsSync(f)); + if (missing.length > 0) { + throw new Error(`Files not found: ${missing.join(', ')}`); } - console.log(`[weibo-post] Pasting image: ${imagePath}`); + const absolutePaths = allFiles.map((f) => path.resolve(f)); + console.log(`[weibo-post] Uploading ${absolutePaths.length} file(s) via file input...`); - if (!copyImageToClipboard(imagePath)) { - console.warn(`[weibo-post] Failed to copy image to clipboard: ${imagePath}`); - continue; - } + await cdp.send('DOM.enable', {}, { sessionId }); - await sleep(500); + const { root } = await cdp.send<{ root: { nodeId: number } }>('DOM.getDocument', {}, { sessionId }); - await cdp.send('Runtime.evaluate', { - expression: `document.querySelector('#homeWrap textarea')?.focus()`, + const { nodeId } = await cdp.send<{ nodeId: number }>('DOM.querySelector', { + nodeId: root.nodeId, + selector: '#homeWrap input[type="file"]', }, { sessionId }); - await sleep(200); - // Count images before paste - const imgCountBefore = await cdp.send<{ result: { value: number } }>('Runtime.evaluate', { - expression: `document.querySelectorAll('#homeWrap img[src^="blob:"], #homeWrap img[src^="data:"]').length`, + if (!nodeId || nodeId === 0) { + throw new Error('File input not found. Make sure the Weibo compose area is visible.'); + } + + await cdp.send('DOM.setFileInputFiles', { + nodeId, + files: absolutePaths, + }, { sessionId }); + + console.log('[weibo-post] Files set on input. Waiting for upload...'); + await sleep(2000); + + const uploadCheck = await cdp.send<{ result: { value: number } }>('Runtime.evaluate', { + expression: `document.querySelectorAll('#homeWrap img[src^="blob:"], #homeWrap img[src^="data:"], #homeWrap video').length`, returnByValue: true, }, { sessionId }); - console.log('[weibo-post] Pasting from clipboard...'); - pasteFromClipboard('Google Chrome', 5, 500); - - // Verify image appeared - console.log('[weibo-post] Verifying image upload...'); - const expectedImgCount = imgCountBefore.result.value + 1; - let imgUploadOk = false; - const imgWaitStart = Date.now(); - while (Date.now() - imgWaitStart < 15_000) { - const r = await cdp!.send<{ result: { value: number } }>('Runtime.evaluate', { - expression: `document.querySelectorAll('#homeWrap img[src^="blob:"], #homeWrap img[src^="data:"]').length`, - returnByValue: true, - }, { sessionId }); - if (r.result.value >= expectedImgCount) { - imgUploadOk = true; - break; - } - await sleep(1000); - } - - if (imgUploadOk) { - console.log('[weibo-post] Image upload verified'); + if (uploadCheck.result.value > 0) { + console.log(`[weibo-post] Upload verified (${uploadCheck.result.value} media item(s) detected)`); } else { - console.warn('[weibo-post] Image upload not detected after 15s. Check Accessibility permissions.'); + console.warn('[weibo-post] Upload may still be in progress. Please verify in browser.'); } } @@ -215,14 +240,18 @@ Usage: npx -y bun weibo-post.ts [options] [text] Options: - --image Add image (can be repeated, max 9) + --image Add image (can be repeated) + --video Add video (can be repeated) --profile Chrome profile directory --help Show this help +Max ${MAX_FILES} files total (images + videos combined). + Examples: npx -y bun weibo-post.ts "Hello from CLI!" npx -y bun weibo-post.ts "Check this out" --image ./screenshot.png npx -y bun weibo-post.ts "Post it!" --image a.png --image b.png + npx -y bun weibo-post.ts "Watch this" --video ./clip.mp4 `); process.exit(0); } @@ -232,6 +261,7 @@ async function main(): Promise { if (args.includes('--help') || args.includes('-h')) printUsage(); const images: string[] = []; + const videos: string[] = []; let profileDir: string | undefined; const textParts: string[] = []; @@ -239,6 +269,8 @@ async function main(): Promise { const arg = args[i]!; if (arg === '--image' && args[i + 1]) { images.push(args[++i]!); + } else if (arg === '--video' && args[i + 1]) { + videos.push(args[++i]!); } else if (arg === '--profile' && args[i + 1]) { profileDir = args[++i]; } else if (!arg.startsWith('-')) { @@ -248,12 +280,12 @@ async function main(): Promise { const text = textParts.join(' ').trim() || undefined; - if (!text && images.length === 0) { - console.error('Error: Provide text or at least one image.'); + if (!text && images.length === 0 && videos.length === 0) { + console.error('Error: Provide text or at least one image/video.'); process.exit(1); } - await postToWeibo({ text, images, profileDir }); + await postToWeibo({ text, images, videos, profileDir }); } await main().catch((err) => { diff --git a/skills/baoyu-post-to-weibo/scripts/weibo-utils.ts b/skills/baoyu-post-to-weibo/scripts/weibo-utils.ts index e17ef6c..c13d561 100644 --- a/skills/baoyu-post-to-weibo/scripts/weibo-utils.ts +++ b/skills/baoyu-post-to-weibo/scripts/weibo-utils.ts @@ -53,6 +53,20 @@ export function findExistingChromeDebugPort(profileDir: string): number | null { return null; } +export function killChromeByProfile(profileDir: string): void { + try { + const result = spawnSync('ps', ['aux'], { encoding: 'utf-8', timeout: 5000 }); + if (result.status !== 0 || !result.stdout) return; + for (const line of result.stdout.split('\n')) { + if (!line.includes(profileDir) || !line.includes('--remote-debugging-port=')) continue; + const pidMatch = line.trim().split(/\s+/)[1]; + if (pidMatch) { + try { process.kill(Number(pidMatch), 'SIGTERM'); } catch {} + } + } + } catch {} +} + export function getDefaultProfileDir(): string { const override = process.env.BAOYU_CHROME_PROFILE_DIR?.trim() || process.env.WEIBO_BROWSER_PROFILE_DIR?.trim(); if (override) return path.resolve(override);