Files
jackwener__opencli/clis/amazon/product.ts
jakevin 12176de1a5 refactor: simplify core modules (#784)
* refactor: simplify core modules — remove root shims, consolidate error classification, streamline cascade/interceptor, clean up synthesize

1. Remove root-level shim files (errors.ts, logger.ts, registry.ts, types.ts, utils.ts, launcher.ts) — update all ~840 adapter imports to reference src/ directly
2. Consolidate interceptor: reuse shared DISGUISE_FN in tap interceptor instead of reimplementing
3. Unify error classification: single ClassifiedError type with icon/exitCode/hint lookup table, eliminating duplicated pattern matching between resolveExitCode and renderError
4. Simplify cascade probe: replace repetitive switch cases with PROBE_OPTIONS lookup map
12. Clean up synthesize.ts: remove deprecated snake_case field aliases (recommended_args, recommended_columns, recommendedColumnsLegacy) and unnecessary constant aliases

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: update import paths in contributor docs and skill templates

Update all documentation and skill files to reference src/ directly,
matching the shim removal in the previous commit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: update new Gemini adapter imports to use src/ paths

Fix imports in newly added deep-research adapter files that were
still referencing the deleted root shim files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: update xiaohongshu tests for /search_result/ URL change

Tests now expect /search_result/<id> for bare note IDs (matching
the note-helpers.ts change from PR #774) and updated empty-shell
hint assertion.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: update LessWrong and hupu adapter imports to use src/ paths

Fix imports in newly merged LessWrong and hupu/mentions adapter
files that were still referencing the deleted root shim files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test(xueqiu): mock logger via src path

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-05 03:35:12 +08:00

132 lines
4.8 KiB
TypeScript

import { CommandExecutionError } from '../../src/errors.js';
import { cli, Strategy } from '../../src/registry.js';
import type { IPage } from '../../src/types.js';
import {
buildProductUrl,
buildProvenance,
cleanText,
extractAsin,
PRIMARY_PRICE_SELECTORS,
parsePriceText,
parseRatingValue,
parseReviewCount,
normalizeProductUrl,
uniqueNonEmpty,
assertUsableState,
gotoAndReadState,
} from './shared.js';
interface ProductPayload {
href?: string;
title?: string;
product_title?: string | null;
byline?: string | null;
price_text?: string | null;
rating_text?: string | null;
review_count_text?: string | null;
review_url?: string | null;
qa_url?: string | null;
bullets?: string[];
breadcrumbs?: string[];
}
const PRODUCT_TITLE_SELECTOR = '#productTitle, #title span, [data-feature-name="title"] h1 span';
const BYLINE_SELECTOR = '#bylineInfo, [data-feature-name="bylineInfo"] #bylineInfo';
function normalizeProductPayload(payload: ProductPayload): Record<string, unknown> {
const sourceUrl = cleanText(payload.href) || buildProductUrl(cleanText(payload.product_title) || cleanText(payload.href));
const asin = extractAsin(payload.href ?? '') ?? null;
const price = parsePriceText(payload.price_text);
const ratingText = cleanText(payload.rating_text) || null;
const reviewCountText = cleanText(payload.review_count_text) || null;
const provenance = buildProvenance(sourceUrl);
return {
asin,
title: cleanText(payload.product_title) || cleanText(payload.title) || null,
product_url: normalizeProductUrl(payload.href),
...provenance,
brand_text: cleanText(payload.byline) || null,
price_text: price.price_text,
price_value: price.price_value,
currency: price.currency,
rating_text: ratingText,
rating_value: parseRatingValue(ratingText),
review_count_text: reviewCountText,
review_count: parseReviewCount(reviewCountText),
review_url: cleanText(payload.review_url) || null,
qa_url: cleanText(payload.qa_url) || null,
breadcrumbs: uniqueNonEmpty(payload.breadcrumbs ?? []),
bullet_points: uniqueNonEmpty(payload.bullets ?? []),
};
}
async function readProductPayload(page: IPage, input: string): Promise<ProductPayload> {
const url = buildProductUrl(input);
const state = await gotoAndReadState(page, url, 2500, 'product');
assertUsableState(state, 'product');
// Amazon can report a "stable" DOM before the product title block hydrates,
// especially when reconnecting to an existing shared CDP target.
await page.wait({ selector: PRODUCT_TITLE_SELECTOR, timeout: 6 }).catch(() => {});
return await page.evaluate(`
(() => ({
href: window.location.href,
title: document.title || '',
product_title: document.querySelector(${JSON.stringify(PRODUCT_TITLE_SELECTOR)})?.textContent || '',
byline: document.querySelector(${JSON.stringify(BYLINE_SELECTOR)})?.textContent || '',
price_text: (() => {
const selectors = ${JSON.stringify(PRIMARY_PRICE_SELECTORS)};
for (const selector of selectors) {
const text = document.querySelector(selector)?.textContent || '';
if (text.trim()) return text;
}
return '';
})(),
rating_text:
document.querySelector('#acrPopover')?.getAttribute('title')
|| document.querySelector('#acrPopover')?.textContent
|| '',
review_count_text: document.querySelector('#acrCustomerReviewText')?.textContent || '',
review_url: document.querySelector('a[href*="#customerReviews"]')?.href || '',
qa_url: document.querySelector('a[href*="ask/questions"]')?.href || '',
bullets: Array.from(document.querySelectorAll('#feature-bullets li .a-list-item')).map((node) => node.textContent || ''),
breadcrumbs: Array.from(document.querySelectorAll('#wayfinding-breadcrumbs_feature_div a')).map((node) => node.textContent || ''),
}))()
`) as ProductPayload;
}
cli({
site: 'amazon',
name: 'product',
description: 'Amazon product page facts for candidate validation',
domain: 'amazon.com',
strategy: Strategy.COOKIE,
navigateBefore: false,
args: [
{
name: 'input',
required: true,
positional: true,
help: 'ASIN or product URL, for example B0FJS72893',
},
],
columns: ['asin', 'title', 'price_text', 'rating_value', 'review_count'],
func: async (page, kwargs) => {
const input = String(kwargs.input ?? '');
const payload = await readProductPayload(page, input);
if (!cleanText(payload.product_title)) {
throw new CommandExecutionError(
'amazon product page did not expose product content',
'The product page may have changed or hit a robot check. Open the product page in Chrome and retry.',
);
}
return [normalizeProductPayload(payload)];
},
});
export const __test__ = {
normalizeProductPayload,
};