jd-detail-diagnostic.mjs 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. #!/usr/bin/env node
  2. import crypto from 'node:crypto';
  3. const trim = (value) => String(value || '').replace(/\/+$/, '');
  4. const sourceHeaders = {
  5. 'X-Parse-Application-Id': process.env.JD_SOURCE_PARSE_APP_ID,
  6. 'X-Parse-Master-Key': process.env.JD_SOURCE_PARSE_MASTER_KEY,
  7. };
  8. const authQuery = new URLSearchParams({
  9. where: JSON.stringify({ platform: 'jd', type: 'access_token' }),
  10. limit: '1',
  11. order: '-createdAt',
  12. });
  13. const productId = process.argv[2];
  14. if (!productId) throw new Error('usage: node tools/jd-detail-diagnostic.mjs <productId>');
  15. let successfulPayload = null;
  16. async function loadToken() {
  17. const url = `${trim(process.env.JD_SOURCE_PARSE_URL)}/classes/EcomAuth?${authQuery}`;
  18. for (let attempt = 1; attempt <= 4; attempt += 1) {
  19. const response = await fetch(url, { headers: sourceHeaders });
  20. const payload = await response.json().catch(() => null);
  21. const transientUnauthorized = response.status === 403
  22. && payload?.code === 119
  23. && /unauthorized/i.test(String(payload?.error || ''));
  24. if (response.ok) {
  25. const accessToken = payload?.results?.[0]?.data?.access_token;
  26. if (!accessToken) throw new Error('missing_token');
  27. return accessToken;
  28. }
  29. if (!transientUnauthorized || attempt === 4) {
  30. throw new Error(`source_parse_${response.status}`);
  31. }
  32. await new Promise((resolve) => setTimeout(resolve, 250 * attempt));
  33. }
  34. throw new Error('source_parse_retry_exhausted');
  35. }
  36. const token = await loadToken();
  37. function signature(fields) {
  38. const plain = Object.keys(fields).sort().map((key) => `${key}${fields[key] ?? ''}`).join('');
  39. return crypto.createHash('md5')
  40. .update(`${process.env.JD_APP_SECRET}${plain}${process.env.JD_APP_SECRET}`)
  41. .digest('hex')
  42. .toUpperCase();
  43. }
  44. async function probe(name, query, pathFields) {
  45. const timestamp = String(Date.now());
  46. const common = {
  47. 'X-JOS-App-Key': process.env.JD_APP_KEY,
  48. 'X-JOS-Access-Token': token,
  49. 'X-JOS-Timestamp': timestamp,
  50. };
  51. const signed = { ...query, ...pathFields, ...common };
  52. const headers = {
  53. ...common,
  54. 'X-JOS-Sign-Method': 'md5',
  55. 'X-JOS-Request-Identity': 'vender',
  56. 'X-JOS-Sign': signature(signed),
  57. };
  58. const queryString = new URLSearchParams(
  59. Object.entries(query).map(([key, value]) => [key, String(value)]),
  60. );
  61. const response = await fetch(
  62. `https://api-cn.jd.com/rest/sp-product/v0/products/${productId}?${queryString}`,
  63. { headers },
  64. );
  65. let body = {};
  66. try { body = await response.json(); } catch {}
  67. if (response.ok && body?.success === true && successfulPayload == null) successfulPayload = body;
  68. const error = body?.errorList?.[0] || body?.error || {};
  69. const traceHeaders = {};
  70. for (const [key, value] of response.headers) {
  71. if (/request|trace/i.test(key)) traceHeaders[key] = value;
  72. }
  73. console.log(JSON.stringify({
  74. name,
  75. httpStatus: response.status,
  76. success: body?.success ?? null,
  77. errorCode: error?.code ?? body?.code ?? null,
  78. errorMessage: error?.message ?? error?.details ?? body?.message ?? null,
  79. signedFields: Object.keys(signed).sort(),
  80. traceHeaders,
  81. }, null, 2));
  82. }
  83. await probe('current_scene_without_path_field', { scene: 'pop' }, {});
  84. await probe('scene_with_productId_path_field', { scene: 'pop' }, { productId });
  85. await probe('request_json_with_path_field', { request: JSON.stringify({ scene: 'pop' }) }, { productId });
  86. await probe('request_json_without_path_field', { request: JSON.stringify({ scene: 'pop' }) }, {});
  87. function collectFieldPaths(value, prefix = '', paths = [], depth = 0) {
  88. if (depth > 8 || paths.length >= 800 || value == null) return paths;
  89. if (Array.isArray(value)) {
  90. paths.push({ path: `${prefix}[]`, type: 'Array', count: value.length });
  91. if (value[0] !== undefined) collectFieldPaths(value[0], `${prefix}[]`, paths, depth + 1);
  92. return paths;
  93. }
  94. if (typeof value !== 'object') {
  95. paths.push({ path: prefix, type: typeof value });
  96. return paths;
  97. }
  98. for (const [key, child] of Object.entries(value)) {
  99. const path = prefix ? `${prefix}.${key}` : key;
  100. if (child && typeof child === 'object') {
  101. paths.push({ path, type: Array.isArray(child) ? 'Array' : 'Object' });
  102. collectFieldPaths(child, path, paths, depth + 1);
  103. } else {
  104. paths.push({ path, type: child === null ? 'null' : typeof child });
  105. }
  106. }
  107. return paths;
  108. }
  109. console.log(JSON.stringify({
  110. successfulResponseFieldPaths: successfulPayload
  111. ? collectFieldPaths(successfulPayload.data ?? successfulPayload)
  112. : [],
  113. }, null, 2));