question.js
1 import { cli, Strategy } from '@jackwener/opencli/registry'; 2 import { AuthRequiredError, CliError } from '@jackwener/opencli/errors'; 3 function stripHtml(html) { 4 return html 5 .replace(/<[^>]+>/g, '') 6 .replace(/ /g, ' ') 7 .replace(/</g, '<') 8 .replace(/>/g, '>') 9 .replace(/&/g, '&') 10 .trim(); 11 } 12 cli({ 13 site: 'zhihu', 14 name: 'question', 15 description: '知乎问题详情和回答', 16 domain: 'www.zhihu.com', 17 strategy: Strategy.COOKIE, 18 args: [ 19 { name: 'id', required: true, positional: true, help: 'Question ID (numeric)' }, 20 { name: 'limit', type: 'int', default: 5, help: 'Number of answers' }, 21 ], 22 columns: ['rank', 'author', 'votes', 'content'], 23 func: async (page, kwargs) => { 24 const { id, limit = 5 } = kwargs; 25 const questionId = String(id); 26 if (!/^\d+$/.test(questionId)) { 27 throw new CliError('INVALID_INPUT', 'Question ID must be numeric', 'Example: opencli zhihu question 123456789'); 28 } 29 const answerLimit = Number(limit); 30 await page.goto(`https://www.zhihu.com/question/${questionId}`); 31 const url = `https://www.zhihu.com/api/v4/questions/${questionId}/answers?limit=${answerLimit}&offset=0&sort_by=default&include=data[*].content,voteup_count,comment_count,author`; 32 const data = await page.evaluate(` 33 (async () => { 34 const r = await fetch(${JSON.stringify(url)}, { credentials: 'include' }); 35 if (!r.ok) return { __httpError: r.status }; 36 return await r.json(); 37 })() 38 `); 39 if (!data || data.__httpError) { 40 const status = data?.__httpError; 41 if (status === 401 || status === 403) { 42 throw new AuthRequiredError('www.zhihu.com', 'Failed to fetch question data from Zhihu'); 43 } 44 throw new CliError('FETCH_ERROR', status ? `Zhihu question answers request failed (HTTP ${status})` : 'Zhihu question answers request failed', 'Try again later or rerun with -v for more detail'); 45 } 46 return (data.data || []).map((item, i) => ({ 47 rank: i + 1, 48 author: item.author?.name || 'anonymous', 49 votes: item.voteup_count || 0, 50 content: stripHtml(item.content || '').substring(0, 200), 51 })); 52 }, 53 });