Files
wehub-resource-sync 9b395f5cc3
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
Build Chrome Extension / build (push) Has been cancelled
Trigger Website Rebuild (Docs Updated) / dispatch (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:48 +08:00

154 lines
6.2 KiB
JavaScript

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,
};