#!/usr/bin/env node import crypto from 'node:crypto'; const trim = (value) => String(value || '').replace(/\/+$/, ''); const sourceHeaders = { 'X-Parse-Application-Id': process.env.JD_SOURCE_PARSE_APP_ID, 'X-Parse-Master-Key': process.env.JD_SOURCE_PARSE_MASTER_KEY, }; const authQuery = new URLSearchParams({ where: JSON.stringify({ platform: 'jd', type: 'access_token' }), limit: '1', order: '-createdAt', }); const productId = process.argv[2]; if (!productId) throw new Error('usage: node tools/jd-detail-diagnostic.mjs '); let successfulPayload = null; async function loadToken() { const url = `${trim(process.env.JD_SOURCE_PARSE_URL)}/classes/EcomAuth?${authQuery}`; for (let attempt = 1; attempt <= 4; attempt += 1) { const response = await fetch(url, { headers: sourceHeaders }); const payload = await response.json().catch(() => null); const transientUnauthorized = response.status === 403 && payload?.code === 119 && /unauthorized/i.test(String(payload?.error || '')); if (response.ok) { const accessToken = payload?.results?.[0]?.data?.access_token; if (!accessToken) throw new Error('missing_token'); return accessToken; } if (!transientUnauthorized || attempt === 4) { throw new Error(`source_parse_${response.status}`); } await new Promise((resolve) => setTimeout(resolve, 250 * attempt)); } throw new Error('source_parse_retry_exhausted'); } const token = await loadToken(); function signature(fields) { const plain = Object.keys(fields).sort().map((key) => `${key}${fields[key] ?? ''}`).join(''); return crypto.createHash('md5') .update(`${process.env.JD_APP_SECRET}${plain}${process.env.JD_APP_SECRET}`) .digest('hex') .toUpperCase(); } async function probe(name, query, pathFields) { const timestamp = String(Date.now()); const common = { 'X-JOS-App-Key': process.env.JD_APP_KEY, 'X-JOS-Access-Token': token, 'X-JOS-Timestamp': timestamp, }; const signed = { ...query, ...pathFields, ...common }; const headers = { ...common, 'X-JOS-Sign-Method': 'md5', 'X-JOS-Request-Identity': 'vender', 'X-JOS-Sign': signature(signed), }; const queryString = new URLSearchParams( Object.entries(query).map(([key, value]) => [key, String(value)]), ); const response = await fetch( `https://api-cn.jd.com/rest/sp-product/v0/products/${productId}?${queryString}`, { headers }, ); let body = {}; try { body = await response.json(); } catch {} if (response.ok && body?.success === true && successfulPayload == null) successfulPayload = body; const error = body?.errorList?.[0] || body?.error || {}; const traceHeaders = {}; for (const [key, value] of response.headers) { if (/request|trace/i.test(key)) traceHeaders[key] = value; } console.log(JSON.stringify({ name, httpStatus: response.status, success: body?.success ?? null, errorCode: error?.code ?? body?.code ?? null, errorMessage: error?.message ?? error?.details ?? body?.message ?? null, signedFields: Object.keys(signed).sort(), traceHeaders, }, null, 2)); } await probe('current_scene_without_path_field', { scene: 'pop' }, {}); await probe('scene_with_productId_path_field', { scene: 'pop' }, { productId }); await probe('request_json_with_path_field', { request: JSON.stringify({ scene: 'pop' }) }, { productId }); await probe('request_json_without_path_field', { request: JSON.stringify({ scene: 'pop' }) }, {}); function collectFieldPaths(value, prefix = '', paths = [], depth = 0) { if (depth > 8 || paths.length >= 800 || value == null) return paths; if (Array.isArray(value)) { paths.push({ path: `${prefix}[]`, type: 'Array', count: value.length }); if (value[0] !== undefined) collectFieldPaths(value[0], `${prefix}[]`, paths, depth + 1); return paths; } if (typeof value !== 'object') { paths.push({ path: prefix, type: typeof value }); return paths; } for (const [key, child] of Object.entries(value)) { const path = prefix ? `${prefix}.${key}` : key; if (child && typeof child === 'object') { paths.push({ path, type: Array.isArray(child) ? 'Array' : 'Object' }); collectFieldPaths(child, path, paths, depth + 1); } else { paths.push({ path, type: child === null ? 'null' : typeof child }); } } return paths; } console.log(JSON.stringify({ successfulResponseFieldPaths: successfulPayload ? collectFieldPaths(successfulPayload.data ?? successfulPayload) : [], }, null, 2));