comment.js
1 import { cli, Strategy } from '@jackwener/opencli/registry'; 2 /** 3 * 评论即刻帖子 4 * 5 * 帖子详情页有评论输入框(contenteditable 或 textarea), 6 * 填入文本后点击"回复"或"发布"按钮提交。 7 */ 8 cli({ 9 site: 'jike', 10 name: 'comment', 11 description: '评论即刻帖子', 12 domain: 'web.okjike.com', 13 strategy: Strategy.UI, 14 browser: true, 15 args: [ 16 { name: 'id', type: 'string', required: true, positional: true, help: '帖子 ID' }, 17 { name: 'text', type: 'string', required: true, positional: true, help: '评论内容' }, 18 ], 19 columns: ['status', 'message'], 20 func: async (page, kwargs) => { 21 await page.goto(`https://web.okjike.com/originalPost/${kwargs.id}`); 22 // 1. 找到评论输入框并填入文本 23 const inputResult = await page.evaluate(`(async () => { 24 try { 25 const textToInsert = ${JSON.stringify(kwargs.text)}; 26 27 // 优先在评论区容器内找 contenteditable,避免误选页面其他编辑器; 28 // 若评论区 class 名变更则回退到全页查找 29 const editor = 30 document.querySelector('[class*="_comment_"] [contenteditable="true"]') || 31 document.querySelector('[contenteditable="true"]'); 32 if (editor) { 33 editor.focus(); 34 const dt = new DataTransfer(); 35 dt.setData('text/plain', textToInsert); 36 editor.dispatchEvent(new ClipboardEvent('paste', { 37 clipboardData: dt, bubbles: true, cancelable: true, 38 })); 39 await new Promise(r => setTimeout(r, 800)); 40 if (editor.textContent?.length > 0) { 41 return { ok: true, message: 'contenteditable' }; 42 } 43 } 44 45 // 回退:textarea(带评论相关 placeholder) 46 const textareas = document.querySelectorAll('textarea'); 47 for (const ta of textareas) { 48 const ph = ta.getAttribute('placeholder') || ''; 49 if (ph.includes('评论') || ph.includes('回复') || ph.includes('说点什么')) { 50 ta.focus(); 51 const setter = Object.getOwnPropertyDescriptor( 52 HTMLTextAreaElement.prototype, 'value' 53 )?.set; 54 setter?.call(ta, textToInsert); 55 ta.dispatchEvent(new Event('input', { bubbles: true })); 56 await new Promise(r => setTimeout(r, 500)); 57 return { ok: true, message: 'textarea' }; 58 } 59 } 60 61 // 兜底:任意 textarea 62 if (textareas.length > 0) { 63 const ta = textareas[0]; 64 ta.focus(); 65 const setter = Object.getOwnPropertyDescriptor( 66 HTMLTextAreaElement.prototype, 'value' 67 )?.set; 68 setter?.call(ta, textToInsert); 69 ta.dispatchEvent(new Event('input', { bubbles: true })); 70 await new Promise(r => setTimeout(r, 500)); 71 return { ok: true, message: 'textarea-fallback' }; 72 } 73 74 return { ok: false, message: '未找到评论输入框' }; 75 } catch (e) { 76 return { ok: false, message: e.toString() }; 77 } 78 })()`); 79 if (!inputResult.ok) { 80 return [{ status: 'failed', message: inputResult.message }]; 81 } 82 // 2. 点击"回复"或"发布"按钮 83 const submitResult = await page.evaluate(`(async () => { 84 try { 85 await new Promise(r => setTimeout(r, 500)); 86 const btns = Array.from(document.querySelectorAll('button')).filter(btn => { 87 const text = btn.textContent?.trim() || ''; 88 return (text === '回复' || text === '发布' || text === '发送' || text === '评论') && !btn.disabled; 89 }); 90 if (btns.length === 0) { 91 return { ok: false, message: '未找到可用的回复按钮(可能因内容为空而禁用)' }; 92 } 93 btns[0].click(); 94 return { ok: true, message: '评论发布成功' }; 95 } catch (e) { 96 return { ok: false, message: e.toString() }; 97 } 98 })()`); 99 if (submitResult.ok) 100 await page.wait(3); 101 return [{ 102 status: submitResult.ok ? 'success' : 'failed', 103 message: submitResult.message, 104 }]; 105 }, 106 });