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
154 lines
6.2 KiB
JavaScript
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,
|
|
};
|