diff --git a/skills/baoyu-post-to-wechat/scripts/cdp.ts b/skills/baoyu-post-to-wechat/scripts/cdp.ts index 77d2020..840a75d 100644 --- a/skills/baoyu-post-to-wechat/scripts/cdp.ts +++ b/skills/baoyu-post-to-wechat/scripts/cdp.ts @@ -171,6 +171,37 @@ export interface ChromeSession { targetId: string; } +export async function tryConnectExisting(port: number): Promise { + try { + const version = await fetchJson<{ webSocketDebuggerUrl?: string }>(`http://127.0.0.1:${port}/json/version`); + if (version.webSocketDebuggerUrl) { + const cdp = await CdpConnection.connect(version.webSocketDebuggerUrl, 5_000); + return cdp; + } + } catch {} + return null; +} + +export async function findExistingChromeDebugPort(): Promise { + if (process.platform !== 'darwin' && process.platform !== 'linux') return null; + try { + const { execSync } = await import('node:child_process'); + const cmd = process.platform === 'darwin' + ? `lsof -nP -iTCP -sTCP:LISTEN 2>/dev/null | grep -i 'google\\|chrome' | awk '{print $9}' | sed 's/.*://'` + : `ss -tlnp 2>/dev/null | grep -i chrome | awk '{print $4}' | sed 's/.*://'`; + const output = execSync(cmd, { encoding: 'utf-8', timeout: 5_000 }).trim(); + if (!output) return null; + const ports = output.split('\n').map(p => parseInt(p, 10)).filter(p => !isNaN(p) && p > 0); + for (const port of ports) { + try { + const version = await fetchJson<{ webSocketDebuggerUrl?: string }>(`http://127.0.0.1:${port}/json/version`); + if (version.webSocketDebuggerUrl) return port; + } catch {} + } + } catch {} + return null; +} + export async function launchChrome(url: string, profileDir?: string): Promise<{ cdp: CdpConnection; chrome: ReturnType }> { const chromePath = findChromeExecutable(); if (!chromePath) throw new Error('Chrome not found. Set WECHAT_BROWSER_CHROME_PATH env var.'); diff --git a/skills/baoyu-post-to-wechat/scripts/wechat-article.ts b/skills/baoyu-post-to-wechat/scripts/wechat-article.ts index 73463eb..e2d8ddb 100644 --- a/skills/baoyu-post-to-wechat/scripts/wechat-article.ts +++ b/skills/baoyu-post-to-wechat/scripts/wechat-article.ts @@ -3,7 +3,7 @@ import path from 'node:path'; import { spawnSync } from 'node:child_process'; import process from 'node:process'; import { fileURLToPath } from 'node:url'; -import { launchChrome, getPageSession, waitForNewTab, clickElement, typeText, evaluate, sleep, type ChromeSession, type CdpConnection } from './cdp.ts'; +import { launchChrome, tryConnectExisting, findExistingChromeDebugPort, getPageSession, waitForNewTab, clickElement, typeText, evaluate, sleep, type ChromeSession, type CdpConnection } from './cdp.ts'; const WECHAT_URL = 'https://mp.weixin.qq.com/'; @@ -25,6 +25,7 @@ interface ArticleOptions { contentImages?: ImageInfo[]; submit?: boolean; profileDir?: string; + cdpPort?: number; } async function waitForLogin(session: ChromeSession, timeoutMs = 120_000): Promise { @@ -37,6 +38,16 @@ async function waitForLogin(session: ChromeSession, timeoutMs = 120_000): Promis return false; } +async function waitForElement(session: ChromeSession, selector: string, timeoutMs = 10_000): Promise { + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + const found = await evaluate(session, `!!document.querySelector('${selector}')`); + if (found) return true; + await sleep(500); + } + return false; +} + async function clickMenuByText(session: ChromeSession, text: string): Promise { console.log(`[wechat] Clicking "${text}" menu...`); const posResult = await session.cdp.send<{ result: { value: string } }>('Runtime.evaluate', { @@ -234,7 +245,7 @@ async function pressDeleteKey(session: ChromeSession): Promise { } export async function postArticle(options: ArticleOptions): Promise { - const { title, content, htmlFile, markdownFile, theme, author, summary, images = [], submit = false, profileDir } = options; + const { title, content, htmlFile, markdownFile, theme, author, summary, images = [], submit = false, profileDir, cdpPort } = options; let { contentImages = [] } = options; let effectiveTitle = title || ''; let effectiveAuthor = author || ''; @@ -268,16 +279,67 @@ export async function postArticle(options: ArticleOptions): Promise { if (effectiveTitle && effectiveTitle.length > 64) throw new Error(`Title too long: ${effectiveTitle.length} chars (max 64)`); if (!content && !effectiveHtmlFile) throw new Error('Either --content, --html, or --markdown is required'); - const { cdp, chrome } = await launchChrome(WECHAT_URL, profileDir); + let cdp: CdpConnection; + let chrome: ReturnType | null = null; + + // Try connecting to existing Chrome: explicit port > auto-detect > launch new + const portToTry = cdpPort ?? await findExistingChromeDebugPort(); + if (portToTry) { + const existing = await tryConnectExisting(portToTry); + if (existing) { + console.log(`[cdp] Connected to existing Chrome on port ${portToTry}`); + cdp = existing; + } else { + console.log(`[cdp] Port ${portToTry} not available, launching new Chrome...`); + const launched = await launchChrome(WECHAT_URL, profileDir); + cdp = launched.cdp; + chrome = launched.chrome; + } + } else { + const launched = await launchChrome(WECHAT_URL, profileDir); + cdp = launched.cdp; + chrome = launched.chrome; + } try { console.log('[wechat] Waiting for page load...'); await sleep(3000); - let session = await getPageSession(cdp, 'mp.weixin.qq.com'); + let session: ChromeSession; + if (!chrome) { + // Reusing existing Chrome: find an already-logged-in tab (has token in URL) + const allTargets = await cdp.send<{ targetInfos: Array<{ targetId: string; url: string; type: string }> }>('Target.getTargets'); + const loggedInTab = allTargets.targetInfos.find(t => t.type === 'page' && t.url.includes('mp.weixin.qq.com') && t.url.includes('token=')); + const wechatTab = loggedInTab || allTargets.targetInfos.find(t => t.type === 'page' && t.url.includes('mp.weixin.qq.com')); + + if (wechatTab) { + console.log(`[wechat] Reusing existing tab: ${wechatTab.url.substring(0, 80)}...`); + const { sessionId: reuseSid } = await cdp.send<{ sessionId: string }>('Target.attachToTarget', { targetId: wechatTab.targetId, flatten: true }); + await cdp.send('Page.enable', {}, { sessionId: reuseSid }); + await cdp.send('Runtime.enable', {}, { sessionId: reuseSid }); + await cdp.send('DOM.enable', {}, { sessionId: reuseSid }); + session = { cdp, sessionId: reuseSid, targetId: wechatTab.targetId }; + + // Navigate to home if not already there + const currentUrl = await evaluate(session, 'window.location.href'); + if (!currentUrl.includes('/cgi-bin/home')) { + console.log('[wechat] Navigating to home...'); + await evaluate(session, `window.location.href = '${WECHAT_URL}cgi-bin/home?t=home/index'`); + await sleep(5000); + } + } else { + // No WeChat tab found, create one + console.log('[wechat] No WeChat tab found, opening...'); + await cdp.send('Target.createTarget', { url: WECHAT_URL }); + await sleep(5000); + session = await getPageSession(cdp, 'mp.weixin.qq.com'); + } + } else { + session = await getPageSession(cdp, 'mp.weixin.qq.com'); + } const url = await evaluate(session, 'window.location.href'); - if (!url.includes('/cgi-bin/home')) { + if (!url.includes('/cgi-bin/')) { console.log('[wechat] Not logged in. Please scan QR code...'); const loggedIn = await waitForLogin(session); if (!loggedIn) throw new Error('Login timeout'); @@ -285,6 +347,10 @@ export async function postArticle(options: ArticleOptions): Promise { console.log('[wechat] Logged in.'); await sleep(2000); + // Wait for menu to be ready + const menuReady = await waitForElement(session, '.new-creation__menu', 20_000); + if (!menuReady) throw new Error('Home page menu did not load'); + const targets = await cdp.send<{ targetInfos: Array<{ targetId: string; url: string; type: string }> }>('Target.getTargets'); const initialIds = new Set(targets.targetInfos.map(t => t.targetId)); @@ -413,6 +479,7 @@ Options: --image Content image, can repeat (only with --content) --submit Save as draft --profile Chrome profile directory + --cdp-port Connect to existing Chrome debug port instead of launching new instance Examples: npx -y bun wechat-article.ts --markdown article.md @@ -442,6 +509,7 @@ async function main(): Promise { let summary: string | undefined; let submit = false; let profileDir: string | undefined; + let cdpPort: number | undefined; for (let i = 0; i < args.length; i++) { const arg = args[i]!; @@ -455,15 +523,18 @@ async function main(): Promise { else if (arg === '--image' && args[i + 1]) images.push(args[++i]!); else if (arg === '--submit') submit = true; else if (arg === '--profile' && args[i + 1]) profileDir = args[++i]; + else if (arg === '--cdp-port' && args[i + 1]) cdpPort = parseInt(args[++i]!, 10); } if (!markdownFile && !htmlFile && !title) { console.error('Error: --title is required (or use --markdown/--html)'); process.exit(1); } if (!markdownFile && !htmlFile && !content) { console.error('Error: --content, --html, or --markdown is required'); process.exit(1); } - await postArticle({ title: title || '', content, htmlFile, markdownFile, theme, author, summary, images, submit, profileDir }); + await postArticle({ title: title || '', content, htmlFile, markdownFile, theme, author, summary, images, submit, profileDir, cdpPort }); } -await main().catch((err) => { +await main().then(() => { + process.exit(0); +}).catch((err) => { console.error(`Error: ${err instanceof Error ? err.message : String(err)}`); process.exit(1); });