notifications.js
1 /** 2 * V2EX Notifications adapter. 3 */ 4 import { CommandExecutionError } from '@jackwener/opencli/errors'; 5 import { cli, Strategy } from '@jackwener/opencli/registry'; 6 cli({ 7 site: 'v2ex', 8 name: 'notifications', 9 description: 'V2EX 获取提醒 (回复/由于)', 10 domain: 'www.v2ex.com', 11 strategy: Strategy.COOKIE, 12 browser: true, 13 args: [ 14 { name: 'limit', type: 'int', default: 20, help: 'Number of notifications' } 15 ], 16 columns: ['type', 'content', 'time'], 17 func: async (page, kwargs) => { 18 if (!page) 19 throw new CommandExecutionError('Browser page required'); 20 if (process.env.OPENCLI_VERBOSE) { 21 console.error('[opencli:v2ex] Navigating to /notifications'); 22 } 23 await page.goto('https://www.v2ex.com/notifications'); 24 await new Promise(r => setTimeout(r, 1500)); // waitForLoadState doesn't always work robustly 25 // Evaluate DOM to extract notifications 26 const data = await page.evaluate(` 27 async () => { 28 const items = Array.from(document.querySelectorAll('#Main .box .cell[id^="n_"]')); 29 return items.map(item => { 30 let type = '通知'; 31 let time = ''; 32 33 // determine type based on text content 34 const text = item.textContent || ''; 35 if (text.includes('回复了你')) type = '回复'; 36 else if (text.includes('感谢了你')) type = '感谢'; 37 else if (text.includes('收藏了你')) type = '收藏'; 38 else if (text.includes('提及你')) type = '提及'; 39 40 const timeEl = item.querySelector('.snow'); 41 if (timeEl) { 42 time = timeEl.textContent?.trim() || ''; 43 } 44 45 // payload contains the actual reply text if any 46 let payload = ''; 47 const payloadEl = item.querySelector('.payload'); 48 if (payloadEl) { 49 payload = payloadEl.textContent?.trim() || ''; 50 } 51 52 // fallback to full text cleaning if no payload (e.g. for favorites/thanks) 53 let content = payload; 54 if (!content) { 55 content = text.replace(/\\s+/g, ' ').trim(); 56 // strip out time from content if present 57 if (time && content.includes(time)) { 58 content = content.replace(time, '').trim(); 59 } 60 } 61 62 return { type, content, time }; 63 }); 64 } 65 `); 66 if (!Array.isArray(data)) 67 throw new CommandExecutionError('Failed to parse notifications data'); 68 const limit = kwargs.limit || 20; 69 return data.slice(0, limit); 70 }, 71 });