This commit is contained in:
2026-08-21 16:04:16 +08:00
parent dceec6cbed
commit 0e7df9052a
68 changed files with 3985 additions and 267 deletions
@@ -87,11 +87,11 @@ async function createManualSchedule(context, suffix, oceanBookingId, customerId)
}
export async function run(context) {
context.addCoverageGap(MODULE, OPTIONS.scenario, 'ZIM官网自动查询',
'ZIM启用hCaptcha,本期禁止绕过;真实契约返回MANUAL_REQUIRED并提供官方入口和人工结果绑定',
context.addCoverageGap(MODULE, OPTIONS.scenario, 'ZIM官方API自动查询',
'ZIM Cargo Tracking API 未配置订阅凭据或来源授权时返回MANUAL_REQUIRED;不记录或回显订阅键',
'external-third-party')
context.addCoverageGap(MODULE, OPTIONS.scenario, 'COSCO官网自动查询',
'COSCO为动态页面/iframe且没有稳定公开机器接口;真实契约返回MANUAL_REQUIRED,不伪造自动成功',
context.addCoverageGap(MODULE, OPTIONS.scenario, 'COSCO官方API自动查询',
'COSCO Shipping Open Platform Cargo Tracking API 待注册订阅和凭据;当前返回MANUAL_REQUIRED,不逆向网页WAF或伪造自动成功',
'external-third-party')
const suffix = context.marker.replace(/\D/g, '').slice(-10)
@@ -100,8 +100,10 @@ export async function run(context) {
const byCode = new Map((Array.isArray(carriers) ? carriers : []).map((item) => [item.carrierCode, item]))
context.assert(MODULE, '六家官方来源及合规模式完整', carriersResponse,
['MATSON', 'SML', 'ZIM', 'COSCO', 'EMC', 'HEDE'].every((code) => byCode.has(code))
&& byCode.get('ZIM')?.accessMode === 'HCAPTCHA' && byCode.get('ZIM')?.coverageGap === true
&& byCode.get('COSCO')?.accessMode === 'DYNAMIC_IFRAME' && byCode.get('COSCO')?.coverageGap === true
&& byCode.get('ZIM')?.accessMode === 'OFFICIAL_API' && byCode.get('ZIM')?.coverageGap === true
&& byCode.get('ZIM')?.coverageGapCode === 'OFFICIAL_API_CREDENTIALS_REQUIRED'
&& byCode.get('COSCO')?.accessMode === 'OFFICIAL_API' && byCode.get('COSCO')?.coverageGap === true
&& byCode.get('COSCO')?.coverageGapCode === 'OFFICIAL_API_CREDENTIALS_REQUIRED'
&& [...byCode.values()].every((item) => item.officialUrl?.startsWith('http') && item.automated === false),
`codes=${[...byCode.keys()].join(',') || 'missing'}`, OPTIONS)
@@ -130,29 +132,7 @@ export async function run(context) {
id(data(replay)?.query?.query?.id) === queryId,
`first=${queryId}, replay=${id(data(replay)?.query?.query?.id) || 'missing'}`, OPTIONS)
let version = Number(created.query.query.version)
for (let attemptNo = 1; attemptNo <= 3; attemptNo += 1) {
const response = await request(context, 'POST', `${BASE}/${queryId}/attempts`,
{ expectedVersion: version, reason: `第${attemptNo}次官网查询` },
{ 'Idempotency-Key': `${context.marker}-carrier-attempt-${attemptNo}`.slice(0, 128) })
const detail = context.apiSuccess(MODULE, `第${attemptNo}次查询返回结构化人工兜底`, response, '', OPTIONS)
context.assert(MODULE, `第${attemptNo}次查询记录来源和审计状态`, response,
detail?.query?.query?.status === 'MANUAL_REQUIRED'
&& Number(detail?.query?.query?.attemptCount) === attemptNo
&& detail?.attempts?.length === attemptNo
&& detail?.attempts?.at(-1)?.coverageGap === true
&& detail?.attempts?.at(-1)?.coverageGapCode === 'HCAPTCHA_REQUIRED'
&& Boolean(detail?.query?.query?.lastAttemptAt) && Boolean(detail?.query?.query?.manualReason),
`status=${detail?.query?.query?.status || 'missing'}, attempts=${detail?.query?.query?.attemptCount ?? 'missing'}`, OPTIONS)
version = Number(detail?.query?.query?.version)
}
const fourth = await request(context, 'POST', `${BASE}/${queryId}/attempts`,
{ expectedVersion: version, reason: '超过窗口上限' },
{ 'Idempotency-Key': `${context.marker}-carrier-attempt-4`.slice(0, 128) })
step(context, '同一3天窗口第4次查询被服务端拒绝', fourth,
fourth.http === 200 && ['A0409', '409'].includes(codeOf(fourth)),
`HTTP=${fourth.http}, code=${codeOf(fourth)}`)
const version = Number(created.query.query.version)
const unknownCarrier = await request(context, 'POST', BASE,
{ ...createBody, carrierCode: 'UNKNOWN', trackingNo: `${context.marker}-UNKNOWN` },
@@ -175,25 +155,17 @@ export async function run(context) {
{ 'Idempotency-Key': `${context.marker}-carrier-manual-result`.slice(0, 128) })
const completed = context.apiSuccess(MODULE, '绑定真实人工船期结果', manual, '', OPTIONS)
context.assert(MODULE, '人工兜底保留官网尝试且形成有效更新时间和过期状态', manual,
completed?.query?.query?.status === 'SUCCESS' && id(completed?.query?.query?.resultPortScheduleId) === scheduleId
completed?.query?.query?.status === 'READY' && id(completed?.query?.query?.resultPortScheduleId) === scheduleId
&& Boolean(completed?.query?.query?.lastSuccessAt) && Boolean(completed?.query?.query?.sourceUpdatedAt)
&& Boolean(completed?.query?.nextAllowedAt) && completed?.query?.expired === false
&& completed?.histories?.some((item) => item.action === 'MANUAL_RESULT'),
`status=${completed?.query?.query?.status || 'missing'}, result=${id(completed?.query?.query?.resultPortScheduleId) || 'missing'}`, OPTIONS)
const early = await request(context, 'POST', `${BASE}/${queryId}/attempts`,
{ expectedVersion: Number(completed?.query?.query?.version), reason: '未到三天再次查询' },
{ 'Idempotency-Key': `${context.marker}-carrier-too-early`.slice(0, 128) })
step(context, '成功后未到3天禁止再次查询', early,
early.http === 200 && ['A0409', '409'].includes(codeOf(early)),
`HTTP=${early.http}, code=${codeOf(early)}`)
const detailResponse = await request(context, 'GET', `${BASE}/${queryId}`)
const detail = context.apiSuccess(MODULE, '回查船司查询任务详情与历史', detailResponse, '', OPTIONS)
context.assert(MODULE, '查询尝试和版本历史完整且不伪装自动成功', detailResponse,
detail?.attempts?.length === 3 && detail?.attempts?.every((item) => item.attemptStatus === 'MANUAL_REQUIRED')
context.assert(MODULE, '平台详情不暴露调度尝试且保留业务版本历史', detailResponse,
!Object.hasOwn(detail || {}, 'attempts')
&& detail?.histories?.some((item) => item.action === 'CREATE')
&& detail?.histories?.filter((item) => item.action === 'ATTEMPT').length === 3
&& detail?.histories?.some((item) => item.action === 'MANUAL_RESULT'),
`attempts=${detail?.attempts?.length ?? 'missing'}, histories=${detail?.histories?.length ?? 'missing'}`, OPTIONS)
`attemptsExposed=${Object.hasOwn(detail || {}, 'attempts')}, histories=${detail?.histories?.length ?? 'missing'}`, OPTIONS)
}