import { AuthRequiredError, EmptyResultError, selectorError } from '@jackwener/opencli/errors'; import { cli, Strategy } from '@jackwener/opencli/registry'; import { normalizeNumericId } from './utils.js'; function buildItemUrl(itemId) { return `https://www.goofish.com/item?id=${encodeURIComponent(itemId)}`; } function buildFetchItemEvaluate(itemId) { return ` (async () => { const clean = (value) => String(value ?? '').replace(/\\s+/g, ' ').trim(); const extractRetCode = (ret) => { const first = Array.isArray(ret) ? ret[0] : ''; return clean(first).split('::')[0] || ''; }; const waitFor = async (predicate, timeoutMs = 5000) => { const start = Date.now(); while (Date.now() - start < timeoutMs) { if (predicate()) return true; await new Promise((r) => setTimeout(r, 150)); } return false; }; const bodyText = document.body?.innerText || ''; if (/请先登录|登录后/.test(bodyText)) { return { error: 'auth-required' }; } if (/验证码|安全验证|异常访问/.test(bodyText)) { return { error: 'blocked' }; } await waitFor(() => window.lib?.mtop?.request); if (!window.lib || !window.lib.mtop || typeof window.lib.mtop.request !== 'function') { return { error: 'mtop-not-ready' }; } let response; try { response = await window.lib.mtop.request({ api: 'mtop.taobao.idle.pc.detail', data: { itemId: ${JSON.stringify(itemId)} }, type: 'POST', v: '1.0', dataType: 'json', needLogin: false, needLoginPC: false, sessionOption: 'AutoLoginOnly', ecode: 0, }); } catch (error) { const ret = error?.ret || []; return { error: 'mtop-request-failed', error_code: extractRetCode(ret), error_message: clean(Array.isArray(ret) ? ret.join(' | ') : error?.message || error), }; } const retCode = extractRetCode(response?.ret || []); if (retCode && retCode !== 'SUCCESS') { return { error: 'mtop-response-error', error_code: retCode, error_message: clean((response?.ret || []).join(' | ')), }; } const data = response?.data || {}; const item = data.itemDO || {}; const seller = data.sellerDO || {}; const labels = Array.isArray(item.itemLabelExtList) ? item.itemLabelExtList : []; const findLabel = (name) => labels.find((label) => clean(label.propertyText) === name)?.text || ''; const images = Array.isArray(item.imageInfos) ? item.imageInfos.map((entry) => entry?.url).filter(Boolean) : []; return { item_id: clean(item.itemId || ${JSON.stringify(itemId)}), title: clean(item.title || ''), description: clean(item.desc || ''), price: clean('¥' + (item.soldPrice || item.defaultPrice || '')).replace(/^¥\\s*$/, ''), original_price: clean(item.originalPrice || ''), want_count: String(item.wantCnt ?? ''), collect_count: String(item.collectCnt ?? ''), browse_count: String(item.browseCnt ?? ''), status: clean(item.itemStatusStr || ''), condition: clean(findLabel('成色')), brand: clean(findLabel('品牌')), category: clean(findLabel('分类')), location: clean(seller.publishCity || seller.city || ''), seller_name: clean(seller.nick || seller.uniqueName || ''), seller_id: String(seller.sellerId || ''), seller_score: clean(seller.xianyuSummary || ''), reply_ratio_24h: clean(seller.replyRatio24h || ''), reply_interval: clean(seller.replyInterval || ''), item_url: ${JSON.stringify(buildItemUrl(itemId))}, seller_url: seller.sellerId ? 'https://www.goofish.com/personal?userId=' + seller.sellerId : '', image_count: String(images.length), image_urls: images, }; })() `; } cli({ site: 'xianyu', name: 'item', access: 'read', description: '查看闲鱼商品详情', domain: 'www.goofish.com', strategy: Strategy.COOKIE, navigateBefore: false, browser: true, args: [ { name: 'item_id', required: true, positional: true, help: '闲鱼商品 item_id' }, ], columns: ['item_id', 'title', 'price', 'condition', 'brand', 'location', 'seller_name', 'want_count'], func: async (page, kwargs) => { const itemId = normalizeNumericId(kwargs.item_id, 'item_id', '1040754408976'); await page.goto(buildItemUrl(itemId)); await page.wait(2); const result = await page.evaluate(buildFetchItemEvaluate(itemId)); if (result?.error === 'auth-required') { throw new AuthRequiredError('www.goofish.com', 'Xianyu item detail requires a logged-in browser session'); } if (result?.error === 'blocked') { throw new EmptyResultError('xianyu item', 'Xianyu item detail is blocked by verification or risk control'); } if (result?.error === 'mtop-not-ready') { throw selectorError('window.lib.mtop', '闲鱼页面未完成初始化,无法调用商品详情接口'); } if (!result || typeof result !== 'object') { throw new EmptyResultError('xianyu item', '闲鱼商品详情接口未返回有效数据'); } const errorCode = String(result.error_code || ''); const errorMessage = String(result.error_message || ''); if (/FAIL_SYS_SESSION_EXPIRED|SESSION_EXPIRED|FAIL_SYS/.test(errorCode) || /FAIL_SYS_SESSION_EXPIRED|SESSION_EXPIRED/.test(errorMessage)) { throw new AuthRequiredError('www.goofish.com', 'Xianyu item detail requires a logged-in browser session'); } if (result.error) { throw new EmptyResultError('xianyu item', errorMessage || `Xianyu item detail request failed: ${result.error}`); } if (!String(result.title || '').trim()) { throw new EmptyResultError('xianyu item', 'No item detail was returned for the specified item_id'); } return [result]; }, }); export const __test__ = { normalizeNumericId, buildItemUrl, };