mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-30 13:49:48 +08:00
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 <beforesun@BeforedeMac-mini.local>
This commit is contained in:
@@ -245,7 +245,7 @@ Files created:
|
|||||||
|-------|-----|
|
|-------|-----|
|
||||||
| Missing API credentials | Follow guided setup in Step 2 |
|
| Missing API credentials | Follow guided setup in Step 2 |
|
||||||
| Access token error | Verify credentials valid and not expired |
|
| 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` |
|
| Chrome not found | Set `WECHAT_BROWSER_CHROME_PATH` |
|
||||||
| Title/summary missing | Use auto-generation or provide manually |
|
| Title/summary missing | Use auto-generation or provide manually |
|
||||||
| No cover image | Add frontmatter cover or place `imgs/cover.png` in article directory |
|
| No cover image | Add frontmatter cover or place `imgs/cover.png` in article directory |
|
||||||
|
|||||||
@@ -31,7 +31,95 @@ interface ArticleOptions {
|
|||||||
cdpPort?: number;
|
cdpPort?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function sendQrToTelegram(session: ChromeSession): Promise<void> {
|
||||||
|
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<boolean> {
|
async function waitForLogin(session: ChromeSession, timeoutMs = 120_000): Promise<boolean> {
|
||||||
|
// Wait for QR to render, then notify via Telegram if configured
|
||||||
|
await sleep(2000);
|
||||||
|
await sendQrToTelegram(session);
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
while (Date.now() - start < timeoutMs) {
|
while (Date.now() - start < timeoutMs) {
|
||||||
const url = await evaluate<string>(session, 'window.location.href');
|
const url = await evaluate<string>(session, 'window.location.href');
|
||||||
@@ -557,7 +645,8 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
|
|||||||
|
|
||||||
const url = await evaluate<string>(session, 'window.location.href');
|
const url = await evaluate<string>(session, 'window.location.href');
|
||||||
if (!url.includes('/cgi-bin/')) {
|
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);
|
const loggedIn = await waitForLogin(session);
|
||||||
if (!loggedIn) throw new Error('Login timeout');
|
if (!loggedIn) throw new Error('Login timeout');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user