user.js
1 import { CliError } from '@jackwener/opencli/errors'; 2 import { cli, Strategy } from '@jackwener/opencli/registry'; 3 const GITEE_BASE_URL = 'https://gitee.com'; 4 const GITEE_USER_API = 'https://gitee.com/api/v5/users'; 5 function normalizeText(value) { 6 return value.replace(/\s+/g, ' ').trim(); 7 } 8 function sanitizeUsername(value) { 9 return value.trim().replace(/^@+/, '').replace(/^\/+|\/+$/g, ''); 10 } 11 function asRecord(value) { 12 if (!value || typeof value !== 'object' || Array.isArray(value)) 13 return null; 14 return value; 15 } 16 function firstText(value) { 17 if (typeof value === 'string') 18 return normalizeText(value); 19 if (typeof value === 'number' && Number.isFinite(value)) 20 return String(value); 21 if (Array.isArray(value)) { 22 for (const item of value) { 23 const text = firstText(item); 24 if (text) 25 return text; 26 } 27 } 28 return ''; 29 } 30 function normalizeCount(value) { 31 const raw = firstText(value); 32 if (!raw) 33 return ''; 34 const compact = raw.replace(/,/g, ''); 35 const match = compact.match(/\d+(?:[.]\d+)?(?:[kKmMwW]|\u4E07)?/); 36 if (match) 37 return match[0]; 38 return ''; 39 } 40 function pickFirst(...values) { 41 for (const value of values) { 42 if (typeof value === 'string' && value.trim()) 43 return value.trim(); 44 } 45 return ''; 46 } 47 function apiGiteeIndex(user) { 48 if (!user) 49 return ''; 50 const keys = [ 51 'gitee_index', 52 'giteeIndex', 53 'index', 54 'score', 55 'contribution_score', 56 'contributionScore', 57 'contribution_index', 58 'contributionIndex', 59 ]; 60 for (const key of keys) { 61 const value = normalizeCount(user[key]); 62 if (value) 63 return value; 64 } 65 return ''; 66 } 67 cli({ 68 site: 'gitee', 69 name: 'user', 70 description: 'Show a Gitee user profile panel', 71 domain: 'gitee.com', 72 strategy: Strategy.PUBLIC, 73 browser: true, 74 args: [ 75 { name: 'username', positional: true, required: true, help: 'Gitee username' }, 76 ], 77 columns: ['field', 'value'], 78 func: async (page, args) => { 79 const username = sanitizeUsername(String(args.username ?? '')); 80 if (!username) { 81 throw new CliError('INVALID_ARGUMENT', 'Username is required', 'Use: opencli gitee user <username>'); 82 } 83 const profileUrl = `${GITEE_BASE_URL}/${encodeURIComponent(username)}`; 84 await page.goto(profileUrl); 85 await page.wait(2); 86 const rawDomSnapshot = await page.evaluate(` 87 (() => { 88 const normalize = (value) => (value || '').replace(/\\s+/g, ' ').trim(); 89 const extractCount = (value) => { 90 const text = normalize(value).replace(/,/g, ''); 91 if (!text) return ''; 92 const match = text.match(/\\d+(?:[.]\\d+)?(?:\\s*[kKmMwW\\u4E07])?/); 93 return match ? match[0].replace(/\\s+/g, '') : ''; 94 }; 95 96 const title = normalize(document.title || ''); 97 const bodyText = normalize(document.body?.innerText || ''); 98 const notFound = /404|页面不存在|资源不存在|page not found/i.test(title + ' ' + bodyText); 99 const blocked = /访问受限|没有访问权限|forbidden|denied/i.test(bodyText); 100 101 const nicknameCandidates = Array.from( 102 document.querySelectorAll('.users__personal-name h2 span[title], .users__personal-name h2 [title], .users__personal-name h2 span, .users__personal-name h2, h1'), 103 ); 104 const nicknameNode = nicknameCandidates.find((node) => { 105 const titleAttr = node && node.getAttribute ? node.getAttribute('title') : ''; 106 const text = normalize((titleAttr || node.textContent || '').replace(/\\(\\s*备注名\\s*\\)/g, '')); 107 return !!text && !/备注名/.test(text); 108 }) || null; 109 const nicknameFromAttr = nicknameNode && nicknameNode.getAttribute ? nicknameNode.getAttribute('title') : ''; 110 const nickname = normalize((nicknameFromAttr || nicknameNode?.textContent || '').replace(/\\(\\s*备注名\\s*\\)/g, '')); 111 112 let followers = extractCount(document.querySelector('#followers-number .social-count, #followers-number .follow-num')?.textContent || ''); 113 if (!followers) { 114 const card = Array.from(document.querySelectorAll('.users__personal-socials .four.wide.column, .users__personal-socials [class*="column"]')) 115 .find((el) => /followers/i.test(normalize(el.textContent || ''))); 116 if (card) followers = extractCount(card.textContent || ''); 117 } 118 119 let publicRepos = ''; 120 const projectLink = Array.from(document.querySelectorAll('a[href]')) 121 .find((el) => /\\/[^/?#]+\\/projects(?:$|[/?#])/i.test(el.getAttribute('href') || '')); 122 if (projectLink) publicRepos = extractCount(projectLink.textContent || ''); 123 124 let giteeIndex = ''; 125 const indexNodes = Array.from( 126 document.querySelectorAll('.users__personal-info *, .users__personal-container *, [class*="index" i], [id*="index" i], [class*="score" i], [id*="score" i]'), 127 ); 128 for (const node of indexNodes) { 129 const text = normalize(node.textContent || ''); 130 if (!/(码云指数|gitee\\s*index|gitee\\s*指数)/i.test(text)) continue; 131 132 const direct = text.match(/(?:码云指数|gitee\\s*index|gitee\\s*指数)[::]?\\s*(\\d+(?:[.]\\d+)?(?:\\s*[kKmMwW\\u4E07])?)/i); 133 if (direct?.[1]) { 134 giteeIndex = direct[1].replace(/\\s+/g, ''); 135 break; 136 } 137 138 const siblingText = normalize(node.nextElementSibling?.textContent || ''); 139 const parentText = normalize(node.parentElement?.textContent || ''); 140 const around = extractCount(siblingText + ' ' + parentText); 141 if (around) { 142 giteeIndex = around; 143 break; 144 } 145 } 146 147 return { 148 notFound, 149 blocked, 150 nickname, 151 followers, 152 publicRepos, 153 giteeIndex, 154 }; 155 })() 156 `); 157 const domSnapshotRecord = asRecord(rawDomSnapshot); 158 const domSnapshot = { 159 notFound: domSnapshotRecord?.notFound === true, 160 blocked: domSnapshotRecord?.blocked === true, 161 nickname: firstText(domSnapshotRecord?.nickname), 162 followers: normalizeCount(domSnapshotRecord?.followers), 163 publicRepos: normalizeCount(domSnapshotRecord?.publicRepos), 164 giteeIndex: normalizeCount(domSnapshotRecord?.giteeIndex), 165 }; 166 if (domSnapshot.notFound) { 167 throw new CliError('NOT_FOUND', `Gitee user "${username}" does not exist`, 'Check the username and retry: opencli gitee user <username>'); 168 } 169 if (domSnapshot.blocked) { 170 throw new CliError('FORBIDDEN', `Gitee user page "${username}" is not accessible`, 'The profile may be private/restricted, or the account may be unavailable'); 171 } 172 const apiUrl = `${GITEE_USER_API}/${encodeURIComponent(username)}`; 173 const apiResponse = await fetch(apiUrl, { 174 headers: { 175 Accept: 'application/json', 176 'User-Agent': 'Mozilla/5.0', 177 Referer: profileUrl, 178 }, 179 }); 180 if (apiResponse.status === 404) { 181 throw new CliError('NOT_FOUND', `Gitee user "${username}" does not exist`, 'Check the username and retry: opencli gitee user <username>'); 182 } 183 if (!apiResponse.ok) { 184 throw new CliError('REQUEST_FAILED', `Failed to read Gitee user profile API: ${apiResponse.status}`, 'Try again later or verify network access to gitee.com'); 185 } 186 const apiUser = asRecord(await apiResponse.json()); 187 const nickname = pickFirst(domSnapshot.nickname, firstText(apiUser?.name), firstText(apiUser?.login), username); 188 const followers = pickFirst(domSnapshot.followers, normalizeCount(apiUser?.followers), '-'); 189 const publicRepos = pickFirst(domSnapshot.publicRepos, normalizeCount(apiUser?.public_repos), '-'); 190 const giteeIndex = pickFirst(domSnapshot.giteeIndex, apiGiteeIndex(apiUser), '-'); 191 return [ 192 { field: 'Nickname', value: nickname }, 193 { field: 'Followers', value: followers }, 194 { field: 'Public Repositories', value: publicRepos }, 195 { field: 'Gitee Index', value: giteeIndex }, 196 { field: 'URL', value: profileUrl }, 197 ]; 198 }, 199 });