From 9baf570caa8b9420200c1e2e67dd3db1e22c1b24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=99=E6=96=8C=E9=94=8B?= <45909516+beforesun@users.noreply.github.com> Date: Thu, 14 May 2026 14:21:45 +0800 Subject: [PATCH] feat(baoyu-post-to-wechat): send WeChat login QR code to Telegram (#150) When `TELEGRAM_BOT_TOKEN` and `TELEGRAM_CHAT_ID` env vars are set, the browser login flow automatically sends the WeChat QR code image to the configured Telegram chat so headless / remote runs don't need a screen to scan the code. Strategy (in priority order): 1. Extract QR img.src from known DOM selectors 2. If URL-based src: re-fetch inside Chrome to carry session cookies 3. Fallback: full-page CDP screenshot Feature is fully opt-in: skipped silently when env vars are absent. Co-authored-by: Before SUN --- skills/baoyu-post-to-wechat/SKILL.md | 2 +- .../scripts/wechat-article.ts | 91 ++++++++++++++++++- 2 files changed, 91 insertions(+), 2 deletions(-) diff --git a/skills/baoyu-post-to-wechat/SKILL.md b/skills/baoyu-post-to-wechat/SKILL.md index 7968e33..beb2dec 100644 --- a/skills/baoyu-post-to-wechat/SKILL.md +++ b/skills/baoyu-post-to-wechat/SKILL.md @@ -245,7 +245,7 @@ Files created: |-------|-----| | Missing API credentials | Follow guided setup in Step 2 | | Access token error | Verify credentials valid and not expired | -| Not logged in (browser) | First run opens browser — scan QR to log in | +| Not logged in (browser) | First run opens browser — scan QR to log in. Set `TELEGRAM_BOT_TOKEN` + `TELEGRAM_CHAT_ID` to receive the QR image via Telegram | | Chrome not found | Set `WECHAT_BROWSER_CHROME_PATH` | | Title/summary missing | Use auto-generation or provide manually | | No cover image | Add frontmatter cover or place `imgs/cover.png` in article directory | diff --git a/skills/baoyu-post-to-wechat/scripts/wechat-article.ts b/skills/baoyu-post-to-wechat/scripts/wechat-article.ts index 2e52cd5..5cbd4f7 100644 --- a/skills/baoyu-post-to-wechat/scripts/wechat-article.ts +++ b/skills/baoyu-post-to-wechat/scripts/wechat-article.ts @@ -31,7 +31,95 @@ interface ArticleOptions { cdpPort?: number; } +async function sendQrToTelegram(session: ChromeSession): Promise { + const botToken = process.env.TELEGRAM_BOT_TOKEN; + const chatId = process.env.TELEGRAM_CHAT_ID; + if (!botToken || !chatId) return; + + try { + // Try to extract QR image from DOM first (avoids full-page screenshot noise) + const domResult = await session.cdp.send<{ result: { value: string } }>('Runtime.evaluate', { + expression: ` + (function() { + const selectors = [ + '.login__type__container__scan img', + '.login_img img', + '#login_container img', + '.qrcode img', + 'img[src*="qrcode"]', + 'img[src*="login"]', + ]; + for (const sel of selectors) { + const el = document.querySelector(sel); + if (el?.src && !el.src.startsWith('data:,')) return el.src.startsWith('data:') ? el.src : 'url:' + el.src; + } + const canvas = document.querySelector('canvas'); + if (canvas) try { return canvas.toDataURL('image/png'); } catch {} + return ''; + })() + `, + returnByValue: true, + }, { sessionId: session.sessionId }); + + const raw = (domResult.result.value as string) ?? ''; + let imgBuffer: Buffer; + + if (raw.startsWith('data:image')) { + imgBuffer = Buffer.from(raw.split(',')[1] ?? '', 'base64'); + } else if (raw.startsWith('url:')) { + // Fetch inside Chrome to carry WeChat session cookies + const imgUrl = raw.slice(4); + const inBrowserFetch = await session.cdp.send<{ result: { value: string } }>('Runtime.evaluate', { + expression: ` + (async () => { + const resp = await fetch(${JSON.stringify(imgUrl)}, { credentials: 'include' }); + const buf = await resp.arrayBuffer(); + const bytes = new Uint8Array(buf); + let b = ''; + for (let i = 0; i < bytes.length; i++) b += String.fromCharCode(bytes[i]); + return btoa(b); + })() + `, + returnByValue: true, + awaitPromise: true, + }, { sessionId: session.sessionId }); + imgBuffer = Buffer.from((inBrowserFetch.result.value as string) ?? '', 'base64'); + } else { + // Fallback: full-page screenshot + const screenshotResp = await session.cdp.send<{ data: string }>( + 'Page.captureScreenshot', { format: 'png' }, { sessionId: session.sessionId } + ); + imgBuffer = Buffer.from(screenshotResp.data ?? '', 'base64'); + } + + const boundary = `tgboundary${Date.now()}`; + const parts: Buffer[] = [ + Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="chat_id"\r\n\r\n${chatId}\r\n`), + Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="caption"\r\n\r\nWeChat QR code — please scan to log in\r\n`), + Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="photo"; filename="qr.png"\r\nContent-Type: image/png\r\n\r\n`), + imgBuffer, + Buffer.from(`\r\n--${boundary}--\r\n`), + ]; + const tgResp = await fetch(`https://api.telegram.org/bot${botToken}/sendPhoto`, { + method: 'POST', + headers: { 'Content-Type': `multipart/form-data; boundary=${boundary}` }, + body: Buffer.concat(parts), + }); + const tgJson = await tgResp.json() as { ok: boolean; description?: string }; + if (tgJson.ok) { + console.log('[wechat] QR code sent to Telegram.'); + } else { + console.error('[wechat] Telegram send failed:', tgJson.description); + } + } catch (err) { + console.error('[wechat] Failed to send QR to Telegram:', err); + } +} + async function waitForLogin(session: ChromeSession, timeoutMs = 120_000): Promise { + // Wait for QR to render, then notify via Telegram if configured + await sleep(2000); + await sendQrToTelegram(session); const start = Date.now(); while (Date.now() - start < timeoutMs) { const url = await evaluate(session, 'window.location.href'); @@ -557,7 +645,8 @@ export async function postArticle(options: ArticleOptions): Promise { const url = await evaluate(session, 'window.location.href'); if (!url.includes('/cgi-bin/')) { - console.log('[wechat] Not logged in. Please scan QR code...'); + const hasTelegram = !!(process.env.TELEGRAM_BOT_TOKEN && process.env.TELEGRAM_CHAT_ID); + console.log(`[wechat] Not logged in. Please scan QR code...${hasTelegram ? ' (sending to Telegram)' : ''}`); const loggedIn = await waitForLogin(session); if (!loggedIn) throw new Error('Login timeout'); }