@danceiny/gotry 0.0.1-rc.21 → 0.0.1-rc.24
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/README.md +102 -194
- package/README.zh-CN.md +115 -210
- package/bin/gotry-backend.js +53 -0
- package/bin/gotry-bootstrap.js +466 -35
- package/bin/gotry-inner.js +255 -36
- package/bin/gotry-process-liveness.d.ts +27 -0
- package/bin/gotry-process-liveness.js +97 -0
- package/bin/gotry-runtime-resolution.d.ts +1 -1
- package/bin/gotry-runtime-resolution.js +2 -2
- package/client/client.js +174 -0
- package/cordis.gotry-patch.yml +70 -93
- package/dist/capabilities/agent-reach-deep.js +45 -122
- package/dist/capabilities/agent-reach.js +63 -148
- package/dist/capabilities/anything.js +89 -75
- package/dist/capabilities/artifacts.js +123 -104
- package/dist/capabilities/channel-health.js +65 -45
- package/dist/capabilities/channel-registry.js +108 -87
- package/dist/capabilities/doctor.js +357 -235
- package/dist/capabilities/effect.js +223 -309
- package/dist/capabilities/fact-log.js +28 -14
- package/dist/capabilities/flyai.js +245 -156
- package/dist/capabilities/ground-transfer.js +509 -0
- package/dist/capabilities/hbcli.js +144 -148
- package/dist/capabilities/hotelbyte-transaction.js +194 -0
- package/dist/capabilities/incident-log.js +131 -61
- package/dist/capabilities/model-override.js +30 -10
- package/dist/capabilities/opensky.js +54 -43
- package/dist/capabilities/resilience.js +50 -32
- package/dist/capabilities/session/action-cache.js +50 -44
- package/dist/capabilities/session/adapters/ctrip-flight.js +119 -62
- package/dist/capabilities/session/adapters/ctrip-hotel.js +125 -110
- package/dist/capabilities/session/adapters/dida-portal.js +206 -0
- package/dist/capabilities/session/adapters/meituan-local.js +35 -54
- package/dist/capabilities/session/adapters/rail-12306.js +169 -112
- package/dist/capabilities/session/benchmark.js +85 -149
- package/dist/capabilities/session/extension-bridge.js +320 -259
- package/dist/capabilities/session/extension-channel.js +57 -76
- package/dist/capabilities/session/extension-distribution.js +115 -119
- package/dist/capabilities/session/extract.js +31 -21
- package/dist/capabilities/session/golden-score.js +55 -33
- package/dist/capabilities/session/health-watch.js +146 -154
- package/dist/capabilities/session/read-guard.js +55 -47
- package/dist/capabilities/session/static-flight-golden.js +49 -36
- package/dist/capabilities/session/transport.js +54 -61
- package/dist/capabilities/session/wizard.js +63 -93
- package/dist/capabilities/session-consent.js +143 -47
- package/dist/capabilities/session-login.js +95 -126
- package/dist/capabilities/session-search.js +391 -253
- package/dist/capabilities/sponsor-plugin.js +341 -0
- package/dist/capabilities/visa-policy.js +87 -73
- package/dist/capabilities/weather.js +163 -235
- package/dist/scripts/action-cache-tests.js +24 -59
- package/dist/scripts/agent-planning-turn-deadline-e2e.js +91 -142
- package/dist/scripts/agent-planning-turn-deadline-tests.js +80 -176
- package/dist/scripts/agent-reach-deep-tests.js +30 -40
- package/dist/scripts/agent-reach-tests.js +26 -18
- package/dist/scripts/agent-reach-wrapper-tests.js +46 -50
- package/dist/scripts/anything-tests.js +58 -62
- package/dist/scripts/artifact-client-contract-tests.js +69 -0
- package/dist/scripts/artifacts-capability-tests.js +235 -0
- package/dist/scripts/async-collect.js +26 -15
- package/dist/scripts/benchmark-environment-bridge-e2e.js +287 -684
- package/dist/scripts/benchmark-environment-bridge-tests.js +818 -3123
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +66 -279
- package/dist/scripts/booking-copilot-availability-policy-tests.js +153 -1257
- package/dist/scripts/booking-copilot-bin-proof-tests.js +22 -45
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +64 -76
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +69 -199
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +393 -759
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +33 -75
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +60 -110
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +28 -66
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +72 -271
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +73 -275
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +784 -3398
- package/dist/scripts/booking-copilot-server-proof-tests.js +63 -168
- package/dist/scripts/booking-copilot-startup-proof-tests.js +38 -124
- package/dist/scripts/booking-executor-tests.js +80 -0
- package/dist/scripts/booking-recovery-chain-tests.js +182 -0
- package/dist/scripts/booking-saga-tests.js +61 -123
- package/dist/scripts/booking-surface-contract-proof-tests.js +152 -1167
- package/dist/scripts/bootstrap-tests.js +1383 -128
- package/dist/scripts/build-changelog.js +89 -130
- package/dist/scripts/build-metrics-report.js +135 -143
- package/dist/scripts/changelog-tests.js +35 -82
- package/dist/scripts/channel-probe-tests.js +44 -116
- package/dist/scripts/channel-probe.js +84 -167
- package/dist/scripts/channel-registry-tests.js +97 -185
- package/dist/scripts/companion-tests.js +25 -70
- package/dist/scripts/dida-cookie-probe.js +87 -0
- package/dist/scripts/dida-sw-probe.js +114 -0
- package/dist/scripts/diff-test.js +17 -12
- package/dist/scripts/doctor-tests.js +428 -120
- package/dist/scripts/dsh-artifact-e2e.js +150 -0
- package/dist/scripts/dsh-artifact-web-e2e.js +443 -0
- package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
- package/dist/scripts/dsh-runtime-closure-tests.js +110 -153
- package/dist/scripts/dsh-runtime-closure.js +75 -63
- package/dist/scripts/dsh-session-v3-migration-proof.js +106 -0
- package/dist/scripts/dsh-subprocess-local-proof.js +149 -0
- package/dist/scripts/dsh-target-closure-proof.js +178 -0
- package/dist/scripts/effect-tests.js +165 -552
- package/dist/scripts/engine-run.js +1 -3
- package/dist/scripts/engine-tests.js +16 -10
- package/dist/scripts/evaluation-cadence-tests.js +69 -242
- package/dist/scripts/evaluation-contract-tests.js +193 -499
- package/dist/scripts/extension-distribution-cli.js +19 -15
- package/dist/scripts/extension-distribution-tests.js +98 -211
- package/dist/scripts/extension-tests.js +366 -389
- package/dist/scripts/fact-gate-tests.js +725 -263
- package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
- package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
- package/dist/scripts/flight-malformed-tests.js +159 -0
- package/dist/scripts/flyai-tests.js +325 -68
- package/dist/scripts/g5-guard-tests.js +131 -0
- package/dist/scripts/g5-guard.js +189 -0
- package/dist/scripts/gen-lunar-anchors.js +106 -0
- package/dist/scripts/gotry-backend-kernel-tests.js +79 -0
- package/dist/scripts/ground-transfer-tests.js +956 -0
- package/dist/scripts/hbcli-e2e-tests.js +71 -90
- package/dist/scripts/hbcli-live-optin-tests.js +96 -0
- package/dist/scripts/hbcli-release-contract-tests.js +122 -0
- package/dist/scripts/hbcli-tests.js +41 -115
- package/dist/scripts/health-watch-cli.js +42 -28
- package/dist/scripts/hotel-date-gate-tests.js +496 -0
- package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
- package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
- package/dist/scripts/i18n-tests.js +16 -9
- package/dist/scripts/incident-tests.js +281 -87
- package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
- package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
- package/dist/scripts/issue-271-liveness-tests.js +547 -0
- package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
- package/dist/scripts/issue-289-model-retry-tests.js +441 -0
- package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
- package/dist/scripts/issue-338-home-city-e2e.js +577 -0
- package/dist/scripts/issue343-real-entry-e2e.js +160 -0
- package/dist/scripts/journey-tests.js +22 -27
- package/dist/scripts/kernel-manifest-evidence.js +109 -0
- package/dist/scripts/kernel-manifest-gate.js +126 -0
- package/dist/scripts/kernel-manifest-tests.js +212 -0
- package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
- package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
- package/dist/scripts/kernel-manifest.js +369 -0
- package/dist/scripts/ledger-repair-apply-tests.js +316 -0
- package/dist/scripts/ledger-repair-plan-tests.js +375 -0
- package/dist/scripts/ledger-tests.js +325 -340
- package/dist/scripts/ledger-workflow-crash.js +10 -20
- package/dist/scripts/map-tools-vendor-package-proof.js +206 -134
- package/dist/scripts/memory-capture-tests.js +242 -61
- package/dist/scripts/memory-decay-tests.js +18 -52
- package/dist/scripts/memory-lifecycle-tests.js +657 -0
- package/dist/scripts/memory-lifecycle.js +196 -0
- package/dist/scripts/memory-metrics.js +24 -12
- package/dist/scripts/memory-value-report.js +474 -222
- package/dist/scripts/memory-value-tests.js +328 -0
- package/dist/scripts/metrics-report-tests.js +62 -199
- package/dist/scripts/model-override-e2e.js +66 -105
- package/dist/scripts/nightly-evidence-tests.js +64 -89
- package/dist/scripts/nightly-evidence.js +108 -111
- package/dist/scripts/nudge-digest.js +39 -28
- package/dist/scripts/onboarding-tests.js +63 -88
- package/dist/scripts/opensky-check.js +18 -13
- package/dist/scripts/opensky-tests.js +28 -28
- package/dist/scripts/persona-surface-guard-tests.js +37 -12
- package/dist/scripts/pnpm-dsh-closure-proof.js +2 -5
- package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
- package/dist/scripts/price-drift-tests.js +126 -227
- package/dist/scripts/price-drift-watch.js +209 -253
- package/dist/scripts/probe-poi-tests.js +52 -45
- package/dist/scripts/product-metrics.js +187 -273
- package/dist/scripts/publish-preverify.js +47 -41
- package/dist/scripts/realtime-pricing-tests.js +37 -76
- package/dist/scripts/replay-async.js +20 -18
- package/dist/scripts/replay-real.js +16 -18
- package/dist/scripts/replay.js +52 -82
- package/dist/scripts/session-attach-diagnose.js +16 -12
- package/dist/scripts/session-attach-poc.js +34 -37
- package/dist/scripts/session-benchmark.js +66 -147
- package/dist/scripts/session-dida-live-e2e.js +151 -0
- package/dist/scripts/session-extract-tests.js +14 -35
- package/dist/scripts/session-login.js +14 -13
- package/dist/scripts/session-tests.js +545 -583
- package/dist/scripts/sf-live-benchmark.js +92 -110
- package/dist/scripts/sf-live-cli-tests.js +2 -9
- package/dist/scripts/sf-soft-score-tests.js +19 -60
- package/dist/scripts/sf-summary-tests.js +149 -0
- package/dist/scripts/sf-summary.js +466 -84
- package/dist/scripts/skeleton-check.js +16 -14
- package/dist/scripts/skeleton-integration-test.js +11 -10
- package/dist/scripts/skills-contract-tests.js +51 -46
- package/dist/scripts/smoke-session-gate-tests.js +12 -20
- package/dist/scripts/smoke.js +541 -422
- package/dist/scripts/sponsor-reuse-tests.js +262 -0
- package/dist/scripts/state-cli-tests.js +226 -101
- package/dist/scripts/state-cli.js +549 -212
- package/dist/scripts/static-golden-tests.js +46 -180
- package/dist/scripts/time-eval-tests.js +210 -192
- package/dist/scripts/travel-timeline-tests.js +29 -93
- package/dist/scripts/turn-handoff-collect-tests.js +46 -93
- package/dist/scripts/turn-handoff-collect.js +71 -99
- package/dist/scripts/turn-policy-tests.js +28 -29
- package/dist/scripts/typed-contract-canary.js +77 -162
- package/dist/scripts/tz-resolver-sanity.js +50 -0
- package/dist/scripts/tz-resolver-tests.js +584 -0
- package/dist/scripts/unified-tests.js +23 -19
- package/dist/scripts/visa-policy-tests.js +31 -44
- package/dist/scripts/weather-tests.js +121 -543
- package/dist/scripts/wish-channel-gate-tests.js +29 -86
- package/dist/scripts/write-gate-tests.js +522 -0
- package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
- package/dist/scripts/z3-lifecycle-tests.js +170 -0
- package/dist/scripts/z3-race-repeat-tests.js +39 -0
- package/dist/scripts/z3-race-tests.js +18 -12
- package/dist/src/artifact-gate.js +652 -221
- package/dist/src/backend/kernel.js +91 -0
- package/dist/src/backend/modules/booking-copilot.js +55 -0
- package/dist/src/backend/modules/booking-executor.js +124 -0
- package/dist/src/backend/modules/session-search.js +194 -0
- package/dist/src/benchmark-agent-conformance.js +296 -262
- package/dist/src/benchmark-environment-bridge.js +298 -413
- package/dist/src/benchmark-headless-child-diagnostics.js +93 -100
- package/dist/src/benchmark-tool-isolation.js +94 -67
- package/dist/src/bookable-facts.js +299 -174
- package/dist/src/booking-saga.js +64 -120
- package/dist/src/booking-surface/availability-policy.js +443 -576
- package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
- package/dist/src/booking-surface/contracts.js +24 -80
- package/dist/src/booking-surface/dsh-planner.js +510 -281
- package/dist/src/booking-surface/dsh-plugin.js +17 -1
- package/dist/src/booking-surface/error-codes.js +82 -10
- package/dist/src/booking-surface/index.js +0 -3
- package/dist/src/booking-surface/profile.js +10 -54
- package/dist/src/booking-surface/recovery-chain.js +367 -0
- package/dist/src/booking-surface/runtime.js +765 -891
- package/dist/src/booking-surface/server.js +200 -251
- package/dist/src/booking-surface/startup.js +58 -54
- package/dist/src/booking-surface/validation.js +150 -218
- package/dist/src/bridge.js +20 -13
- package/dist/src/companions.js +48 -74
- package/dist/src/contracts.js +21 -43
- package/dist/src/dsh-llm.js +82 -120
- package/dist/src/engine.js +253 -239
- package/dist/src/evaluation-cadence.js +72 -209
- package/dist/src/evaluation-contracts.js +220 -843
- package/dist/src/flight-pack-adapter.js +54 -0
- package/dist/src/flight-pack-contract.js +2 -0
- package/dist/src/gotry-backend.js +70 -0
- package/dist/src/hotel-date-gate.js +118 -0
- package/dist/src/i18n.js +18 -7
- package/dist/src/index.js +1425 -1629
- package/dist/src/journey.js +132 -81
- package/dist/src/ledger-repair-apply.js +538 -0
- package/dist/src/ledger-repair-plan.js +463 -0
- package/dist/src/loop.js +257 -166
- package/dist/src/memory-capture.js +180 -32
- package/dist/src/memory-decay.js +30 -14
- package/dist/src/memory-lifecycle.js +1310 -0
- package/dist/src/memory-utility.js +27 -40
- package/dist/src/mock-llm.js +36 -61
- package/dist/src/model.js +81 -36
- package/dist/src/realtime-pricing.js +63 -72
- package/dist/src/slot-spec.js +84 -106
- package/dist/src/state-ledger.js +310 -471
- package/dist/src/subagent-job-id-guard.js +58 -0
- package/dist/src/time-anchor.js +231 -33
- package/dist/src/tool-packet.js +20 -8
- package/dist/src/travel-slots.js +58 -35
- package/dist/src/travel-timeline.js +44 -63
- package/dist/src/turn-deadline.js +121 -127
- package/dist/src/turn-policy.js +61 -89
- package/dist/src/tz-resolver.js +143 -0
- package/dist/src/unified.js +504 -381
- package/dist/src/wish-pool.js +39 -14
- package/dist/src/write-gate.js +495 -0
- package/dist/src/z3-shared.js +273 -7
- package/extension/README.md +4 -3
- package/extension/background.js +125 -14
- package/extension/content-bridge.js +23 -1
- package/extension/content-main.js +15 -4
- package/extension/manifest.json +16 -8
- package/package.json +251 -220
- package/ts/capabilities/agent-reach.ts +4 -4
- package/ts/capabilities/anything.ts +1 -1
- package/ts/capabilities/artifacts.ts +46 -9
- package/ts/capabilities/flyai.ts +156 -37
- package/ts/capabilities/hbcli.ts +1 -1
- package/ts/capabilities/incident-log.ts +88 -59
- package/ts/capabilities/session/adapters/ctrip-flight.ts +99 -36
- package/ts/capabilities/session/adapters/dida-portal.ts +250 -0
- package/ts/capabilities/session/adapters/rail-12306.ts +123 -24
- package/ts/capabilities/session/extension-bridge.ts +226 -101
- package/ts/capabilities/session/extension-channel.ts +22 -9
- package/ts/capabilities/session/wizard.ts +2 -2
- package/ts/capabilities/session-consent.ts +98 -14
- package/ts/capabilities/session-login.ts +1 -0
- package/ts/capabilities/session-search.ts +315 -31
- package/ts/dsh-runtime/vendor/README.md +8 -4
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +7 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +5 -3
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +1 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +1 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +6 -6
- package/ts/package.json +236 -16
- package/ts/scripts/map-tools-vendor-package-proof.ts +152 -31
- package/ts/scripts/state-cli.ts +418 -51
- package/ts/src/artifact-gate.ts +561 -15
- package/ts/src/benchmark-environment-bridge.ts +37 -20
- package/ts/src/bookable-facts.ts +183 -7
- package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
- package/ts/src/booking-surface/dsh-planner.ts +240 -32
- package/ts/src/booking-surface/dsh-plugin.js +17 -1
- package/ts/src/booking-surface/error-codes.ts +75 -0
- package/ts/src/booking-surface/recovery-chain.ts +409 -0
- package/ts/src/booking-surface/server.ts +84 -51
- package/ts/src/booking-surface/validation.ts +2 -0
- package/ts/src/contracts.ts +12 -2
- package/ts/src/dsh-llm.ts +4 -8
- package/ts/src/index.ts +609 -90
- package/ts/src/loop.ts +116 -7
- package/ts/src/memory-capture.ts +227 -3
- package/ts/src/mock-llm.ts +4 -8
- package/ts/src/model.ts +88 -2
- package/ts/src/state-ledger.ts +47 -29
- package/ts/src/subagent-job-id-guard.ts +72 -0
- package/ts/src/time-anchor.ts +217 -13
- package/ts/src/unified.ts +316 -54
|
@@ -1,137 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 携程酒店站点适配器(2026-09-03 迪拜 session 复盘:账号会话面此前只有机票,
|
|
3
|
+
* 用户要「携程找酒店」时被迫撞 flyai 429 与打码 web 读页;会话面酒店从
|
|
4
|
+
* 「备份」实装为通道之一,rfc §2.2 同款只读纪律)。
|
|
5
|
+
*
|
|
6
|
+
* - entry:https://hotels.ctrip.com/hotels/list?city=<id>&checkin=&checkout=&adult=N
|
|
7
|
+
* - networkHints:酒店检索 XHR 的 URL 形态(接口名公开资料多版并存,**首个真会话
|
|
8
|
+
* 后校准**——D-13 同款边界);URL hint 未命中时由 content-main 的**形状嗅探**
|
|
9
|
+
* 兜底(响应 JSON 含酒店清单签名即转发),对接口改名免疫
|
|
10
|
+
* - 解析:走形(walk)JSON 找「名字 + 价格」签名的条目数组,归一化最小字段;
|
|
11
|
+
* 页面/接口 JSON 是不可信输入(RFC §3.5)——本层只归形状,语义判定在工具层
|
|
12
|
+
*
|
|
13
|
+
* 只读:适配器不含任何提交/预订语义;URL 直开即零 DOM 交互;ReadGuard 对酒店
|
|
14
|
+
* 页面照常生效(下单/支付面 URL 仍被物理拦截)。
|
|
15
|
+
*/
|
|
16
|
+
/** 城市码表(**实测校准 2026-09-03**:每条 id 均经 hotels.ctrip.com list 页
|
|
17
|
+
* <title> 验证——探测法曾纠出 104=平遥县(传猜成都)等 5 处错猜,表内只收
|
|
18
|
+
* 验证过的 id;词表外 unresolved 逐字保留,调用方可显式传 cityId 覆盖) */
|
|
1
19
|
const HOTEL_CITY_CODES = {
|
|
2
|
-
上海: '2',
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
苏州: '14',
|
|
10
|
-
无锡: '13',
|
|
11
|
-
扬州: '15',
|
|
12
|
-
镇江: '16',
|
|
13
|
-
青岛: '7',
|
|
14
|
-
厦门: '25',
|
|
15
|
-
珠海: '31',
|
|
16
|
-
三亚: '43',
|
|
17
|
-
海口: '42',
|
|
18
|
-
桂林: '33',
|
|
19
|
-
昆明: '34',
|
|
20
|
-
大理: '36',
|
|
21
|
-
丽江: '37',
|
|
22
|
-
西双版纳: '35',
|
|
23
|
-
贵阳: '38',
|
|
24
|
-
拉萨: '41',
|
|
25
|
-
乌鲁木齐: '39',
|
|
26
|
-
太原: '105',
|
|
27
|
-
长春: '158',
|
|
28
|
-
南通: '82',
|
|
29
|
-
敦煌: '11',
|
|
30
|
-
舟山: '19',
|
|
31
|
-
张家界: '27',
|
|
32
|
-
黄山市: '23',
|
|
33
|
-
九江: '24',
|
|
34
|
-
武夷山: '26',
|
|
35
|
-
迪拜: '220'
|
|
20
|
+
上海: '2', 北京: '1', 广州: '32', 深圳: '30', 成都: '28',
|
|
21
|
+
杭州: '17', 南京: '12', 苏州: '14', 无锡: '13', 扬州: '15', 镇江: '16',
|
|
22
|
+
青岛: '7', 厦门: '25', 珠海: '31', 三亚: '43', 海口: '42',
|
|
23
|
+
桂林: '33', 昆明: '34', 大理: '36', 丽江: '37', 西双版纳: '35',
|
|
24
|
+
贵阳: '38', 拉萨: '41', 乌鲁木齐: '39', 太原: '105', 长春: '158', 南通: '82',
|
|
25
|
+
敦煌: '11', 舟山: '19', 张家界: '27', 黄山市: '23', 九江: '24', 武夷山: '26',
|
|
26
|
+
迪拜: '220',
|
|
36
27
|
};
|
|
37
28
|
export function buildHotelEntryUrl(q) {
|
|
38
29
|
const unresolved = [];
|
|
39
30
|
const cityId = String(q.cityId ?? '').trim() || HOTEL_CITY_CODES[q.to.trim()];
|
|
40
|
-
if (!cityId)
|
|
31
|
+
if (!cityId)
|
|
32
|
+
unresolved.push(q.to);
|
|
41
33
|
if (unresolved.length > 0) {
|
|
42
|
-
return {
|
|
43
|
-
ok: false,
|
|
44
|
-
unresolved
|
|
45
|
-
};
|
|
34
|
+
return { ok: false, unresolved };
|
|
46
35
|
}
|
|
47
|
-
const params = new URLSearchParams({
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (q.
|
|
51
|
-
|
|
52
|
-
if (q.adults && q.adults > 0)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
url: `https://hotels.ctrip.com/hotels/list?${params.toString()}`
|
|
56
|
-
};
|
|
36
|
+
const params = new URLSearchParams({ city: cityId });
|
|
37
|
+
if (q.checkIn)
|
|
38
|
+
params.set('checkin', q.checkIn);
|
|
39
|
+
if (q.checkOut)
|
|
40
|
+
params.set('checkout', q.checkOut);
|
|
41
|
+
if (q.adults && q.adults > 0)
|
|
42
|
+
params.set('adult', String(q.adults));
|
|
43
|
+
return { ok: true, url: `https://hotels.ctrip.com/hotels/list?${params.toString()}` };
|
|
57
44
|
}
|
|
45
|
+
/** networkHints:酒店检索 XHR 的 URL 形态(restapi/soa2/34951/fetchHotelList
|
|
46
|
+
* 为 2026-09-03 自 list 页一方确认的现行接口;其余为多版并存口径;命中即读) */
|
|
58
47
|
export const HOTEL_NETWORK_HINTS = [
|
|
59
48
|
/hotels\.ctrip\.com\/(hotels\/api|domestic\/pc\/api)/i,
|
|
60
49
|
/GetHotelListBySOA|GetHotelListByCity|HotelSearch|hotelsearch/i,
|
|
61
|
-
/restapi\/soa2\/\d+\/fetchHotelList/i
|
|
50
|
+
/restapi\/soa2\/\d+\/fetchHotelList/i,
|
|
62
51
|
];
|
|
52
|
+
/** 形状嗅探签名:URL hint 未命中时的兜底(对接口改名免疫);大小上限由调用方先把关 */
|
|
63
53
|
export function looksLikeHotelListBody(body) {
|
|
64
|
-
if (!body || body.length > 2_000_000)
|
|
54
|
+
if (!body || body.length > 2_000_000)
|
|
55
|
+
return false;
|
|
65
56
|
return /"hotelList"|"hotelMatchInfos"|"hotelName"/.test(body);
|
|
66
57
|
}
|
|
58
|
+
/** 酒店 page 域(content_scripts 注入面与 background 白名单的对账源) */
|
|
67
59
|
export const HOTEL_SITE_HOST = 'hotels.ctrip.com';
|
|
60
|
+
/** 条目签名:像不像一家酒店(名字类字段必有 + 价格类字段至少其一) */
|
|
68
61
|
function hotelSignature(o) {
|
|
69
|
-
const name = o.hotelName ?? o.name ?? o.hotelNameEn;
|
|
70
|
-
if (typeof name !== 'string' || !name.trim())
|
|
62
|
+
const name = (o.hotelName ?? o.name ?? o.hotelNameEn);
|
|
63
|
+
if (typeof name !== 'string' || !name.trim())
|
|
64
|
+
return null;
|
|
71
65
|
let price;
|
|
72
66
|
let priceRaw;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
67
|
+
// 顶层扁平价(数字/字符串直给;字符串价是打码/区间形态,原样保留不数值化)
|
|
68
|
+
if (typeof o.price === 'number' && o.price > 0)
|
|
69
|
+
price = o.price;
|
|
70
|
+
else if (typeof o.price === 'string' && o.price.trim())
|
|
71
|
+
priceRaw = o.price.trim();
|
|
72
|
+
const probe = (v, seen = new Set()) => {
|
|
73
|
+
if (v == null || typeof v !== 'object')
|
|
74
|
+
return;
|
|
75
|
+
if (seen.has(v))
|
|
76
|
+
return;
|
|
78
77
|
seen.add(v);
|
|
79
|
-
for (const [k, child] of Object.entries(v)){
|
|
80
|
-
if (/^(avgPrice|totalPrice|price|amount|total|startPrice)$/i.test(k) && typeof child === 'number' && child > 0 && price === undefined)
|
|
81
|
-
|
|
82
|
-
else
|
|
78
|
+
for (const [k, child] of Object.entries(v)) {
|
|
79
|
+
if (/^(avgPrice|totalPrice|price|amount|total|startPrice)$/i.test(k) && typeof child === 'number' && child > 0 && price === undefined)
|
|
80
|
+
price = child;
|
|
81
|
+
else if (/^(price|priceDisplay|amountText)$/i.test(k) && typeof child === 'string' && child.trim() && priceRaw === undefined)
|
|
82
|
+
priceRaw = child.trim();
|
|
83
|
+
else
|
|
84
|
+
probe(child, seen);
|
|
83
85
|
}
|
|
84
86
|
};
|
|
85
87
|
probe(o.priceInfo);
|
|
86
88
|
const flatPrice = o.totalPrice ?? o.avgPrice ?? o.minPrice;
|
|
87
|
-
if (typeof flatPrice === 'number' && flatPrice > 0 && price === undefined)
|
|
89
|
+
if (typeof flatPrice === 'number' && flatPrice > 0 && price === undefined)
|
|
90
|
+
price = flatPrice;
|
|
88
91
|
const flatRaw = o.priceText ?? o.priceDisplay;
|
|
89
|
-
if (typeof flatRaw === 'string' && flatRaw.trim() && priceRaw === undefined)
|
|
90
|
-
|
|
92
|
+
if (typeof flatRaw === 'string' && flatRaw.trim() && priceRaw === undefined)
|
|
93
|
+
priceRaw = flatRaw.trim();
|
|
94
|
+
if (price === undefined && priceRaw === undefined)
|
|
95
|
+
return null;
|
|
91
96
|
const starRaw = o.star ?? o.level ?? o.hotelStar;
|
|
92
97
|
const star = typeof starRaw === 'number' && starRaw > 0 ? starRaw : Number.isFinite(Number(starRaw)) && Number(starRaw) > 0 ? Number(starRaw) : undefined;
|
|
93
98
|
const scoreRaw = o.score ?? o.commentScore ?? o.reviewScore;
|
|
94
99
|
const score = scoreRaw == null ? undefined : String(scoreRaw);
|
|
95
100
|
const pos = o.position;
|
|
96
101
|
const address = (typeof pos?.address === 'string' ? pos.address : undefined) ?? (typeof o.address === 'string' ? o.address : undefined);
|
|
97
|
-
const hotelId = (typeof o.hotelId === 'number' || typeof o.hotelId === 'string' ? String(o.hotelId) : undefined)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
price,
|
|
101
|
-
priceRaw,
|
|
102
|
-
star,
|
|
103
|
-
score,
|
|
104
|
-
address,
|
|
105
|
-
hotelId
|
|
106
|
-
};
|
|
102
|
+
const hotelId = (typeof o.hotelId === 'number' || typeof o.hotelId === 'string' ? String(o.hotelId) : undefined)
|
|
103
|
+
?? (typeof o.hotelIdStr === 'string' ? o.hotelIdStr : undefined);
|
|
104
|
+
return { name: name.trim(), price, priceRaw, star, score, address, hotelId };
|
|
107
105
|
}
|
|
106
|
+
/** 结构化候选:携程现行 list 形态(**一方校准 2026-09-03**,取自 list 页官方
|
|
107
|
+
* 前端代码反查 + SSR initListData 实测):
|
|
108
|
+
* - 条目 = hotelList[].hotelInfo{summary{hotelId,nameInfo{name,names[]},hotelStar{star},positionInfo},commentInfo{commentScore}}
|
|
109
|
+
* - 价格 = roomInfo[].priceInfo.price(多房型取最低;页面 room_price 即读此);
|
|
110
|
+
* priceToken 在而无 price = 加密价(fetchHotelListPrices 页面内存解密),
|
|
111
|
+
* 嗅探面拿不到明文——如实不报价,summary 指引到 jumpUrl 落地页
|
|
112
|
+
* - displayPrice(如 "¥468")作 priceRaw 兜底;通用 price/amount 子树探测殿后
|
|
113
|
+
* (直连复现 fetchHotelList 被 hotel-spider-defence 反爬 404——「让站点自己
|
|
114
|
+
* 发请求」正是会话桥的设计原因;官方前端为公开静态资产,路径自其中反查) */
|
|
108
115
|
function ctripStructuredCandidate(item) {
|
|
109
116
|
const info = item.hotelInfo;
|
|
110
|
-
const summary = info?.summary ?? item;
|
|
117
|
+
const summary = (info?.summary ?? item);
|
|
111
118
|
const nameInfo = summary.nameInfo;
|
|
112
119
|
const names = nameInfo?.names;
|
|
113
120
|
const nameRaw = nameInfo?.name ?? (Array.isArray(names) && names.length > 0 ? names[0] : undefined) ?? summary.hotelName ?? summary.name;
|
|
114
|
-
if (typeof nameRaw !== 'string' || !nameRaw.trim())
|
|
121
|
+
if (typeof nameRaw !== 'string' || !nameRaw.trim())
|
|
122
|
+
return null;
|
|
123
|
+
// 价格:roomInfo[].priceInfo 官方路径优先(数值 price 取 min;displayPrice/字符串价作 priceRaw)
|
|
115
124
|
let price;
|
|
116
125
|
let priceRaw;
|
|
117
126
|
let encrypted = false;
|
|
118
127
|
const rooms = info?.roomInfo ?? item.roomInfo;
|
|
119
128
|
if (Array.isArray(rooms)) {
|
|
120
|
-
for (const room of rooms){
|
|
121
|
-
if (room == null || typeof room !== 'object')
|
|
129
|
+
for (const room of rooms) {
|
|
130
|
+
if (room == null || typeof room !== 'object')
|
|
131
|
+
continue;
|
|
122
132
|
const pi = room.priceInfo;
|
|
123
|
-
if (!pi)
|
|
124
|
-
|
|
125
|
-
if (typeof pi.price === '
|
|
126
|
-
|
|
127
|
-
if (pi.
|
|
133
|
+
if (!pi)
|
|
134
|
+
continue;
|
|
135
|
+
if (typeof pi.price === 'number' && pi.price > 0)
|
|
136
|
+
price = price === undefined ? pi.price : Math.min(price, pi.price);
|
|
137
|
+
if (typeof pi.price === 'string' && pi.price.trim() && !priceRaw)
|
|
138
|
+
priceRaw = pi.price.trim();
|
|
139
|
+
if (typeof pi.displayPrice === 'string' && pi.displayPrice.trim() && !priceRaw)
|
|
140
|
+
priceRaw = pi.displayPrice.trim();
|
|
141
|
+
if (pi.priceToken != null && pi.price === undefined)
|
|
142
|
+
encrypted = true;
|
|
128
143
|
}
|
|
129
144
|
}
|
|
145
|
+
// 兜底:条目子树 price/amount 探测(SSR 形态 productInfo.priceInfo.price 等)
|
|
130
146
|
if (price === undefined && !priceRaw) {
|
|
131
|
-
const priceProbe = (v, depth, seen)=>{
|
|
132
|
-
if (price !== undefined || depth > 6 || v == null || typeof v !== 'object' || seen.has(v))
|
|
147
|
+
const priceProbe = (v, depth, seen) => {
|
|
148
|
+
if (price !== undefined || depth > 6 || v == null || typeof v !== 'object' || seen.has(v))
|
|
149
|
+
return;
|
|
133
150
|
seen.add(v);
|
|
134
|
-
for (const [k, child] of Object.entries(v)){
|
|
151
|
+
for (const [k, child] of Object.entries(v)) {
|
|
135
152
|
if (/^(price|avgPrice|totalPrice|minPrice|amount)$/i.test(k) && typeof child === 'number' && child > 0) {
|
|
136
153
|
price = child;
|
|
137
154
|
return;
|
|
@@ -145,7 +162,9 @@ function ctripStructuredCandidate(item) {
|
|
|
145
162
|
};
|
|
146
163
|
priceProbe(item, 0, new Set());
|
|
147
164
|
}
|
|
148
|
-
|
|
165
|
+
// 加密价(priceToken 在而无明文 price):条目保留,价如实为 0——不伪造任何价
|
|
166
|
+
if (price === undefined && !priceRaw && !encrypted)
|
|
167
|
+
return null;
|
|
149
168
|
const starOf = summary.hotelStar;
|
|
150
169
|
const starRaw = starOf?.star ?? summary.star;
|
|
151
170
|
const star = typeof starRaw === 'number' && starRaw > 0 ? starRaw : undefined;
|
|
@@ -153,33 +172,31 @@ function ctripStructuredCandidate(item) {
|
|
|
153
172
|
const pos = summary.positionInfo;
|
|
154
173
|
const address = (typeof pos?.address === 'string' ? pos.address : undefined) ?? (typeof summary.address === 'string' ? summary.address : undefined);
|
|
155
174
|
const hotelId = summary.hotelId != null ? String(summary.hotelId) : undefined;
|
|
156
|
-
return {
|
|
157
|
-
name: nameRaw.trim(),
|
|
158
|
-
price,
|
|
159
|
-
priceRaw,
|
|
160
|
-
star,
|
|
161
|
-
score: scoreRaw == null ? undefined : String(scoreRaw),
|
|
162
|
-
address,
|
|
163
|
-
hotelId
|
|
164
|
-
};
|
|
175
|
+
return { name: nameRaw.trim(), price, priceRaw, star, score: scoreRaw == null ? undefined : String(scoreRaw), address, hotelId };
|
|
165
176
|
}
|
|
177
|
+
/** 走形:深找第一个酒店签名数组(纯函数,fixture 测试锚点);malformed 一律返空,不抛错 */
|
|
166
178
|
export function parseCtripHotelList(body, opts = {}) {
|
|
167
179
|
let raw;
|
|
168
180
|
try {
|
|
169
181
|
raw = JSON.parse(body);
|
|
170
|
-
}
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
171
184
|
return [];
|
|
172
185
|
}
|
|
173
186
|
const out = [];
|
|
174
187
|
const maxItems = opts.maxItems ?? 20;
|
|
175
|
-
const walk = (node, depth, seen)=>{
|
|
176
|
-
if (out.length >= maxItems || depth > 12 || node == null || typeof node !== 'object' || seen.has(node))
|
|
188
|
+
const walk = (node, depth, seen) => {
|
|
189
|
+
if (out.length >= maxItems || depth > 12 || node == null || typeof node !== 'object' || seen.has(node))
|
|
190
|
+
return;
|
|
177
191
|
if (Array.isArray(node)) {
|
|
178
|
-
for (const item of node){
|
|
179
|
-
if (out.length >= maxItems)
|
|
180
|
-
|
|
192
|
+
for (const item of node) {
|
|
193
|
+
if (out.length >= maxItems)
|
|
194
|
+
return;
|
|
195
|
+
if (item == null || typeof item !== 'object' || Array.isArray(item))
|
|
196
|
+
continue;
|
|
181
197
|
const cand = hotelSignature(item) ?? ctripStructuredCandidate(item);
|
|
182
|
-
if (!cand)
|
|
198
|
+
if (!cand)
|
|
199
|
+
continue;
|
|
183
200
|
out.push({
|
|
184
201
|
name: cand.name,
|
|
185
202
|
price: cand.price ?? 0,
|
|
@@ -188,20 +205,18 @@ export function parseCtripHotelList(body, opts = {}) {
|
|
|
188
205
|
score: cand.score,
|
|
189
206
|
address: cand.address,
|
|
190
207
|
hotelId: cand.hotelId,
|
|
191
|
-
jumpUrl: cand.hotelId ? `https://hotels.ctrip.com/hotel/${cand.hotelId}` : undefined
|
|
208
|
+
jumpUrl: cand.hotelId ? `https://hotels.ctrip.com/hotel/${cand.hotelId}` : undefined,
|
|
192
209
|
});
|
|
193
210
|
}
|
|
194
211
|
return;
|
|
195
212
|
}
|
|
196
213
|
seen.add(node);
|
|
197
|
-
for (const child of Object.values(node)){
|
|
214
|
+
for (const child of Object.values(node)) {
|
|
198
215
|
walk(child, depth + 1, seen);
|
|
199
|
-
if (out.length >= maxItems)
|
|
216
|
+
if (out.length >= maxItems)
|
|
217
|
+
return;
|
|
200
218
|
}
|
|
201
219
|
};
|
|
202
220
|
walk(raw, 0, new Set());
|
|
203
221
|
return out;
|
|
204
222
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
//# sourceURL=ts/capabilities/session/adapters/ctrip-hotel.ts
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dida 供应商门户站点适配器(2026-09-09 实装;hotel-be portal integration 迁移线:
|
|
3
|
+
* 员工子账号免知凭据访问供应商 portal——登录经 hotel-be portal 自动填充跳板在
|
|
4
|
+
* dida 官网由人完成,gotry 只在用户本人 Chrome 登录态里被动嗅探检索回包)。
|
|
5
|
+
*
|
|
6
|
+
* - entry:https://portal.dida.com/hotel/find(SPA,页内交互触发检索;深链参数
|
|
7
|
+
* 形态未知——D-13 同款边界,首个真会话后校准)
|
|
8
|
+
* - networkHints:portal-webapi.dida.com 实时价/价格监控 XHR(接口名取自
|
|
9
|
+
* hotel-be supplier/integration/dida/portal 已捕获端点,2026-07 抓包校准);
|
|
10
|
+
* URL hint 未命中时由 content-main 的形状嗅探兜底(响应 JSON 含报价签名即
|
|
11
|
+
* 转发),对接口改名免疫
|
|
12
|
+
* - 解析:SearchRealTime 信封(Envelope{Data.HotelPriceList[].RoomTypeList[]
|
|
13
|
+
* .RatePlanList[]})逐报价展平;SearchMonitor 响应形状未捕获前不猜测——
|
|
14
|
+
* 只按 URL 命中转发、解析留给首个真会话校准(D-13)
|
|
15
|
+
*
|
|
16
|
+
* 只读:适配器不含任何提交/预订语义;预订面(PriceConfirm 等)URL 属 hotel-be
|
|
17
|
+
* 适配器边界,gotry 侧不存在对应原语;登录语义红线同全局:gotry 永不读取/存储/
|
|
18
|
+
* 回传 cookie 值,只读票据 cookie 名(CN_M_DidaTravel)存在性。
|
|
19
|
+
*/
|
|
20
|
+
/** 票据 cookie 名(hotel-be portal 会话同款 HttpOnly cookie;只读名字,永不读值) */
|
|
21
|
+
export const DIDA_LOGIN_COOKIE_NAMES = ['CN_M_DidaTravel'];
|
|
22
|
+
/** page 域(content_scripts 注入面与 background 白名单的对账源) */
|
|
23
|
+
export const DIDA_SITE_HOST = 'portal.dida.com';
|
|
24
|
+
/** cookie 域(chrome.cookies 过滤器与登录票据检查;did a 站点 cookie 挂 .dida.com 域 cookie) */
|
|
25
|
+
export const DIDA_SITE_DOMAIN = 'dida.com';
|
|
26
|
+
/** 默认检索入口(酒店 find 页;SPA 加载后由页面自身代码发起价格检索) */
|
|
27
|
+
export const DIDA_ENTRY_URL = 'https://portal.dida.com/hotel/find';
|
|
28
|
+
/** entry 构造(v1:固定 find 页 + 显式覆盖守白名单域;URL 直开即零 DOM 交互) */
|
|
29
|
+
export function buildDidaEntryUrl(q = {}) {
|
|
30
|
+
const url = (q.entryUrl ?? '').trim() || DIDA_ENTRY_URL;
|
|
31
|
+
if (!/^https:\/\/portal\.dida\.com\//.test(url))
|
|
32
|
+
return { ok: false };
|
|
33
|
+
return { ok: true, url };
|
|
34
|
+
}
|
|
35
|
+
/** networkHints:portal-webapi.dida.com 实时价/价格监控 XHR(2026-07 抓包口径;
|
|
36
|
+
* 首个真会话后按现行接口校准——D-13 同款边界) */
|
|
37
|
+
export const DIDA_NETWORK_HINTS = [
|
|
38
|
+
/portal-webapi\.dida\.com\/HotelPriceAPI\/SearchRealTime/i,
|
|
39
|
+
/portal-webapi\.dida\.com\/HotelPriceAPI\/SearchMonitor/i,
|
|
40
|
+
// 2026-09-10 UAT 实测校准:find 页加载态自发的是推荐流(非 SearchRealTime——
|
|
41
|
+
// 那是用户点「预订」后的实时价接口)。推荐酒店 + 推荐价格两个接口成对出现。
|
|
42
|
+
/portal-webapi\.dida\.com\/HotelRecommendAPI\/SearchHomepageRecommendHotels/i,
|
|
43
|
+
/portal-webapi\.dida\.com\/HotelRecommendAPI\/SearchHomepageRecommendPrices/i,
|
|
44
|
+
/portal-webapi\.dida\.com\/PopularDestinationAPI\/SearchHotels/i,
|
|
45
|
+
/portal-webapi\.dida\.com\/PopularDestinationAPI\/SearchHotelPrices/i,
|
|
46
|
+
];
|
|
47
|
+
/** 形状嗅探签名:URL hint 未命中时的兜底(实时价信封报价签名,对接口改名免疫);
|
|
48
|
+
* 大小上限由调用方先把关 */
|
|
49
|
+
export function looksLikeDidaRatesBody(body) {
|
|
50
|
+
if (!body || body.length > 2_000_000)
|
|
51
|
+
return false;
|
|
52
|
+
return /"HotelPriceList"|"RatePlanList"/.test(body);
|
|
53
|
+
}
|
|
54
|
+
/** 解析推荐酒店列表(Data.Hotels[]:HotelID/Name/Name_CN/Name_EN;Current 页加载态无价) */
|
|
55
|
+
export function parseDidaRecommendHotels(body, opts = {}) {
|
|
56
|
+
let raw;
|
|
57
|
+
try {
|
|
58
|
+
raw = JSON.parse(body);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
const hotels = raw?.Data?.Hotels;
|
|
64
|
+
if (!Array.isArray(hotels))
|
|
65
|
+
return [];
|
|
66
|
+
const out = [];
|
|
67
|
+
for (const item of hotels.slice(0, opts.maxItems ?? 60)) {
|
|
68
|
+
if (item == null || typeof item !== 'object')
|
|
69
|
+
continue;
|
|
70
|
+
const h = item;
|
|
71
|
+
const hotelId = h.HotelID != null ? String(h.HotelID) : '';
|
|
72
|
+
const name = typeof h.Name_CN === 'string' && h.Name_CN.trim() ? h.Name_CN : (typeof h.Name === 'string' ? h.Name : '');
|
|
73
|
+
if (!hotelId || !name)
|
|
74
|
+
continue;
|
|
75
|
+
out.push({ hotelId, name, nameEN: typeof h.Name_EN === 'string' ? h.Name_EN : undefined });
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
// (parser types continue below)
|
|
80
|
+
/** 解析价格列表:兼容 Recommend Prices(HotelId/Date)与 PopularDestination(HotelID/CheckInDate)两种键形 */
|
|
81
|
+
export function parseDidaPrices(body) {
|
|
82
|
+
let raw;
|
|
83
|
+
try {
|
|
84
|
+
raw = JSON.parse(body);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
const prices = raw?.Data?.Prices;
|
|
90
|
+
if (!Array.isArray(prices))
|
|
91
|
+
return [];
|
|
92
|
+
const out = [];
|
|
93
|
+
for (const item of prices) {
|
|
94
|
+
if (item == null || typeof item !== 'object')
|
|
95
|
+
continue;
|
|
96
|
+
const p = item;
|
|
97
|
+
const hotelId = p.HotelId != null ? String(p.HotelId) : (p.HotelID != null ? String(p.HotelID) : '');
|
|
98
|
+
const price = typeof p.Price === 'number' ? p.Price : NaN;
|
|
99
|
+
if (!hotelId || !Number.isFinite(price) || price <= 0)
|
|
100
|
+
continue;
|
|
101
|
+
const dateOf = (v) => (typeof v === 'string' && v.length >= 10 ? v.slice(0, 10) : undefined);
|
|
102
|
+
out.push({
|
|
103
|
+
hotelId,
|
|
104
|
+
price,
|
|
105
|
+
originalPrice: typeof p.OriginalPrice === 'number' ? p.OriginalPrice : undefined,
|
|
106
|
+
currency: typeof p.Currency === 'string' ? p.Currency : undefined,
|
|
107
|
+
priceDate: dateOf(p.Date),
|
|
108
|
+
checkInDate: dateOf(p.CheckInDate),
|
|
109
|
+
checkOutDate: dateOf(p.CheckOutDate),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
/** 从 RoomTypePrice 展平一条报价候选(RatePlanList 元素为 dida 已知形态);
|
|
115
|
+
* 酒店字段取 HotelPriceItem.Hotel 嵌套(HotelStatic),容忍旧式扁平形态 */
|
|
116
|
+
function rateCandidate(hotel, room, plan) {
|
|
117
|
+
if (plan == null || typeof plan !== 'object')
|
|
118
|
+
return null;
|
|
119
|
+
const ratePlanId = plan.RatePlanID;
|
|
120
|
+
const price = plan.Price;
|
|
121
|
+
const totalPrice = plan.TotalPrice;
|
|
122
|
+
if (typeof ratePlanId !== 'string' || !ratePlanId.trim())
|
|
123
|
+
return null;
|
|
124
|
+
if (typeof price !== 'number' && typeof totalPrice !== 'number')
|
|
125
|
+
return null;
|
|
126
|
+
const hotelStatic = (hotel.Hotel != null && typeof hotel.Hotel === 'object' ? hotel.Hotel : hotel);
|
|
127
|
+
const hotelId = hotelStatic.HotelID;
|
|
128
|
+
const roomTypeId = room.DidaRoomTypeID;
|
|
129
|
+
return {
|
|
130
|
+
hotelId: hotelId != null ? String(hotelId) : undefined,
|
|
131
|
+
hotelName: typeof hotelStatic.Name === 'string' ? hotelStatic.Name : undefined,
|
|
132
|
+
roomTypeId: roomTypeId != null ? String(roomTypeId) : undefined,
|
|
133
|
+
roomName: typeof room.DidaRoomTypeName_CN === 'string' && room.DidaRoomTypeName_CN.trim()
|
|
134
|
+
? room.DidaRoomTypeName_CN
|
|
135
|
+
: (typeof room.DidaRoomTypeName_EN === 'string' ? room.DidaRoomTypeName_EN : undefined),
|
|
136
|
+
plan,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/** 走形:SearchRealTime 信封 → 展平报价列表(纯函数,fixture 测试锚点);
|
|
140
|
+
* malformed 一律返空,不抛错——页面/接口 JSON 是不可信输入(RFC §3.5) */
|
|
141
|
+
export function parseDidaRates(body, opts = {}) {
|
|
142
|
+
let raw;
|
|
143
|
+
try {
|
|
144
|
+
raw = JSON.parse(body);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
const maxItems = opts.maxItems ?? 40;
|
|
150
|
+
const out = [];
|
|
151
|
+
// 定位 HotelPriceList:信封顶层 Data 下;无信封时容忍裸数组形态
|
|
152
|
+
let hotelList;
|
|
153
|
+
if (raw != null && typeof raw === 'object' && !Array.isArray(raw)) {
|
|
154
|
+
const data = raw.Data;
|
|
155
|
+
if (data != null && typeof data === 'object')
|
|
156
|
+
hotelList = data.HotelPriceList;
|
|
157
|
+
}
|
|
158
|
+
else if (Array.isArray(raw)) {
|
|
159
|
+
hotelList = raw;
|
|
160
|
+
}
|
|
161
|
+
if (!Array.isArray(hotelList))
|
|
162
|
+
return out;
|
|
163
|
+
for (const item of hotelList) {
|
|
164
|
+
if (out.length >= maxItems || item == null || typeof item !== 'object')
|
|
165
|
+
continue;
|
|
166
|
+
const hotel = item;
|
|
167
|
+
// RoomTypeList 与 RoomList 是合并关系(hotel-be converter 两表都吃),不是二选一
|
|
168
|
+
const rooms = [
|
|
169
|
+
...(Array.isArray(hotel.RoomTypeList) ? hotel.RoomTypeList : []),
|
|
170
|
+
...(Array.isArray(hotel.RoomList) ? hotel.RoomList : []),
|
|
171
|
+
];
|
|
172
|
+
for (const room of rooms) {
|
|
173
|
+
if (out.length >= maxItems || room == null || typeof room !== 'object')
|
|
174
|
+
continue;
|
|
175
|
+
const plans = room.RatePlanList;
|
|
176
|
+
if (!Array.isArray(plans))
|
|
177
|
+
continue;
|
|
178
|
+
for (const plan of plans) {
|
|
179
|
+
if (out.length >= maxItems)
|
|
180
|
+
return out;
|
|
181
|
+
const cand = rateCandidate(hotel, room, plan);
|
|
182
|
+
if (!cand)
|
|
183
|
+
continue;
|
|
184
|
+
const p = cand.plan;
|
|
185
|
+
out.push({
|
|
186
|
+
hotelId: cand.hotelId,
|
|
187
|
+
hotelName: cand.hotelName,
|
|
188
|
+
roomTypeId: cand.roomTypeId,
|
|
189
|
+
roomName: cand.roomName,
|
|
190
|
+
ratePlanId: typeof p.RatePlanID === 'string' ? p.RatePlanID : undefined,
|
|
191
|
+
price: typeof p.Price === 'number' ? p.Price : 0,
|
|
192
|
+
totalPrice: typeof p.TotalPrice === 'number' ? p.TotalPrice : undefined,
|
|
193
|
+
currency: typeof p.Currency === 'string' ? p.Currency : undefined,
|
|
194
|
+
mealType: typeof p.MealType === 'string' ? p.MealType : undefined,
|
|
195
|
+
breakfastType: typeof p.BreakfastType === 'string' ? p.BreakfastType : undefined,
|
|
196
|
+
bedType: typeof p.BedType === 'string' ? p.BedType : undefined,
|
|
197
|
+
paymentType: typeof p.PaymentType === 'string' ? p.PaymentType : undefined,
|
|
198
|
+
inventory: typeof p.Inventory === 'number' ? p.Inventory : (typeof p.RoomCount === 'number' ? p.RoomCount : undefined),
|
|
199
|
+
referenceNo: typeof p.ReferenceNo === 'string' ? p.ReferenceNo : undefined,
|
|
200
|
+
jumpUrl: cand.hotelId ? `https://portal.dida.com/hotel/find` : undefined,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
@@ -1,73 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 美团本地适配器骨架(民宿/门票——官方通道盲区,RFC §3.1)。
|
|
3
|
+
*
|
|
4
|
+
* 实测边界(2026-08-28 tick):匿名实例 hotel.meituan.com 直接 403——三站中最强反爬,
|
|
5
|
+
* **登录态是硬前置**(founder 纠偏「不能匿名实例」在美团侧是 403 级事实,非策略偏好)。
|
|
6
|
+
* 本骨架:entry 城市拼音表 + 登录票据名单 + networkHint 占位;live 接口形状待用户登录态
|
|
7
|
+
* (profile 内用户自登美团)后由心跳轮实测回填——在此之前 parse 返回空、verdict=miss。
|
|
8
|
+
*/
|
|
9
|
+
/** 城市拼音表(美团 URL 路径段;与 ctrip-flight 城市码表同界:词表外 unresolved 不猜) */
|
|
1
10
|
const CITY_SLUGS = {
|
|
2
|
-
上海: 'shanghai',
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
成都: 'chengdu',
|
|
7
|
-
昆明: 'kunming',
|
|
8
|
-
大理: 'dali',
|
|
9
|
-
丽江: 'lijiang',
|
|
10
|
-
西安: 'xian',
|
|
11
|
-
杭州: 'hangzhou',
|
|
12
|
-
三亚: 'sanya',
|
|
13
|
-
厦门: 'xiamen',
|
|
14
|
-
重庆: 'chongqing',
|
|
15
|
-
青岛: 'qingdao',
|
|
16
|
-
长沙: 'changsha',
|
|
17
|
-
武汉: 'wuhan',
|
|
18
|
-
南京: 'nanjing',
|
|
19
|
-
郑州: 'zhengzhou',
|
|
20
|
-
贵阳: 'guiyang',
|
|
21
|
-
桂林: 'guilin'
|
|
11
|
+
上海: 'shanghai', 北京: 'beijing', 广州: 'guangzhou', 深圳: 'shenzhen', 成都: 'chengdu',
|
|
12
|
+
昆明: 'kunming', 大理: 'dali', 丽江: 'lijiang', 西安: 'xian', 杭州: 'hangzhou',
|
|
13
|
+
三亚: 'sanya', 厦门: 'xiamen', 重庆: 'chongqing', 青岛: 'qingdao', 长沙: 'changsha',
|
|
14
|
+
武汉: 'wuhan', 南京: 'nanjing', 郑州: 'zhengzhou', 贵阳: 'guiyang', 桂林: 'guilin',
|
|
22
15
|
};
|
|
23
16
|
export const SITE_DOMAIN = 'meituan.com';
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
'u',
|
|
27
|
-
'token',
|
|
28
|
-
'n'
|
|
29
|
-
];
|
|
17
|
+
/** 登录票据 cookie 名单(公开常识:lt=login ticket/u=uid/token;登录后校准,同 ctrip 侧做法) */
|
|
18
|
+
export const LOGIN_COOKIE_NAMES = ['lt', 'u', 'token', 'n'];
|
|
30
19
|
export function buildMeituanEntry(city, vertical, keyword) {
|
|
31
20
|
const slug = CITY_SLUGS[city];
|
|
32
|
-
if (!slug)
|
|
33
|
-
ok: false,
|
|
34
|
-
unresolved: [
|
|
35
|
-
city
|
|
36
|
-
]
|
|
37
|
-
};
|
|
21
|
+
if (!slug)
|
|
22
|
+
return { ok: false, unresolved: [city] };
|
|
38
23
|
const base = vertical === 'hotel' ? `https://hotel.meituan.com/${slug}/` : `https://minsu.meituan.com/${slug}/`;
|
|
39
|
-
return {
|
|
40
|
-
ok: true,
|
|
41
|
-
url: keyword ? base + `?q=${encodeURIComponent(keyword)}` : base
|
|
42
|
-
};
|
|
24
|
+
return { ok: true, url: keyword ? base + `?q=${encodeURIComponent(keyword)}` : base };
|
|
43
25
|
}
|
|
26
|
+
/** networkHint 占位:登录态实测后回填真实搜索接口 pattern(当前为空=嗅探层不命中,a11y 兜底接管) */
|
|
44
27
|
export const NETWORK_HINTS = [];
|
|
28
|
+
/** XHR JSON 解析占位:接口形状实测回填前恒返空(调用方走 a11y 兜底) */
|
|
45
29
|
export function parseMeituanSearch(_body) {
|
|
46
30
|
void _body;
|
|
47
31
|
return [];
|
|
48
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* a11y 兜底:从快照条目抽「名称+价格」候选(纯函数;供 extract.ts 条目流消费)。
|
|
35
|
+
* 形状规则:名称=非空 heading/listitem/link;价格=条目名内 ¥\d+(宽松元级);不做语义猜测。
|
|
36
|
+
*/
|
|
49
37
|
export function extractListings(entries, opts = {}) {
|
|
50
38
|
const out = [];
|
|
51
|
-
const roles = new Set([
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
for (const e of entries){
|
|
58
|
-
if (out.length >= (opts.max ?? 30)) break;
|
|
59
|
-
if (!roles.has(e.role) || !e.name) continue;
|
|
39
|
+
const roles = new Set(['heading', 'listitem', 'link', 'article']);
|
|
40
|
+
for (const e of entries) {
|
|
41
|
+
if (out.length >= (opts.max ?? 30))
|
|
42
|
+
break;
|
|
43
|
+
if (!roles.has(e.role) || !e.name)
|
|
44
|
+
continue;
|
|
60
45
|
const pm = /¥\s*(\d+)/.exec(e.name);
|
|
61
|
-
if (!pm)
|
|
46
|
+
if (!pm)
|
|
47
|
+
continue;
|
|
62
48
|
const name = e.name.replace(/¥\s*\d+.*$/, '').trim().slice(0, 80);
|
|
63
|
-
if (!name)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
price: Number(pm[1])
|
|
67
|
-
});
|
|
49
|
+
if (!name)
|
|
50
|
+
continue;
|
|
51
|
+
out.push({ name, price: Number(pm[1]) });
|
|
68
52
|
}
|
|
69
53
|
return out;
|
|
70
54
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
//# sourceURL=ts/capabilities/session/adapters/meituan-local.ts
|