like.js
1 import { CliError, CommandExecutionError } from '@jackwener/opencli/errors'; 2 import { cli, Strategy } from '@jackwener/opencli/registry'; 3 import { assertAllowedKinds, parseTarget } from './target.js'; 4 import { buildResultRow, requireExecute } from './write-shared.js'; 5 cli({ 6 site: 'zhihu', 7 name: 'like', 8 description: 'Like a Zhihu answer or article', 9 domain: 'zhihu.com', 10 strategy: Strategy.UI, 11 browser: true, 12 args: [ 13 { name: 'target', positional: true, required: true, help: 'Zhihu target URL or typed target' }, 14 { name: 'execute', type: 'boolean', help: 'Actually perform the write action' }, 15 ], 16 columns: ['status', 'outcome', 'message', 'target_type', 'target'], 17 func: async (page, kwargs) => { 18 if (!page) 19 throw new CommandExecutionError('Browser session required for zhihu like'); 20 requireExecute(kwargs); 21 const rawTarget = String(kwargs.target); 22 const target = assertAllowedKinds('like', parseTarget(rawTarget)); 23 await page.goto(target.url); 24 const result = await page.evaluate(`(async () => { 25 const targetKind = ${JSON.stringify(target.kind)}; 26 const targetQuestionId = ${JSON.stringify(target.kind === 'answer' ? target.questionId : null)}; 27 const targetAnswerId = ${JSON.stringify(target.kind === 'answer' ? target.id : null)}; 28 29 let btn = null; 30 if (targetKind === 'answer') { 31 const block = Array.from(document.querySelectorAll('article, .AnswerItem, [data-zop-question-answer]')).find((node) => { 32 const dataAnswerId = node.getAttribute('data-answerid') || node.getAttribute('data-zop-question-answer') || ''; 33 if (dataAnswerId && dataAnswerId.includes(targetAnswerId)) return true; 34 return Array.from(node.querySelectorAll('a[href*="/answer/"]')).some((link) => { 35 const href = link.getAttribute('href') || ''; 36 return href.includes('/question/' + targetQuestionId + '/answer/' + targetAnswerId); 37 }); 38 }); 39 if (!block) return { state: 'wrong_answer' }; 40 const candidates = Array.from(block?.querySelectorAll('button') || []).filter((node) => { 41 const text = (node.textContent || '').trim(); 42 const inCommentItem = Boolean(node.closest('[data-comment-id], .CommentItem')); 43 return /赞同|赞/.test(text) && node.hasAttribute('aria-pressed') && !inCommentItem; 44 }); 45 if (candidates.length !== 1) return { state: 'ambiguous_answer_like' }; 46 btn = candidates[0]; 47 } else { 48 const articleRoot = 49 document.querySelector('article') 50 || document.querySelector('.Post-Main') 51 || document.querySelector('[itemprop="articleBody"]') 52 || document; 53 const candidates = Array.from(articleRoot.querySelectorAll('button')).filter((node) => { 54 const text = (node.textContent || '').trim(); 55 return /赞同|赞/.test(text) && node.hasAttribute('aria-pressed'); 56 }); 57 if (candidates.length !== 1) return { state: 'ambiguous_article_like' }; 58 btn = candidates[0]; 59 } 60 61 if (!btn) return { state: 'missing' }; 62 if (btn.getAttribute('aria-pressed') === 'true') return { state: 'already_liked' }; 63 64 btn.click(); 65 await new Promise((resolve) => setTimeout(resolve, 1200)); 66 67 return btn.getAttribute('aria-pressed') === 'true' 68 ? { state: 'liked' } 69 : { state: 'unknown' }; 70 })()`); 71 if (result?.state === 'wrong_answer') { 72 throw new CliError('TARGET_NOT_FOUND', 'Resolved answer target no longer matches the requested answer:<questionId>:<answerId>'); 73 } 74 if (result?.state === 'already_liked') { 75 return buildResultRow(`Already liked ${target.kind}`, target.kind, rawTarget, 'already_applied'); 76 } 77 if (result?.state === 'ambiguous_answer_like') { 78 throw new CliError('ACTION_NOT_AVAILABLE', 'Answer like control was not uniquely anchored on the requested answer'); 79 } 80 if (result?.state === 'ambiguous_article_like') { 81 throw new CliError('ACTION_NOT_AVAILABLE', 'Article like control was not uniquely anchored on the requested target'); 82 } 83 if (result?.state === 'missing') { 84 throw new CliError('ACTION_FAILED', 'Zhihu like control was missing before any write was dispatched'); 85 } 86 if (result?.state !== 'liked') { 87 throw new CliError('OUTCOME_UNKNOWN', 'Zhihu like click was dispatched, but the final state could not be verified safely'); 88 } 89 return buildResultRow(`Liked ${target.kind}`, target.kind, rawTarget, 'applied'); 90 }, 91 });