/ clis / gitee / user.js
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  });