/ clis / taobao / reviews.js
reviews.js
 1  import { cli, Strategy } from '@jackwener/opencli/registry';
 2  import { clampInt, normalizeNumericId } from '../_shared/common.js';
 3  cli({
 4      site: 'taobao',
 5      name: 'reviews',
 6      description: '淘宝商品评价',
 7      domain: 'item.taobao.com',
 8      strategy: Strategy.COOKIE,
 9      args: [
10          { name: 'id', positional: true, required: true, help: '商品 ID' },
11          { name: 'limit', type: 'int', default: 10, help: '返回评价数量 (max 20)' },
12      ],
13      columns: ['rank', 'user', 'content', 'date', 'spec'],
14      navigateBefore: false,
15      func: async (page, kwargs) => {
16          const itemId = normalizeNumericId(kwargs.id, 'id', '827563850178');
17          const limit = clampInt(kwargs.limit, 10, 1, 20);
18          await page.goto('https://www.taobao.com');
19          await page.wait(2);
20          await page.evaluate(`location.href = ${JSON.stringify(`https://item.taobao.com/item.htm?id=${itemId}`)}`);
21          await page.wait(6);
22          const data = await page.evaluate(`
23        (async () => {
24          const normalize = v => (v || '').replace(/\\s+/g, ' ').trim();
25          let sellerId = '';
26          const pageText = document.documentElement.innerHTML || '';
27          const sellerMatch = pageText.match(/sellerId['":\\s]+['"]?(\\d+)/) || pageText.match(/userId['":\\s]+['"]?(\\d+)/) || pageText.match(/shopId['":\\s]+['"]?(\\d+)/);
28          if (sellerMatch) sellerId = sellerMatch[1];
29  
30          if (!sellerId) {
31            const shopLink = document.querySelector('a[href*="shopId="], a[href*="seller_id="], a[href*="userId="]');
32            const href = shopLink?.getAttribute('href') || '';
33            const m = href.match(/(?:shopId|seller_id|userId)=(\\d+)/);
34            if (m) sellerId = m[1];
35          }
36  
37          const url = 'https://rate.tmall.com/list_detail_rate.htm?itemId=' + ${JSON.stringify(itemId)}
38            + (sellerId ? '&sellerId=' + sellerId : '')
39            + '&order=3&currentPage=1&append=0&content=1&tagId=&posi=&picture=&groupValue=&needFold=0&_ksTS=' + Date.now();
40  
41          try {
42            const results = await new Promise((resolve) => {
43              const cbName = '_ocli_rate_' + Date.now();
44              let settled = false;
45              const cleanup = (value) => {
46                if (settled) return;
47                settled = true;
48                delete window[cbName];
49                script.remove();
50                resolve(value);
51              };
52              window[cbName] = (payload) => {
53                const list = payload?.rateDetail?.rateList || [];
54                cleanup(list.slice(0, ${limit}).map((item, i) => ({
55                  rank: i + 1,
56                  user: (item.displayUserNick || item.userNick || '').slice(0, 15),
57                  content: normalize(item.rateContent || '').slice(0, 150),
58                  date: (item.rateDate || '').slice(0, 10),
59                  spec: normalize(item.auctionSku || '').slice(0, 40),
60                })));
61              };
62              const script = document.createElement('script');
63              script.src = url + '&callback=' + cbName;
64              script.onerror = () => cleanup([]);
65              document.head.appendChild(script);
66              setTimeout(() => cleanup([]), 10000);
67            });
68            if (results.length > 0) return results;
69          } catch {}
70  
71          return [];
72        })()
73      `);
74          return Array.isArray(data) ? data : [];
75      },
76  });