universal-parcel-scraper 0.240.0-main.1505 → 0.241.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/carriers/china-post/adapter.d.ts +5 -1
- package/dist/carriers/china-post/adapter.js +35 -5
- package/dist/carriers/china-post/carrier.json +2 -1
- package/dist/carriers/china-post/parser.d.ts +1 -2
- package/dist/carriers/china-post/parser.js +1 -2
- package/dist/carriers/china-post/web.d.ts +7 -0
- package/dist/carriers/china-post/web.js +80 -0
- package/dist/server/openapi.json +1 -1
- package/package.json +1 -1
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import { type AdapterFactory, type TrackingContext } from '../../core/adapter/index.js';
|
|
1
|
+
import { type AdapterEnvironment, type AdapterFactory, type TrackingContext } from '../../core/adapter/index.js';
|
|
2
2
|
import type { CarrierResult } from '../../core/result/index.js';
|
|
3
|
+
import type { StepRecorder } from '../../core/telemetry/index.js';
|
|
3
4
|
export declare class ChinaPostTracker {
|
|
4
5
|
#private;
|
|
5
6
|
private readonly options;
|
|
6
7
|
constructor(options?: {
|
|
7
8
|
fetcher?: typeof fetch;
|
|
8
9
|
userAgent?: string;
|
|
10
|
+
trawl?: AdapterEnvironment['trawl'];
|
|
11
|
+
recorder?: StepRecorder;
|
|
9
12
|
});
|
|
13
|
+
direct(raw: string, context?: TrackingContext): Promise<CarrierResult>;
|
|
10
14
|
fetch(raw: string, context?: TrackingContext): Promise<CarrierResult>;
|
|
11
15
|
}
|
|
12
16
|
export declare const adapter: AdapterFactory;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { accepted, lookupBudget, recognizeFromLookup } from '../../core/adapter/index.js';
|
|
2
|
-
import { carrierErrorKind } from '../../core/errors/index.js';
|
|
2
|
+
import { BudgetExceededError, carrierErrorKind } from '../../core/errors/index.js';
|
|
3
|
+
import { recoverableByDefault, runSteps } from '../../core/runner/index.js';
|
|
3
4
|
import { userAgentOf } from '../../core/transport/index.js';
|
|
4
5
|
import { readChinaPostTraces } from './app.js';
|
|
5
6
|
import { normalizeChinaPostNumber, parseChinaPostTraces, PROVIDER } from './parser.js';
|
|
7
|
+
import { CHINA_POST_WEB, CHINA_POST_WEB_FLAG, CHINA_POST_WEB_TRACE, parseChinaPostWebPreview } from './web.js';
|
|
6
8
|
const BUDGET_MS = 15_000;
|
|
7
9
|
export class ChinaPostTracker {
|
|
8
10
|
options;
|
|
@@ -11,23 +13,51 @@ export class ChinaPostTracker {
|
|
|
11
13
|
this.options = options;
|
|
12
14
|
this.#userAgent = userAgentOf(options.userAgent);
|
|
13
15
|
}
|
|
14
|
-
async
|
|
16
|
+
async direct(raw, context = {}) {
|
|
15
17
|
const number = normalizeChinaPostNumber(raw);
|
|
16
18
|
const budget = lookupBudget(context, BUDGET_MS, PROVIDER);
|
|
17
19
|
const payload = await readChinaPostTraces(number, { fetcher: this.options.fetcher, userAgent: this.#userAgent,
|
|
18
20
|
budget, callerSignal: context.signal });
|
|
19
21
|
return parseChinaPostTraces(payload, number);
|
|
20
22
|
}
|
|
23
|
+
async fetch(raw, context = {}) {
|
|
24
|
+
const number = normalizeChinaPostNumber(raw);
|
|
25
|
+
const budgetMs = context.budgetMs ?? (this.options.trawl ? 60_000 : BUDGET_MS);
|
|
26
|
+
const failed = (error, signal) => {
|
|
27
|
+
context.signal?.throwIfAborted();
|
|
28
|
+
if (signal.aborted)
|
|
29
|
+
throw new BudgetExceededError(PROVIDER, budgetMs);
|
|
30
|
+
throw error;
|
|
31
|
+
};
|
|
32
|
+
return runSteps({ carrier: 'china-post', budgetMs,
|
|
33
|
+
signal: context.signal, recorder: this.options.recorder }, [
|
|
34
|
+
{ id: 'direct', run: ({ signal, remainingMs }) => this.direct(number, { signal, budgetMs: remainingMs }).catch(error => failed(error, signal)) },
|
|
35
|
+
{ id: 'trawl', enabled: !!this.options.trawl,
|
|
36
|
+
// The public web preview is anonymous even when the app asks for phone digits.
|
|
37
|
+
recovers: error => carrierErrorKind(error) === 'input_required'
|
|
38
|
+
|| (carrierErrorKind(error) !== 'indeterminate' && recoverableByDefault(error)),
|
|
39
|
+
run: async ({ signal, remainingMs }) => {
|
|
40
|
+
const timeoutMs = Math.max(1, Math.floor(remainingMs - 2500));
|
|
41
|
+
const page = await this.options.trawl.scrape({ url: `${CHINA_POST_WEB}?mailNo=${number}`, skipHttp: true, maxTier: 3,
|
|
42
|
+
maxTimeout: timeoutMs, captureResponses: [CHINA_POST_WEB_FLAG, CHINA_POST_WEB_TRACE] }, { provider: PROVIDER, timeoutMs, signal, requireSolved: false, captureWindow: 'last', maxBytes: 4_000_000 }).catch(error => failed(error, signal));
|
|
43
|
+
if (signal.aborted)
|
|
44
|
+
failed(signal.reason, signal);
|
|
45
|
+
return parseChinaPostWebPreview(page, number);
|
|
46
|
+
} },
|
|
47
|
+
]);
|
|
48
|
+
}
|
|
21
49
|
}
|
|
22
50
|
export const adapter = (environment) => {
|
|
23
|
-
const tracker = new ChinaPostTracker({ fetcher: environment.fetcher, userAgent: environment.userAgent
|
|
51
|
+
const tracker = new ChinaPostTracker({ fetcher: environment.fetcher, userAgent: environment.userAgent,
|
|
52
|
+
trawl: environment.trawl, recorder: environment.recorder });
|
|
24
53
|
return {
|
|
25
54
|
id: 'china-post',
|
|
26
|
-
|
|
55
|
+
recordsSteps: true,
|
|
56
|
+
steps: ['direct', 'trawl'],
|
|
27
57
|
track: (input, context) => tracker.fetch(input.number, context),
|
|
28
58
|
recognize: async (number, context) => {
|
|
29
59
|
try {
|
|
30
|
-
return await recognizeFromLookup(() => tracker.
|
|
60
|
+
return await recognizeFromLookup(() => tracker.direct(number, context), () => accepted(() => normalizeChinaPostNumber(number)));
|
|
31
61
|
}
|
|
32
62
|
catch (error) {
|
|
33
63
|
// The check step asks for a contact phone's digits only about an item China Post holds;
|
|
@@ -3,8 +3,7 @@ import type { CarrierResult } from '../../core/result/index.js';
|
|
|
3
3
|
export declare const PROVIDER = "China Post";
|
|
4
4
|
/**
|
|
5
5
|
* S10 numbers issued in China, and ZS numbers of eleven digits. The service
|
|
6
|
-
*
|
|
7
|
-
* digits, so their lookups end at the check step.
|
|
6
|
+
* asks for phone digits on ZS numbers; the public web preview is separate.
|
|
8
7
|
*/
|
|
9
8
|
export declare function normalizeChinaPostNumber(raw: string): string;
|
|
10
9
|
export interface ChinaPostReply {
|
|
@@ -15,8 +15,7 @@ const GUEST_SCANS = 3;
|
|
|
15
15
|
const ACCEPTED = '已揽收';
|
|
16
16
|
/**
|
|
17
17
|
* S10 numbers issued in China, and ZS numbers of eleven digits. The service
|
|
18
|
-
*
|
|
19
|
-
* digits, so their lookups end at the check step.
|
|
18
|
+
* asks for phone digits on ZS numbers; the public web preview is separate.
|
|
20
19
|
*/
|
|
21
20
|
export function normalizeChinaPostNumber(raw) {
|
|
22
21
|
const number = normalizeTrackingNumber(raw);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CarrierResult } from '../../core/result/index.js';
|
|
2
|
+
import type { TrawlScrapeResponse } from '../../core/transport/index.js';
|
|
3
|
+
export declare const CHINA_POST_WEB = "https://www.ems.com.cn/queryList";
|
|
4
|
+
export declare const CHINA_POST_WEB_FLAG = "https://www.ems.com.cn/ems-web/trackTestQuery/getLogisticsTestFlag";
|
|
5
|
+
export declare const CHINA_POST_WEB_TRACE = "https://www.ems.com.cn/ems-web/mailTrack/queryTrack";
|
|
6
|
+
/** Read only the helper's identity-bound, privacy-filtered guest preview. */
|
|
7
|
+
export declare function parseChinaPostWebPreview(page: TrawlScrapeResponse, number: string): CarrierResult;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { DateTime } from 'luxon';
|
|
2
|
+
import { ChallengeError, IndeterminateError, InputRequiredError, RateLimitedError, SchemaError, TransportError, UpstreamHttpError } from '../../core/errors/index.js';
|
|
3
|
+
import { isRecord } from '../../core/types.js';
|
|
4
|
+
import { chinaPostDescription, PROVIDER } from './parser.js';
|
|
5
|
+
import { chinaPostStatus } from './status.js';
|
|
6
|
+
export const CHINA_POST_WEB = 'https://www.ems.com.cn/queryList';
|
|
7
|
+
export const CHINA_POST_WEB_FLAG = 'https://www.ems.com.cn/ems-web/trackTestQuery/getLogisticsTestFlag';
|
|
8
|
+
export const CHINA_POST_WEB_TRACE = 'https://www.ems.com.cn/ems-web/mailTrack/queryTrack';
|
|
9
|
+
/** Read only the helper's identity-bound, privacy-filtered guest preview. */
|
|
10
|
+
export function parseChinaPostWebPreview(page, number) {
|
|
11
|
+
const entries = page.capturedResponses.filter(row => [CHINA_POST_WEB_FLAG, CHINA_POST_WEB_TRACE].includes(row.url));
|
|
12
|
+
const entry = entries.at(-1);
|
|
13
|
+
if (!entry || entries.length > 10)
|
|
14
|
+
throw new ChallengeError(PROVIDER, 'China Post returned no verified guest preview');
|
|
15
|
+
if (entry.trackingNumberSubmitted !== number)
|
|
16
|
+
throw new SchemaError(PROVIDER, 'China Post browser request did not match the shipment');
|
|
17
|
+
if (entry.status === 429) {
|
|
18
|
+
const retry = entry.headers['retry-after'] ?? '';
|
|
19
|
+
const delay = /^\d+$/.test(retry) ? Number(retry) * 1000 : Date.parse(retry) - Date.now();
|
|
20
|
+
throw new RateLimitedError(PROVIDER, Number.isFinite(delay) ? Math.max(0, delay) : undefined);
|
|
21
|
+
}
|
|
22
|
+
if ([401, 403, 405, 412].includes(entry.status))
|
|
23
|
+
throw new ChallengeError(PROVIDER, 'China Post refused the guest preview');
|
|
24
|
+
if (entry.status >= 500)
|
|
25
|
+
throw new UpstreamHttpError(PROVIDER, entry.status);
|
|
26
|
+
if (entry.status !== 200)
|
|
27
|
+
throw new TransportError(PROVIDER, 'China Post guest preview is unavailable', { status: entry.status });
|
|
28
|
+
if (entry.error === 'invalid-json')
|
|
29
|
+
throw new SchemaError(PROVIDER, 'China Post returned invalid guest preview JSON');
|
|
30
|
+
if (entry.error || entry.truncated || entry.base64Encoded || entry.body === null || Buffer.byteLength(entry.body) > 1_000_000) {
|
|
31
|
+
throw new TransportError(PROVIDER, 'China Post returned an incomplete guest preview');
|
|
32
|
+
}
|
|
33
|
+
let payload;
|
|
34
|
+
try {
|
|
35
|
+
payload = JSON.parse(entry.body);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new SchemaError(PROVIDER, 'China Post returned invalid guest preview JSON');
|
|
39
|
+
}
|
|
40
|
+
if (!isRecord(payload) || typeof payload.success !== 'boolean')
|
|
41
|
+
throw new SchemaError(PROVIDER, 'China Post returned an invalid guest preview');
|
|
42
|
+
if (!payload.success) {
|
|
43
|
+
if (payload.reason === 'rate_limit')
|
|
44
|
+
throw new RateLimitedError(PROVIDER);
|
|
45
|
+
if (payload.reason === 'phone')
|
|
46
|
+
throw new InputRequiredError(PROVIDER, 'recipient verification', 'China Post asks for contact phone verification');
|
|
47
|
+
if (payload.reason === 'empty')
|
|
48
|
+
throw new IndeterminateError(PROVIDER, 'China Post has no guest tracking information for this number');
|
|
49
|
+
if (payload.reason === 'verification')
|
|
50
|
+
throw new ChallengeError(PROVIDER, 'China Post refused the character verification');
|
|
51
|
+
throw new IndeterminateError(PROVIDER, 'China Post did not open the guest preview');
|
|
52
|
+
}
|
|
53
|
+
if (entry.url !== CHINA_POST_WEB_TRACE)
|
|
54
|
+
throw new ChallengeError(PROVIDER, 'China Post verification returned no tracking preview');
|
|
55
|
+
if (payload.match !== true || !Array.isArray(payload.scans) || payload.scans.length > 100) {
|
|
56
|
+
throw new SchemaError(PROVIDER, 'China Post guest preview did not match the shipment');
|
|
57
|
+
}
|
|
58
|
+
if (!payload.scans.length)
|
|
59
|
+
throw new IndeterminateError(PROVIDER, 'China Post returned no guest scans');
|
|
60
|
+
const scans = payload.scans;
|
|
61
|
+
const events = [];
|
|
62
|
+
for (const scan of [...scans].reverse()) {
|
|
63
|
+
if (!isRecord(scan) || scan.number !== number || typeof scan.time !== 'string'
|
|
64
|
+
|| !/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}(?::\d{2})?$/.test(scan.time)
|
|
65
|
+
|| typeof scan.code !== 'string' || !/^[A-Z0-9_]{0,20}$/.test(scan.code) || typeof scan.state !== 'string') {
|
|
66
|
+
throw new SchemaError(PROVIDER, 'China Post returned an invalid guest scan');
|
|
67
|
+
}
|
|
68
|
+
const date = DateTime.fromISO(scan.time.replace(' ', 'T'), { zone: 'UTC' });
|
|
69
|
+
const description = chinaPostDescription('', scan.state);
|
|
70
|
+
if (!date.isValid || !description)
|
|
71
|
+
throw new SchemaError(PROVIDER, 'China Post returned an unreadable guest scan');
|
|
72
|
+
const stage = chinaPostStatus(scan.code, description)?.stage;
|
|
73
|
+
events.push({ local_time: date.toFormat("yyyy-MM-dd'T'HH:mm:ss"), description,
|
|
74
|
+
...(scan.code ? { provider_code: scan.code } : {}), ...(stage ? { stage, stage_source: 'carrier_map' } : {}) });
|
|
75
|
+
}
|
|
76
|
+
const current = chinaPostStatus(events[0].provider_code ?? '', events[0].description ?? '');
|
|
77
|
+
return { status: current?.status ?? 'unknown', ...(current ? { current_stage: current.stage, current_stage_source: 'carrier_map' } : {}),
|
|
78
|
+
last_status_text: events[0].description, last_update: null, last_update_local: events[0].local_time,
|
|
79
|
+
history_truncated: true, events };
|
|
80
|
+
}
|
package/dist/server/openapi.json
CHANGED
package/package.json
CHANGED