| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122 |
- #!/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 <productId>');
- 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));
|