target.js
1 import { CliError } from '@jackwener/opencli/errors'; 2 const USER_RE = /^user:([A-Za-z0-9_-]+)$/; 3 const QUESTION_RE = /^question:(\d+)$/; 4 const ANSWER_RE = /^answer:(\d+):(\d+)$/; 5 const ARTICLE_RE = /^article:(\d+)$/; 6 const USER_PATH_RE = /^\/people\/([A-Za-z0-9_-]+)\/?$/; 7 const QUESTION_PATH_RE = /^\/question\/(\d+)\/?$/; 8 const ANSWER_PATH_RE = /^\/question\/(\d+)\/answer\/(\d+)\/?$/; 9 const ARTICLE_PATH_RE = /^\/p\/(\d+)\/?$/; 10 const EMPTY_AUTHORITY_RE = /^https:\/\/(?::)?@/i; 11 function isAllowedZhihuUrl(url) { 12 return url.protocol === 'https:' && url.username === '' && url.password === '' && url.port === ''; 13 } 14 export function parseTarget(input) { 15 const value = String(input).trim(); 16 if (EMPTY_AUTHORITY_RE.test(value)) { 17 throw new CliError('INVALID_INPUT', 'Zhihu write commands require a normal HTTPS Zhihu URL without malformed authority', 'Example: https://www.zhihu.com/question/123456'); 18 } 19 if (value.startsWith('answer:') && !ANSWER_RE.test(value)) { 20 throw new CliError('INVALID_INPUT', 'Invalid answer target, expected answer:<questionId>:<answerId>', 'Example: opencli zhihu like answer:123:456 --execute'); 21 } 22 let match = value.match(USER_RE); 23 if (match) { 24 return { kind: 'user', slug: match[1], url: `https://www.zhihu.com/people/${match[1]}` }; 25 } 26 match = value.match(QUESTION_RE); 27 if (match) { 28 return { kind: 'question', id: match[1], url: `https://www.zhihu.com/question/${match[1]}` }; 29 } 30 match = value.match(ANSWER_RE); 31 if (match) { 32 return { 33 kind: 'answer', 34 questionId: match[1], 35 id: match[2], 36 url: `https://www.zhihu.com/question/${match[1]}/answer/${match[2]}`, 37 }; 38 } 39 match = value.match(ARTICLE_RE); 40 if (match) { 41 return { kind: 'article', id: match[1], url: `https://zhuanlan.zhihu.com/p/${match[1]}` }; 42 } 43 try { 44 const url = new URL(value); 45 if (!isAllowedZhihuUrl(url)) { 46 throw new Error('unsupported zhihu url variant'); 47 } 48 if (url.hostname === 'www.zhihu.com') { 49 const userMatch = url.pathname.match(USER_PATH_RE); 50 if (userMatch) { 51 const slug = userMatch[1]; 52 return { kind: 'user', slug, url: `https://www.zhihu.com/people/${slug}` }; 53 } 54 const questionMatch = url.pathname.match(QUESTION_PATH_RE); 55 if (questionMatch) { 56 return { kind: 'question', id: questionMatch[1], url: `https://www.zhihu.com/question/${questionMatch[1]}` }; 57 } 58 const answerMatch = url.pathname.match(ANSWER_PATH_RE); 59 if (answerMatch) { 60 return { 61 kind: 'answer', 62 questionId: answerMatch[1], 63 id: answerMatch[2], 64 url: `https://www.zhihu.com/question/${answerMatch[1]}/answer/${answerMatch[2]}`, 65 }; 66 } 67 } 68 if (url.hostname === 'zhuanlan.zhihu.com') { 69 const articleMatch = url.pathname.match(ARTICLE_PATH_RE); 70 if (articleMatch) { 71 return { kind: 'article', id: articleMatch[1], url: `https://zhuanlan.zhihu.com/p/${articleMatch[1]}` }; 72 } 73 } 74 } 75 catch { } 76 throw new CliError('INVALID_INPUT', 'Zhihu write commands require a Zhihu URL or typed target like question:123 or answer:123:456', 'Example: opencli zhihu like answer:123:456 --execute'); 77 } 78 export function assertAllowedKinds(command, target) { 79 const allowed = { 80 follow: ['user', 'question'], 81 like: ['answer', 'article'], 82 favorite: ['answer', 'article'], 83 comment: ['answer', 'article'], 84 answer: ['question'], 85 }; 86 if (!allowed[command]?.includes(target.kind)) { 87 throw new CliError('UNSUPPORTED_TARGET', `zhihu ${command} does not support ${target.kind} targets`); 88 } 89 return target; 90 } 91 export const __test__ = { parseTarget, assertAllowedKinds };