@danceiny/gotry 0.0.1-rc.9 → 0.2.0-rc.28
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 +172 -219
- package/README.zh-CN.md +222 -0
- package/bin/gotry-backend.js +53 -0
- package/bin/gotry-booking-copilot.js +53 -0
- package/bin/gotry-bootstrap.js +1788 -0
- package/bin/gotry-inner.js +733 -83
- package/bin/gotry-process-liveness.d.ts +27 -0
- package/bin/gotry-process-liveness.js +97 -0
- package/bin/gotry-runtime-resolution.d.ts +27 -0
- package/bin/gotry-runtime-resolution.js +49 -0
- package/bin/gotry.js +1 -1
- package/client/client.js +203 -0
- package/cordis.gotry-patch.yml +80 -56
- package/data/airline-airports.json +39 -0
- package/data/stations-12306-verify.json +524 -0
- package/dist/capabilities/agent-reach-deep.js +45 -122
- package/dist/capabilities/agent-reach.js +101 -146
- package/dist/capabilities/anything.js +161 -94
- package/dist/capabilities/artifacts.js +370 -0
- package/dist/capabilities/channel-health.js +131 -0
- package/dist/capabilities/channel-registry.js +256 -0
- package/dist/capabilities/doctor.js +553 -0
- package/dist/capabilities/effect.js +394 -0
- package/dist/capabilities/external-event.js +149 -0
- package/dist/capabilities/fact-log.js +55 -0
- package/dist/capabilities/flyai-config.js +337 -0
- package/dist/capabilities/flyai.js +947 -110
- package/dist/capabilities/fx-contract.js +293 -0
- package/dist/capabilities/geo-atlas.js +246 -0
- package/dist/capabilities/ground-transfer.js +676 -0
- package/dist/capabilities/hbcli.js +260 -123
- package/dist/capabilities/hotelbyte-transaction.js +194 -0
- package/dist/capabilities/incident-log.js +131 -61
- package/dist/capabilities/itinerary-artifact.js +190 -0
- package/dist/capabilities/itinerary-deck-artifact.js +194 -0
- package/dist/capabilities/itinerary-deck-export.js +339 -0
- package/dist/capabilities/lavish-local.js +924 -0
- package/dist/capabilities/model-override.js +38 -0
- package/dist/capabilities/opensky.js +54 -43
- package/dist/capabilities/provider-detail-sanitize.js +64 -0
- package/dist/capabilities/resilience.js +191 -0
- package/dist/capabilities/route-provider-conformance.js +341 -0
- package/dist/capabilities/session/action-cache.js +50 -44
- package/dist/capabilities/session/adapters/ctrip-flight.js +120 -62
- package/dist/capabilities/session/adapters/ctrip-hotel.js +222 -0
- package/dist/capabilities/session/adapters/dida-portal.js +274 -0
- package/dist/capabilities/session/adapters/meituan-local.js +35 -54
- package/dist/capabilities/session/adapters/rail-12306.js +342 -0
- package/dist/capabilities/session/benchmark.js +189 -0
- package/dist/capabilities/session/bridge-ledger.js +224 -0
- package/dist/capabilities/session/extension-bridge.js +636 -0
- package/dist/capabilities/session/extension-channel.js +109 -0
- package/dist/capabilities/session/extension-distribution.js +230 -0
- package/dist/capabilities/session/extract.js +31 -21
- package/dist/capabilities/session/golden-score.js +114 -0
- package/dist/capabilities/session/health-watch.js +215 -0
- package/dist/capabilities/session/read-guard.js +75 -24
- package/dist/capabilities/session/static-flight-golden.js +150 -0
- package/dist/capabilities/session/transport.js +97 -83
- package/dist/capabilities/session/wizard.js +117 -0
- package/dist/capabilities/session-consent.js +178 -0
- package/dist/capabilities/session-login.js +170 -0
- package/dist/capabilities/session-search.js +635 -44
- package/dist/capabilities/spawn-bounded.js +203 -0
- package/dist/capabilities/sponsor-plugin.js +341 -0
- package/dist/capabilities/visa-policy.js +140 -0
- package/dist/capabilities/weather.js +243 -138
- package/dist/data/session-golden-20.json +25 -0
- package/dist/data/sf-golden-manifest.json +103 -0
- package/dist/data/sf-static-routes.json +91 -0
- package/dist/scripts/action-cache-tests.js +24 -59
- package/dist/scripts/agent-planning-turn-deadline-e2e.js +242 -0
- package/dist/scripts/agent-planning-turn-deadline-tests.js +185 -0
- package/dist/scripts/agent-reach-deep-tests.js +30 -40
- package/dist/scripts/agent-reach-error-tests.js +354 -0
- package/dist/scripts/agent-reach-tests.js +26 -18
- package/dist/scripts/agent-reach-wrapper-tests.js +50 -50
- package/dist/scripts/anything-path-measure-tests.js +133 -0
- package/dist/scripts/anything-path-measure.js +150 -0
- package/dist/scripts/anything-tests.js +250 -60
- package/dist/scripts/artifact-client-contract-tests.js +220 -0
- package/dist/scripts/artifacts-capability-tests.js +727 -0
- package/dist/scripts/async-collect.js +50 -11
- package/dist/scripts/benchmark-environment-bridge-e2e.js +741 -0
- package/dist/scripts/benchmark-environment-bridge-tests.js +1452 -0
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +126 -0
- package/dist/scripts/booking-copilot-availability-policy-tests.js +265 -0
- package/dist/scripts/booking-copilot-bin-proof-tests.js +66 -0
- package/dist/scripts/booking-copilot-cleanup-proof-helper.js +76 -0
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +144 -0
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +424 -0
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +2839 -0
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +309 -0
- package/dist/scripts/booking-copilot-dsh-readiness-proof.js +238 -0
- package/dist/scripts/booking-copilot-dsh-warmer-proof.js +126 -0
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +156 -0
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +70 -0
- package/dist/scripts/booking-copilot-intent-proof-tests.js +422 -0
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +184 -0
- package/dist/scripts/booking-copilot-planner-live-probe.js +95 -0
- package/dist/scripts/booking-copilot-proof-runtime.js +42 -0
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +210 -0
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +2349 -0
- package/dist/scripts/booking-copilot-server-proof-tests.js +784 -0
- package/dist/scripts/booking-copilot-startup-proof-tests.js +94 -0
- 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 +125 -0
- package/dist/scripts/booking-surface-contract-proof-tests.js +212 -0
- package/dist/scripts/bootstrap-tests.js +1549 -0
- package/dist/scripts/bridge-events-tests.js +239 -0
- package/dist/scripts/bridge-ledger-tests.js +428 -0
- package/dist/scripts/build-changelog.js +238 -0
- package/dist/scripts/build-metrics-report.js +331 -0
- package/dist/scripts/changelog-tests.js +118 -0
- package/dist/scripts/channel-probe-tests.js +103 -0
- package/dist/scripts/channel-probe.js +169 -0
- package/dist/scripts/channel-registry-tests.js +208 -0
- package/dist/scripts/city-scenario-tier-tests.js +234 -0
- package/dist/scripts/companion-tests.js +25 -70
- package/dist/scripts/dida-cookie-probe.js +87 -0
- package/dist/scripts/dida-runner-stop-tests.js +311 -0
- package/dist/scripts/dida-sw-probe.js +114 -0
- package/dist/scripts/diff-test.js +17 -12
- package/dist/scripts/doctor-tests.js +600 -0
- package/dist/scripts/drill-multiuser-ledger-tests.js +1076 -0
- package/dist/scripts/drill-sdk-descendant-cleanup-tests.js +606 -0
- package/dist/scripts/drill-w2a-sensor-tests.js +788 -0
- package/dist/scripts/dsh-artifact-e2e.js +279 -0
- package/dist/scripts/dsh-artifact-web-e2e.js +675 -0
- package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
- package/dist/scripts/dsh-llm-tests.js +437 -0
- package/dist/scripts/dsh-runtime-closure-tests.js +182 -0
- package/dist/scripts/dsh-runtime-closure.js +162 -0
- package/dist/scripts/dsh-session-v3-migration-proof.js +109 -0
- package/dist/scripts/dsh-subprocess-local-proof.js +150 -0
- package/dist/scripts/dsh-target-closure-proof.js +178 -0
- package/dist/scripts/effect-tests.js +539 -0
- package/dist/scripts/engine-run.js +1 -3
- package/dist/scripts/engine-tests.js +16 -10
- package/dist/scripts/evaluation-cadence-tests.js +174 -0
- package/dist/scripts/evaluation-contract-tests.js +268 -0
- package/dist/scripts/extension-distribution-cli.js +39 -0
- package/dist/scripts/extension-distribution-tests.js +443 -0
- package/dist/scripts/extension-tests.js +988 -0
- package/dist/scripts/external-event-tests.js +350 -0
- package/dist/scripts/fact-gate-tests.js +869 -0
- 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-setup-tests.js +228 -0
- package/dist/scripts/flyai-setup-tool-tests.js +54 -0
- package/dist/scripts/flyai-tests.js +692 -0
- package/dist/scripts/flyai-tool-registration-contract-tests.js +91 -0
- package/dist/scripts/fx-contract-tests.js +201 -0
- package/dist/scripts/g5-guard-tests.js +150 -0
- package/dist/scripts/g5-guard.js +193 -0
- package/dist/scripts/gen-lunar-anchors.js +106 -0
- package/dist/scripts/geo-atlas-tests.js +283 -0
- package/dist/scripts/gotry-backend-kernel-tests.js +206 -0
- package/dist/scripts/gotry-try-demo-tests.js +79 -0
- package/dist/scripts/gotry-try-demo.js +49 -0
- package/dist/scripts/ground-transfer-tests.js +956 -0
- package/dist/scripts/hbcli-e2e-tests.js +159 -0
- 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 +121 -69
- package/dist/scripts/health-watch-cli.js +73 -0
- 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 +41 -0
- 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 +569 -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/issue436-persisted-routing-e2e.js +247 -0
- package/dist/scripts/itinerary-artifact-tests.js +368 -0
- package/dist/scripts/itinerary-deck-artifact-tests.js +416 -0
- package/dist/scripts/itinerary-deck-export-tests.js +552 -0
- package/dist/scripts/itinerary-deck-tests.js +435 -0
- package/dist/scripts/itinerary-html-tests.js +426 -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/lavish-local-tests.js +1226 -0
- package/dist/scripts/lavish-product-tools-tests.js +537 -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 +382 -246
- package/dist/scripts/ledger-workflow-crash.js +10 -20
- package/dist/scripts/m3-cohort-tests.js +714 -0
- package/dist/scripts/m3-cohort.js +310 -0
- package/dist/scripts/managed-dsh-cleanup-diagnostic-proof.js +184 -0
- package/dist/scripts/managed-dsh-close-failure-proof.js +172 -0
- package/dist/scripts/managed-dsh-run-port-proof.js +153 -0
- package/dist/scripts/managed-dsh-terminal-proof.js +252 -0
- package/dist/scripts/map-tools-vendor-package-proof.js +365 -0
- 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 +631 -0
- package/dist/scripts/memory-value-tests.js +328 -0
- package/dist/scripts/metrics-report-tests.js +151 -0
- package/dist/scripts/model-override-e2e.js +137 -0
- package/dist/scripts/nightly-evidence-tests.js +203 -0
- package/dist/scripts/nightly-evidence.js +303 -0
- package/dist/scripts/nudge-digest.js +39 -28
- package/dist/scripts/observability-tests.js +381 -0
- package/dist/scripts/onboarding-tests.js +161 -0
- package/dist/scripts/opensky-check.js +18 -13
- package/dist/scripts/opensky-tests.js +28 -28
- package/dist/scripts/outcome-projection-tests.js +427 -0
- package/dist/scripts/persona-sim-tests.js +603 -0
- package/dist/scripts/persona-sim.js +1081 -0
- package/dist/scripts/persona-surface-guard-tests.js +49 -0
- package/dist/scripts/pnpm-dsh-closure-proof.js +17 -0
- package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
- package/dist/scripts/price-drift-tests.js +340 -0
- package/dist/scripts/price-drift-watch.js +447 -0
- package/dist/scripts/probe-poi-tests.js +53 -45
- package/dist/scripts/product-metrics.js +483 -0
- package/dist/scripts/publish-preverify.js +110 -0
- package/dist/scripts/realtime-pricing-tests.js +76 -0
- package/dist/scripts/recall-tests.js +513 -0
- package/dist/scripts/replay-async.js +22 -18
- package/dist/scripts/replay-real.js +18 -18
- package/dist/scripts/replay.js +54 -82
- package/dist/scripts/route-provider-conformance-tests.js +391 -0
- package/dist/scripts/session-attach-diagnose.js +20 -20
- package/dist/scripts/session-attach-poc.js +34 -37
- package/dist/scripts/session-benchmark.js +180 -0
- package/dist/scripts/session-dida-live-e2e.js +199 -0
- package/dist/scripts/session-extract-tests.js +14 -35
- package/dist/scripts/session-link-tests.js +478 -0
- package/dist/scripts/session-login.js +27 -46
- package/dist/scripts/session-tests.js +796 -96
- package/dist/scripts/session-zone-crash.js +49 -0
- package/dist/scripts/session-zone-ledger-tests.js +432 -0
- package/dist/scripts/session-zone-observation-tests.js +492 -0
- package/dist/scripts/session-zone-observe.js +135 -0
- package/dist/scripts/session-zone-wiring-tests.js +566 -0
- package/dist/scripts/session-zones-tests.js +374 -0
- package/dist/scripts/sf-live-benchmark.js +367 -0
- package/dist/scripts/sf-live-challenge-stop-tests.js +453 -0
- package/dist/scripts/sf-live-cli-tests.js +92 -0
- package/dist/scripts/sf-live-options.js +46 -0
- package/dist/scripts/sf-manifest-tests.js +30 -0
- package/dist/scripts/sf-manifest.js +33 -0
- package/dist/scripts/sf-soft-score-tests.js +67 -0
- package/dist/scripts/sf-summary-tests.js +324 -0
- package/dist/scripts/sf-summary.js +525 -0
- package/dist/scripts/share-tests.js +432 -0
- package/dist/scripts/skeleton-check.js +16 -14
- package/dist/scripts/skeleton-integration-test.js +28 -11
- package/dist/scripts/skills-contract-tests.js +51 -46
- package/dist/scripts/smoke-session-gate-tests.js +21 -0
- package/dist/scripts/smoke.js +690 -246
- package/dist/scripts/spawn-bounded-abort-tests.js +244 -0
- package/dist/scripts/sponsor-reuse-tests.js +262 -0
- package/dist/scripts/state-cli-tests.js +226 -101
- package/dist/scripts/state-cli.js +565 -198
- package/dist/scripts/state-repair-cli.js +66 -0
- package/dist/scripts/state-repair-tests.js +140 -0
- package/dist/scripts/static-golden-tests.js +165 -0
- package/dist/scripts/time-eval-tests.js +212 -192
- package/dist/scripts/travel-timeline-tests.js +29 -93
- package/dist/scripts/turn-handoff-collect-tests.js +169 -0
- package/dist/scripts/turn-handoff-collect.js +161 -0
- package/dist/scripts/turn-policy-tests.js +55 -0
- package/dist/scripts/typed-contract-canary.js +176 -0
- package/dist/scripts/tz-resolver-sanity.js +50 -0
- package/dist/scripts/tz-resolver-tests.js +584 -0
- package/dist/scripts/unified-tests.js +60 -20
- package/dist/scripts/visa-policy-tests.js +62 -0
- package/dist/scripts/weather-tests.js +276 -41
- package/dist/scripts/wish-channel-gate-tests.js +54 -0
- 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 +46 -0
- package/dist/src/artifact-gate.js +829 -0
- package/dist/src/backend/kernel.js +91 -0
- package/dist/src/backend/modules/booking-copilot.js +92 -0
- package/dist/src/backend/modules/booking-executor.js +124 -0
- package/dist/src/backend/modules/session-search.js +551 -0
- package/dist/src/benchmark-agent-conformance.js +578 -0
- package/dist/src/benchmark-environment-bridge.js +515 -0
- package/dist/src/benchmark-headless-child-diagnostics.js +166 -0
- package/dist/src/benchmark-tool-isolation.js +151 -0
- package/dist/src/bookable-facts.js +533 -0
- package/dist/src/booking-saga.js +97 -0
- package/dist/src/booking-surface/availability-policy.js +697 -0
- package/dist/src/booking-surface/booking-intent.js +43 -0
- package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
- package/dist/src/booking-surface/canonical-schema.js +116 -0
- package/dist/src/booking-surface/contracts.js +75 -0
- package/dist/src/booking-surface/dsh-planner.js +1854 -0
- package/dist/src/booking-surface/dsh-plugin.js +378 -0
- package/dist/src/booking-surface/dsh-worker.js +98 -0
- package/dist/src/booking-surface/error-codes.js +231 -0
- package/dist/src/booking-surface/index.js +9 -0
- package/dist/src/booking-surface/managed-dsh-cleanup-diagnostic.js +97 -0
- package/dist/src/booking-surface/managed-dsh-run-port.js +244 -0
- package/dist/src/booking-surface/profile.js +24 -0
- package/dist/src/booking-surface/recovery-chain.js +367 -0
- package/dist/src/booking-surface/runtime.js +1968 -0
- package/dist/src/booking-surface/server.js +674 -0
- package/dist/src/booking-surface/startup.js +168 -0
- package/dist/src/booking-surface/validation.js +261 -0
- package/dist/src/bridge.js +20 -13
- package/dist/src/city-scenario-tier.js +378 -0
- package/dist/src/companions.js +48 -74
- package/dist/src/contracts.js +21 -43
- package/dist/src/dsh-llm.js +171 -115
- package/dist/src/engine.js +276 -193
- package/dist/src/evaluation-cadence.js +97 -0
- package/dist/src/evaluation-contracts.js +283 -0
- package/dist/src/flight-pack-adapter.js +54 -0
- package/dist/src/flight-pack-contract.js +2 -0
- package/dist/src/flyai-setup-tool.js +70 -0
- package/dist/src/gotry-backend.js +125 -0
- package/dist/src/hotel-date-gate.js +118 -0
- package/dist/src/i18n.js +88 -0
- package/dist/src/index.js +2385 -881
- package/dist/src/itinerary-deck.js +139 -0
- package/dist/src/itinerary-doc-shared.js +661 -0
- package/dist/src/itinerary-html.js +131 -0
- package/dist/src/journey.js +155 -108
- package/dist/src/lavish-tools.js +400 -0
- package/dist/src/ledger-repair-apply.js +538 -0
- package/dist/src/ledger-repair-plan.js +463 -0
- package/dist/src/loop.js +331 -161
- package/dist/src/m3-cohort.js +1577 -0
- 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/outcome-projection.js +746 -0
- package/dist/src/realtime-pricing.js +103 -0
- package/dist/src/recall/card.js +74 -0
- package/dist/src/recall/evaluator.js +140 -0
- package/dist/src/recall/integration.js +17 -0
- package/dist/src/recall/tick.js +166 -0
- package/dist/src/session-link/plan-it-action.js +48 -0
- package/dist/src/session-link/session-link.js +248 -0
- package/dist/src/session-zone-ledger.js +312 -0
- package/dist/src/session-zone-observation.js +354 -0
- package/dist/src/session-zone-wiring.js +658 -0
- package/dist/src/session-zones.js +510 -0
- package/dist/src/share/adapters.js +51 -0
- package/dist/src/share/share-consent.js +76 -0
- package/dist/src/share/share-deck.js +58 -0
- package/dist/src/share/share-token.js +146 -0
- package/dist/src/slot-spec.js +84 -106
- package/dist/src/state-ledger.js +696 -524
- package/dist/src/state-repair.js +299 -0
- 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 +287 -0
- package/dist/src/turn-policy.js +118 -0
- package/dist/src/tz-resolver.js +143 -0
- package/dist/src/unified.js +600 -360
- package/dist/src/wish-pool.js +42 -12
- package/dist/src/write-gate.js +495 -0
- package/dist/src/z3-shared.js +284 -0
- package/extension/README.md +21 -0
- package/extension/README.zh-CN.md +21 -0
- package/extension/background.js +463 -0
- package/extension/content-bridge.js +270 -0
- package/extension/content-main.js +111 -0
- package/extension/manifest.json +60 -0
- package/package.json +407 -14
- package/schemas/booking.intent.schema.json +193 -0
- package/schemas/booking.surface.schema.json +2622 -0
- package/ts/capabilities/agent-reach-deep.ts +114 -0
- package/ts/capabilities/agent-reach.ts +198 -0
- package/ts/capabilities/anything.ts +282 -0
- package/ts/capabilities/artifacts.ts +428 -0
- package/ts/capabilities/fact-log.ts +54 -0
- package/ts/capabilities/flyai-config.ts +388 -0
- package/ts/capabilities/flyai.ts +1274 -0
- package/ts/capabilities/hbcli.ts +253 -71
- package/ts/capabilities/incident-log.ts +88 -59
- package/ts/capabilities/itinerary-artifact.ts +214 -0
- package/ts/capabilities/itinerary-deck-artifact.ts +218 -0
- package/ts/capabilities/itinerary-deck-export.ts +378 -0
- package/ts/capabilities/opensky.ts +121 -0
- package/ts/capabilities/session/action-cache.ts +148 -0
- package/ts/capabilities/session/adapters/ctrip-flight.ts +181 -0
- package/ts/capabilities/session/adapters/ctrip-hotel.ts +238 -0
- package/ts/capabilities/session/adapters/dida-portal.ts +338 -0
- package/ts/capabilities/session/adapters/meituan-local.ts +70 -0
- package/ts/capabilities/session/adapters/rail-12306.ts +392 -0
- package/ts/capabilities/session/benchmark.ts +276 -0
- package/ts/capabilities/session/bridge-ledger.ts +333 -0
- package/ts/capabilities/session/extension-bridge.ts +794 -0
- package/ts/capabilities/session/extension-channel.ts +144 -0
- package/ts/capabilities/session/extension-distribution.ts +266 -0
- package/ts/capabilities/session/extract.ts +54 -0
- package/ts/capabilities/session/golden-score.ts +139 -0
- package/ts/capabilities/session/health-watch.ts +264 -0
- package/ts/capabilities/session/read-guard.ts +141 -0
- package/ts/capabilities/session/static-flight-golden.ts +209 -0
- package/ts/capabilities/session/transport.ts +146 -0
- package/ts/capabilities/session/wizard.ts +140 -0
- package/ts/capabilities/session-consent.ts +211 -0
- package/ts/capabilities/session-login.ts +208 -0
- package/ts/capabilities/session-search.ts +893 -0
- package/ts/capabilities/weather.ts +330 -0
- package/ts/dsh-runtime/vendor/README.md +68 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/LICENSE +21 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +405 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +404 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/client/client.js +1584 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/cordis.patch.yml +5 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/icon.svg +5 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/amap.js +595 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/nominatim.js +63 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/osrm.js +58 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/photon.js +55 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-file.js +87 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-route.js +131 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config.js +16 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/geo.js +232 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/index.js +120 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +40 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/staticmap-route.js +115 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/geocode.js +127 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/poi.js +84 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/routes.js +267 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/amap.d.ts +90 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/nominatim.d.ts +17 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/osrm.d.ts +15 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/photon.d.ts +22 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-file.d.ts +33 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-route.d.ts +23 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config.d.ts +23 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/geo.d.ts +61 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/index.d.ts +18 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +14 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/staticmap-route.d.ts +31 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/geocode.d.ts +11 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/poi.d.ts +9 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/routes.d.ts +75 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/types.d.ts +68 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types.js +16 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +97 -0
- package/ts/package.json +293 -3
- package/ts/scripts/async-collect.ts +91 -0
- package/ts/scripts/map-tools-vendor-package-proof.ts +404 -0
- package/ts/scripts/skeleton-check.ts +1 -1
- package/ts/scripts/skeleton-integration-test.ts +23 -1
- package/ts/scripts/state-cli.ts +586 -0
- package/ts/scripts/turn-handoff-collect.ts +178 -0
- package/ts/src/artifact-gate.ts +991 -0
- package/ts/src/benchmark-agent-conformance.ts +619 -0
- package/ts/src/benchmark-environment-bridge.ts +538 -0
- package/ts/src/benchmark-headless-child-diagnostics.ts +184 -0
- package/ts/src/benchmark-tool-isolation.ts +166 -0
- package/ts/src/bookable-facts.ts +837 -0
- package/ts/src/booking-saga.ts +133 -0
- package/ts/src/booking-surface/availability-policy.ts +522 -0
- package/ts/src/booking-surface/booking-intent.ts +86 -0
- package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
- package/ts/src/booking-surface/canonical-schema.js +116 -0
- package/ts/src/booking-surface/contracts.ts +475 -0
- package/ts/src/booking-surface/dsh-planner.ts +1949 -0
- package/ts/src/booking-surface/dsh-plugin.js +378 -0
- package/ts/src/booking-surface/dsh-worker.ts +110 -0
- package/ts/src/booking-surface/error-codes.ts +241 -0
- package/ts/src/booking-surface/index.ts +9 -0
- package/ts/src/booking-surface/managed-dsh-cleanup-diagnostic.ts +151 -0
- package/ts/src/booking-surface/managed-dsh-run-port.ts +273 -0
- package/ts/src/booking-surface/profile.ts +42 -0
- package/ts/src/booking-surface/recovery-chain.ts +409 -0
- package/ts/src/booking-surface/runtime.ts +1793 -0
- package/ts/src/booking-surface/server.ts +733 -0
- package/ts/src/booking-surface/startup.ts +201 -0
- package/ts/src/booking-surface/validation.ts +217 -0
- package/ts/src/companions.ts +113 -0
- package/ts/src/contracts.ts +17 -3
- package/ts/src/dsh-llm.ts +152 -17
- package/ts/src/flyai-setup-tool.ts +77 -0
- package/ts/src/index.ts +2123 -200
- package/ts/src/itinerary-deck.ts +189 -0
- package/ts/src/itinerary-doc-shared.ts +741 -0
- package/ts/src/itinerary-html.ts +171 -0
- package/ts/src/loop.ts +234 -26
- package/ts/src/memory-capture.ts +283 -0
- package/ts/src/memory-decay.ts +55 -0
- package/ts/src/memory-utility.ts +70 -0
- package/ts/src/mock-llm.ts +4 -8
- package/ts/src/model.ts +88 -2
- package/ts/src/recall/card.ts +106 -0
- package/ts/src/recall/evaluator.ts +170 -0
- package/ts/src/recall/integration.ts +35 -0
- package/ts/src/recall/tick.ts +213 -0
- package/ts/src/session-link/plan-it-action.ts +78 -0
- package/ts/src/session-link/session-link.ts +275 -0
- package/ts/src/share/adapters.ts +93 -0
- package/ts/src/share/share-consent.ts +105 -0
- package/ts/src/share/share-deck.ts +85 -0
- package/ts/src/share/share-token.ts +166 -0
- package/ts/src/slot-spec.ts +172 -0
- package/ts/src/state-ledger.ts +1166 -0
- package/ts/src/subagent-job-id-guard.ts +72 -0
- package/ts/src/time-anchor.ts +338 -0
- package/ts/src/tool-packet.ts +32 -0
- package/ts/src/travel-slots.ts +219 -0
- package/ts/src/travel-timeline.ts +83 -0
- package/ts/src/turn-deadline.ts +366 -0
- package/ts/src/turn-policy.ts +154 -0
- package/ts/src/unified.ts +444 -105
- package/ts/src/wish-pool.ts +79 -0
- package/dist/scripts/session-attach-wait.js +0 -41
|
@@ -0,0 +1,1274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FlyAI 官方通道能力层(飞猪开放平台,data-sources.md §8 / RFC P0;issue #521)。
|
|
3
|
+
*
|
|
4
|
+
* 完整对接官方 @fly-ai/flyai-cli 1.0.16 的 8 个公开只读命令:
|
|
5
|
+
* flight/train: search-flight / search-train
|
|
6
|
+
* hotel: search-hotel(别名 search-hotels)
|
|
7
|
+
* poi: search-poi
|
|
8
|
+
* keyword: keyword-search(别名 fliggy-fast-search)
|
|
9
|
+
* ai: ai-search
|
|
10
|
+
* marriott: search-marriott-hotel / search-marriott-package
|
|
11
|
+
*
|
|
12
|
+
* 链路(CLI spawn,与 hbcli/weather 同构):
|
|
13
|
+
* gotry capabilities/flyai.ts → npx -y @fly-ai/flyai-cli <command> [opts]
|
|
14
|
+
* → 飞猪 MCP(实时直连官方商品库)
|
|
15
|
+
*
|
|
16
|
+
* 契约(L4 不变量):
|
|
17
|
+
* - 只读:8 命令全只读,交易经 jumpUrl/detailUrl 由人完成(WriteGate 同构);
|
|
18
|
+
* - 永不抛错:网络/超时/解析失败一律降级为结构化 verdict;
|
|
19
|
+
* - 证据链:成功 [实时API:flyai@ts];失败 [实时API:flyai@error@ts];
|
|
20
|
+
* - key 解析/endpoint 覆盖口径与官方 CLI 同源(flyai-config.ts);
|
|
21
|
+
* - 子进程资源有界:进程组边界统一复用 spawn-bounded.ts(与 hbcli/anything
|
|
22
|
+
* 同策略),主进程退出后 stderr drain 最多等 drainMs(默认 3s),随后杀残留
|
|
23
|
+
* 后代并以 drain-timeout fail-closed 返回——不被持管道的后代挂死,也不把
|
|
24
|
+
* 不完整收集写成成功库存事实。
|
|
25
|
+
*
|
|
26
|
+
* 故障闭集(verdict):
|
|
27
|
+
* hit / miss / auth-error(401)/ forbidden(403)/
|
|
28
|
+
* needs-setup(匿名试用 429 Trial limit)/ rate-limited(普通 429)/
|
|
29
|
+
* timeout / error(网络/Sentinel/malformed)。
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { closeSync, openSync, readFileSync, unlinkSync } from 'node:fs'
|
|
33
|
+
import { tmpdir } from 'node:os'
|
|
34
|
+
import { join } from 'node:path'
|
|
35
|
+
import { resolveFlyaiEndpoint, resolveFlyaiKey, type FlyaiKeySource } from './flyai-config.ts'
|
|
36
|
+
import { spawnBounded } from './spawn-bounded.ts'
|
|
37
|
+
|
|
38
|
+
export type FlyaiKind =
|
|
39
|
+
| 'flight'
|
|
40
|
+
| 'train'
|
|
41
|
+
| 'hotel'
|
|
42
|
+
| 'poi'
|
|
43
|
+
| 'keyword'
|
|
44
|
+
| 'ai'
|
|
45
|
+
| 'marriott-hotel'
|
|
46
|
+
| 'marriott-package'
|
|
47
|
+
|
|
48
|
+
export type FlyaiVerdict =
|
|
49
|
+
| 'hit'
|
|
50
|
+
| 'miss'
|
|
51
|
+
| 'error'
|
|
52
|
+
| 'needs-setup'
|
|
53
|
+
| 'auth-error'
|
|
54
|
+
| 'forbidden'
|
|
55
|
+
| 'rate-limited'
|
|
56
|
+
| 'timeout'
|
|
57
|
+
| 'cancelled'
|
|
58
|
+
|
|
59
|
+
/** 本机子进程生命周期终止;它不是可重试的上游故障。 */
|
|
60
|
+
export type FlyaiLocalTermination = 'deadline' | 'signal' | 'spawn' | 'empty-exit' | 'drain-timeout'
|
|
61
|
+
|
|
62
|
+
export interface FlyaiQuery {
|
|
63
|
+
kind: FlyaiKind
|
|
64
|
+
|
|
65
|
+
// ── flight / train ──
|
|
66
|
+
/** 出发城市/机场(中文或 ID);官方唯一必填 */
|
|
67
|
+
origin?: string
|
|
68
|
+
destination?: string
|
|
69
|
+
/** 出发日 YYYY-MM-DD */
|
|
70
|
+
depDate?: string
|
|
71
|
+
depDateStart?: string
|
|
72
|
+
depDateEnd?: string
|
|
73
|
+
/** 回程日 YYYY-MM-DD */
|
|
74
|
+
backDate?: string
|
|
75
|
+
/** 1=直达 2=中转 */
|
|
76
|
+
journeyType?: number
|
|
77
|
+
/** 舱位/坐席(逗号分隔) */
|
|
78
|
+
seatClassName?: string
|
|
79
|
+
/** 航班号/车次(逗号分隔) */
|
|
80
|
+
transportNo?: string
|
|
81
|
+
/** 中转城市(逗号分隔) */
|
|
82
|
+
transferCity?: string
|
|
83
|
+
depHourStart?: number
|
|
84
|
+
depHourEnd?: number
|
|
85
|
+
arrHourStart?: number
|
|
86
|
+
arrHourEnd?: number
|
|
87
|
+
/** 总时长上限(小时) */
|
|
88
|
+
totalDurationHour?: number
|
|
89
|
+
/** 最高价 CNY */
|
|
90
|
+
maxPrice?: number
|
|
91
|
+
/** 机票/火车排序 1–8;万豪套餐 price_asc/price_desc */
|
|
92
|
+
sortType?: string
|
|
93
|
+
|
|
94
|
+
// ── hotel / marriott-hotel ──
|
|
95
|
+
/** 目的地(国家/省/市/区中文) */
|
|
96
|
+
destName?: string
|
|
97
|
+
checkInDate?: string
|
|
98
|
+
checkOutDate?: string
|
|
99
|
+
/** 商业区/地标/酒店名关键词 */
|
|
100
|
+
keyWords?: string
|
|
101
|
+
/** 周边景点名 */
|
|
102
|
+
poiName?: string
|
|
103
|
+
/** 酒店/民宿/客栈 */
|
|
104
|
+
hotelTypes?: string
|
|
105
|
+
/** distance_asc / rate_desc / price_asc / price_desc / no_rank */
|
|
106
|
+
sort?: string
|
|
107
|
+
/** 1–5 逗号分隔 */
|
|
108
|
+
hotelStars?: string
|
|
109
|
+
/** king/twin/multi 逗号分隔 */
|
|
110
|
+
hotelBedTypes?: string
|
|
111
|
+
|
|
112
|
+
// ── poi ──
|
|
113
|
+
/** 景点所在城市(必填) */
|
|
114
|
+
cityName?: string
|
|
115
|
+
/** 景点等级 1–5 */
|
|
116
|
+
poiLevel?: number
|
|
117
|
+
/** 景点名关键词 */
|
|
118
|
+
keyword?: string
|
|
119
|
+
/** 景点类别(闭集,见 FLYAI_POI_CATEGORIES) */
|
|
120
|
+
category?: string
|
|
121
|
+
|
|
122
|
+
// ── keyword-search / ai-search ──
|
|
123
|
+
query?: string
|
|
124
|
+
|
|
125
|
+
// ── marriott-hotel only ──
|
|
126
|
+
hotelBrands?: string
|
|
127
|
+
hotelName?: string
|
|
128
|
+
/** 万豪套餐的省份或城市筛选。 */
|
|
129
|
+
provinceOrCity?: string
|
|
130
|
+
|
|
131
|
+
// ── 运行面 ──
|
|
132
|
+
/** 默认 30_000 ms(npx 冷启动 + 远端检索) */
|
|
133
|
+
timeoutMs?: number
|
|
134
|
+
/** 主进程 exit 后等待 stderr 管道释放的上限(默认 3_000 ms);超时杀组并 fail-closed */
|
|
135
|
+
drainMs?: number
|
|
136
|
+
/** 显式 CLI bin/命令(默认 npx;测试注入假脚本) */
|
|
137
|
+
cliBin?: string
|
|
138
|
+
/** cliBin 之后的前缀参数(默认 ['-y','@fly-ai/flyai-cli']) */
|
|
139
|
+
cliPrefixArgs?: string[]
|
|
140
|
+
/** 取消信号:aborted 时不 spawn;在途 abort 杀整组进程,verdict='cancelled' */
|
|
141
|
+
signal?: AbortSignal
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface FlyaiOption {
|
|
145
|
+
no: string
|
|
146
|
+
name: string
|
|
147
|
+
depDateTime: string
|
|
148
|
+
arrDateTime: string
|
|
149
|
+
depStation: string
|
|
150
|
+
arrStation: string
|
|
151
|
+
durationMin: number
|
|
152
|
+
price: number
|
|
153
|
+
seatClass?: string
|
|
154
|
+
jumpUrl?: string
|
|
155
|
+
priceRaw?: string
|
|
156
|
+
nonstop?: boolean
|
|
157
|
+
journeyType?: string
|
|
158
|
+
/** 保留官方同票联程的完整分段,避免把中转首段冒充全程。 */
|
|
159
|
+
segments?: FlyaiTransportSegment[]
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface FlyaiTransportSegment {
|
|
163
|
+
no: string
|
|
164
|
+
name: string
|
|
165
|
+
depDateTime: string
|
|
166
|
+
arrDateTime: string
|
|
167
|
+
depStation: string
|
|
168
|
+
arrStation: string
|
|
169
|
+
durationMin: number
|
|
170
|
+
seatClass?: string
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface FlyaiHotelOption {
|
|
174
|
+
name: string
|
|
175
|
+
star?: string
|
|
176
|
+
price: number
|
|
177
|
+
priceRaw?: string
|
|
178
|
+
rate?: string
|
|
179
|
+
address?: string
|
|
180
|
+
poi?: string
|
|
181
|
+
hotelId?: string
|
|
182
|
+
jumpUrl?: string
|
|
183
|
+
mainPic?: string
|
|
184
|
+
score?: string
|
|
185
|
+
scoreDesc?: string
|
|
186
|
+
review?: string
|
|
187
|
+
brandName?: string
|
|
188
|
+
latitude?: string
|
|
189
|
+
longitude?: string
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface FlyaiPoiTicketInfo {
|
|
193
|
+
price?: string | null
|
|
194
|
+
priceDate?: string
|
|
195
|
+
ticketName?: string
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export interface FlyaiPoiOption {
|
|
199
|
+
name: string
|
|
200
|
+
poiId?: string
|
|
201
|
+
mainPic?: string
|
|
202
|
+
jumpUrl?: string
|
|
203
|
+
address?: string
|
|
204
|
+
freePoiStatus?: string
|
|
205
|
+
ticketInfo?: FlyaiPoiTicketInfo
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export interface FlyaiKeywordOption {
|
|
209
|
+
title: string
|
|
210
|
+
jumpUrl?: string
|
|
211
|
+
picUrl?: string
|
|
212
|
+
price?: string
|
|
213
|
+
scoreDesc?: string
|
|
214
|
+
star?: string
|
|
215
|
+
tags?: string[]
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export interface FlyaiMarriottPackageOption {
|
|
219
|
+
name: string
|
|
220
|
+
/** Official CLI field; name is retained as the normalized compatibility field. */
|
|
221
|
+
title?: string
|
|
222
|
+
itemId?: string
|
|
223
|
+
brandName?: string
|
|
224
|
+
hotelName?: string
|
|
225
|
+
cityName?: string
|
|
226
|
+
price?: string
|
|
227
|
+
detailUrl?: string
|
|
228
|
+
mainPic?: string
|
|
229
|
+
picUrl?: string
|
|
230
|
+
sellingPoint?: string
|
|
231
|
+
sellPoint?: string
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export interface FlyaiResult {
|
|
235
|
+
ok: boolean
|
|
236
|
+
via: 'flyai' | 'flyai-error'
|
|
237
|
+
evidence: string
|
|
238
|
+
latencyMs: number
|
|
239
|
+
verdict: FlyaiVerdict
|
|
240
|
+
/** true = 已识别的上游普通网络/HTTP 5xx/429 瞬时失败,可交给 effect 层退避重试 */
|
|
241
|
+
retryable?: boolean
|
|
242
|
+
/** 存在时表示请求未得到可判定的上游响应,应 fail-closed 且只执行一次。 */
|
|
243
|
+
localTermination?: FlyaiLocalTermination
|
|
244
|
+
kind: FlyaiKind
|
|
245
|
+
options?: FlyaiOption[]
|
|
246
|
+
hotels?: FlyaiHotelOption[]
|
|
247
|
+
pois?: FlyaiPoiOption[]
|
|
248
|
+
keywords?: FlyaiKeywordOption[]
|
|
249
|
+
packages?: FlyaiMarriottPackageOption[]
|
|
250
|
+
/** ai-search 上游自由形状数据(原样透传,不猜成事实) */
|
|
251
|
+
aiData?: unknown
|
|
252
|
+
error?: string
|
|
253
|
+
/** 恢复/补配指引(verdict≠hit/miss 时) */
|
|
254
|
+
setup?: string
|
|
255
|
+
/** 本次调用实际生效的 key 来源 */
|
|
256
|
+
keySource?: FlyaiKeySource
|
|
257
|
+
/** true = endpoint 被 DEBUG_FLYAI_MCP_URL 覆盖 */
|
|
258
|
+
endpointDebug?: boolean
|
|
259
|
+
systemMessage?: string
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
interface RawItem {
|
|
263
|
+
journeys?: Array<{
|
|
264
|
+
journeyType?: string | number
|
|
265
|
+
totalDuration?: string | number
|
|
266
|
+
segments?: Array<{
|
|
267
|
+
marketingTransportName?: string
|
|
268
|
+
marketingTransportNo?: string
|
|
269
|
+
depDateTime?: string
|
|
270
|
+
arrDateTime?: string
|
|
271
|
+
depStationName?: string
|
|
272
|
+
arrStationName?: string
|
|
273
|
+
duration?: string | number
|
|
274
|
+
seatClassName?: string
|
|
275
|
+
}>
|
|
276
|
+
}>
|
|
277
|
+
adultPrice?: string
|
|
278
|
+
ticketPrice?: string
|
|
279
|
+
price?: string
|
|
280
|
+
jumpUrl?: string
|
|
281
|
+
// hotel
|
|
282
|
+
name?: string
|
|
283
|
+
shId?: string
|
|
284
|
+
shid?: string
|
|
285
|
+
star?: string
|
|
286
|
+
rate?: string | null
|
|
287
|
+
address?: string
|
|
288
|
+
interestsPoi?: string
|
|
289
|
+
nearbyPoi?: string
|
|
290
|
+
detailUrl?: string
|
|
291
|
+
mainPic?: string
|
|
292
|
+
score?: string | null
|
|
293
|
+
scoreDesc?: string
|
|
294
|
+
review?: string
|
|
295
|
+
brandName?: string
|
|
296
|
+
latitude?: string
|
|
297
|
+
longitude?: string
|
|
298
|
+
// poi
|
|
299
|
+
id?: string | number
|
|
300
|
+
freePoiStatus?: string
|
|
301
|
+
ticketInfo?: unknown
|
|
302
|
+
// keyword
|
|
303
|
+
info?: unknown
|
|
304
|
+
// marriott package
|
|
305
|
+
title?: string
|
|
306
|
+
itemId?: string
|
|
307
|
+
picUrl?: string
|
|
308
|
+
sellPoint?: string
|
|
309
|
+
hotelName?: string
|
|
310
|
+
cityName?: string
|
|
311
|
+
sellingPoint?: string
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
interface ParsedItems<T> {
|
|
315
|
+
options: T[]
|
|
316
|
+
malformedCount: number
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function nonEmptyText(value: unknown): string | undefined {
|
|
320
|
+
if (typeof value !== 'string') return undefined
|
|
321
|
+
const text = value.trim()
|
|
322
|
+
return text || undefined
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function optionalText(value: unknown, allowNull = false): string | undefined | null {
|
|
326
|
+
if (value === undefined) return undefined
|
|
327
|
+
if (allowNull && value === null) return null
|
|
328
|
+
return nonEmptyText(value)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function positiveFiniteNumber(value: unknown): number | undefined {
|
|
332
|
+
const number = typeof value === 'number'
|
|
333
|
+
? value
|
|
334
|
+
: typeof value === 'string' && value.trim() ? Number(value.trim()) : Number.NaN
|
|
335
|
+
return Number.isFinite(number) && number > 0 ? number : undefined
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function positiveFiniteDuration(value: unknown): number | undefined {
|
|
339
|
+
if (typeof value === 'number') return positiveFiniteNumber(value)
|
|
340
|
+
const text = nonEmptyText(value)
|
|
341
|
+
if (!text) return undefined
|
|
342
|
+
const minutes = /^(\d+(?:\.\d+)?)\s*分钟$/.exec(text)?.[1]
|
|
343
|
+
return positiveFiniteNumber(minutes ?? text)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function parseTransportPrice(value: unknown): { price: number; priceRaw?: string } | undefined {
|
|
347
|
+
const raw = nonEmptyText(value)
|
|
348
|
+
if (!raw) return undefined
|
|
349
|
+
const bare = raw.replace(/^¥\s*/, '')
|
|
350
|
+
if (/^\d+(?:\.\d+)?$/.test(bare)) {
|
|
351
|
+
const price = Number(bare)
|
|
352
|
+
return Number.isFinite(price) && price > 0 ? { price } : undefined
|
|
353
|
+
}
|
|
354
|
+
// 未鉴权态打码 1xxx/¥7xx:保留原值,绝不猜成数字。
|
|
355
|
+
if (/^\d+x+$/.test(bare)) return { price: 0, priceRaw: raw }
|
|
356
|
+
return undefined
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* 从 CLI stdout 提取首个含 data.itemList 的完整 JSON 对象。
|
|
361
|
+
* 扫描器识别字符串与转义;npx 前后缀日志不影响切分。
|
|
362
|
+
*/
|
|
363
|
+
export function parseFlyaiItemList(stdout: string): unknown[] {
|
|
364
|
+
let start = -1
|
|
365
|
+
let depth = 0
|
|
366
|
+
let inString = false
|
|
367
|
+
let escaped = false
|
|
368
|
+
let sawIncompleteObject = false
|
|
369
|
+
|
|
370
|
+
for (let index = 0; index < stdout.length; index += 1) {
|
|
371
|
+
const char = stdout[index]!
|
|
372
|
+
if (start < 0) {
|
|
373
|
+
if (char === '{') {
|
|
374
|
+
start = index
|
|
375
|
+
depth = 1
|
|
376
|
+
inString = false
|
|
377
|
+
escaped = false
|
|
378
|
+
sawIncompleteObject = true
|
|
379
|
+
}
|
|
380
|
+
continue
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (inString) {
|
|
384
|
+
if (escaped) escaped = false
|
|
385
|
+
else if (char === '\\') escaped = true
|
|
386
|
+
else if (char === '"') inString = false
|
|
387
|
+
continue
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (char === '"') inString = true
|
|
391
|
+
else if (char === '{') depth += 1
|
|
392
|
+
else if (char === '}') {
|
|
393
|
+
depth -= 1
|
|
394
|
+
if (depth !== 0) continue
|
|
395
|
+
|
|
396
|
+
const candidate = stdout.slice(start, index + 1)
|
|
397
|
+
start = -1
|
|
398
|
+
sawIncompleteObject = false
|
|
399
|
+
try {
|
|
400
|
+
const parsed = JSON.parse(candidate) as { data?: { itemList?: unknown } }
|
|
401
|
+
if (Array.isArray(parsed.data?.itemList)) return parsed.data.itemList
|
|
402
|
+
} catch {
|
|
403
|
+
// 前缀日志里的成对花括号;继续找下一完整对象。
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
throw new Error(sawIncompleteObject
|
|
409
|
+
? 'incomplete FlyAI JSON object'
|
|
410
|
+
: 'no complete FlyAI itemList JSON object')
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** 解析 stdout 中首个完整 JSON 对象(不限 itemList;ai-search 用) */
|
|
414
|
+
export function parseFlyaiJsonEnvelope(stdout: string): Record<string, unknown> {
|
|
415
|
+
let start = -1
|
|
416
|
+
let depth = 0
|
|
417
|
+
let inString = false
|
|
418
|
+
let escaped = false
|
|
419
|
+
|
|
420
|
+
for (let index = 0; index < stdout.length; index += 1) {
|
|
421
|
+
const char = stdout[index]!
|
|
422
|
+
if (start < 0) {
|
|
423
|
+
if (char === '{') { start = index; depth = 1; inString = false; escaped = false }
|
|
424
|
+
continue
|
|
425
|
+
}
|
|
426
|
+
if (inString) {
|
|
427
|
+
if (escaped) escaped = false
|
|
428
|
+
else if (char === '\\') escaped = true
|
|
429
|
+
else if (char === '"') inString = false
|
|
430
|
+
continue
|
|
431
|
+
}
|
|
432
|
+
if (char === '"') inString = true
|
|
433
|
+
else if (char === '{') depth += 1
|
|
434
|
+
else if (char === '}') {
|
|
435
|
+
depth -= 1
|
|
436
|
+
if (depth === 0) {
|
|
437
|
+
const candidate = stdout.slice(start, index + 1)
|
|
438
|
+
try { return JSON.parse(candidate) as Record<string, unknown> } catch { start = -1; depth = 0 }
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
throw new Error('no complete FlyAI JSON envelope')
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
interface ShResult {
|
|
446
|
+
code: number
|
|
447
|
+
stdout: string
|
|
448
|
+
stderr: string
|
|
449
|
+
error?: string
|
|
450
|
+
timedOut?: boolean
|
|
451
|
+
drainTimedOut?: boolean
|
|
452
|
+
/** 调用被 AbortSignal 取消(预先 abort=未 spawn;在途 abort=进程组被杀) */
|
|
453
|
+
cancelled?: boolean
|
|
454
|
+
signal?: NodeJS.Signals | null
|
|
455
|
+
localTermination?: FlyaiLocalTermination
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
interface FlyaiSensitiveContext {
|
|
459
|
+
key?: string
|
|
460
|
+
debugEndpoint?: string
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
interface FlyaiEnvContext {
|
|
464
|
+
note: { keySource: FlyaiKeySource; endpointDebug?: boolean }
|
|
465
|
+
sensitive: FlyaiSensitiveContext
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const REDACTED_FLYAI_KEY = '[已隐藏 FlyAI key]'
|
|
469
|
+
const REDACTED_FLYAI_ENDPOINT = '?(已隐藏 endpoint 敏感参数)'
|
|
470
|
+
|
|
471
|
+
function shellUrlToken(value: string): { token: string; suffix: string } {
|
|
472
|
+
const match = /[),.;!?,。!?]+$/.exec(value)
|
|
473
|
+
const suffix = match?.[0] ?? ''
|
|
474
|
+
return { token: suffix ? value.slice(0, -suffix.length) : value, suffix }
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** 只针对实际 DEBUG endpoint 的 host 脱去 userinfo/query/fragment。 */
|
|
478
|
+
function redactFlyaiText(value: string, sensitive: FlyaiSensitiveContext): string {
|
|
479
|
+
let output = value
|
|
480
|
+
if (sensitive.key) output = output.split(sensitive.key).join(REDACTED_FLYAI_KEY)
|
|
481
|
+
if (!sensitive.debugEndpoint) return output
|
|
482
|
+
|
|
483
|
+
let debugHost: string
|
|
484
|
+
try {
|
|
485
|
+
debugHost = new URL(sensitive.debugEndpoint).host.toLowerCase()
|
|
486
|
+
} catch {
|
|
487
|
+
return output
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return output.replace(/\bhttps?:\/\/[^\s"'<>]+/gi, (raw) => {
|
|
491
|
+
const { token, suffix } = shellUrlToken(raw)
|
|
492
|
+
try {
|
|
493
|
+
const url = new URL(token)
|
|
494
|
+
if (url.host.toLowerCase() !== debugHost) return raw
|
|
495
|
+
const path = url.pathname || '/'
|
|
496
|
+
const hasSensitivePart = Boolean(url.username || url.password || url.search || url.hash)
|
|
497
|
+
return `${url.protocol}//${url.host}${path}${hasSensitivePart ? REDACTED_FLYAI_ENDPOINT : ''}${suffix}`
|
|
498
|
+
} catch {
|
|
499
|
+
return raw
|
|
500
|
+
}
|
|
501
|
+
})
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function redactFlyaiValue(value: unknown, sensitive: FlyaiSensitiveContext): unknown {
|
|
505
|
+
if (typeof value === 'string') return redactFlyaiText(value, sensitive)
|
|
506
|
+
if (Array.isArray(value)) return value.map(item => redactFlyaiValue(item, sensitive))
|
|
507
|
+
if (value && typeof value === 'object') {
|
|
508
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, redactFlyaiValue(item, sensitive)]))
|
|
509
|
+
}
|
|
510
|
+
return value
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function safeFlyaiResult<T extends FlyaiResult>(result: T, sensitive: FlyaiSensitiveContext): T {
|
|
514
|
+
return redactFlyaiValue(result, sensitive) as T
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* 子进程 stdout 走临时文件而非管道(issue #84):管道在 CLI 异步写后
|
|
519
|
+
* 立即 exit 时丢尾部(实测截断 ~7.6KB,exit=0 静默);文件无此问题。
|
|
520
|
+
*
|
|
521
|
+
* 资源边界(#516):进程组/清理策略统一复用 capabilities/spawn-bounded.ts
|
|
522
|
+
* (与 hbcli/anything 同一进程树边界),本函数只叠加 flyai 特有的
|
|
523
|
+
* 文件 stdout 收集与本地终止分类——主进程 exit 后 stderr drain 最多等
|
|
524
|
+
* drainMs(默认 3s),超时杀组、drainTimedOut 结算,不写成成功事实。
|
|
525
|
+
*/
|
|
526
|
+
function sh(
|
|
527
|
+
cmd: string,
|
|
528
|
+
args: string[],
|
|
529
|
+
opts: { timeoutMs: number; env?: NodeJS.ProcessEnv; drainMs?: number; signal?: AbortSignal },
|
|
530
|
+
): Promise<ShResult> {
|
|
531
|
+
// 预先取消:不 spawn 任何进程,不产生 stdout 临时文件。
|
|
532
|
+
if (opts.signal?.aborted) {
|
|
533
|
+
return Promise.resolve({ code: -1, stdout: '', stderr: '', cancelled: true, localTermination: 'signal' })
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const outFile = join(tmpdir(), `gotry-flyai-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.out`)
|
|
537
|
+
const outFd = openSync(outFile, 'w')
|
|
538
|
+
|
|
539
|
+
const readOut = (): string => {
|
|
540
|
+
try { return readFileSync(outFile, 'utf8') } catch { return '' }
|
|
541
|
+
}
|
|
542
|
+
const cleanupFile = (): void => {
|
|
543
|
+
try { closeSync(outFd) } catch { /* ignore */ }
|
|
544
|
+
try { unlinkSync(outFile) } catch { /* ignore */ }
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return spawnBounded(cmd, args, {
|
|
548
|
+
env: opts.env ?? process.env,
|
|
549
|
+
cwd: process.cwd(),
|
|
550
|
+
timeoutMs: opts.timeoutMs,
|
|
551
|
+
signal: opts.signal,
|
|
552
|
+
stdoutFd: outFd,
|
|
553
|
+
drainMs: opts.drainMs ?? 3_000,
|
|
554
|
+
}).then((r): ShResult => {
|
|
555
|
+
// 必须先读出 stdout 再清理:closeSync 之后文件即删除,
|
|
556
|
+
// 先 cleanup 会把成功响应(含空 itemList miss)读成空串。
|
|
557
|
+
const stdout = readOut()
|
|
558
|
+
cleanupFile()
|
|
559
|
+
const localTermination: FlyaiLocalTermination | undefined = r.aborted
|
|
560
|
+
? 'signal'
|
|
561
|
+
: r.timedOut
|
|
562
|
+
? 'deadline'
|
|
563
|
+
: r.drainTimedOut
|
|
564
|
+
? 'drain-timeout'
|
|
565
|
+
: r.error
|
|
566
|
+
? 'spawn'
|
|
567
|
+
: r.signal
|
|
568
|
+
? 'empty-exit'
|
|
569
|
+
: undefined
|
|
570
|
+
return {
|
|
571
|
+
code: r.code ?? -1,
|
|
572
|
+
stdout,
|
|
573
|
+
stderr: r.stderr,
|
|
574
|
+
...(r.signal !== null && r.signal !== undefined ? { signal: r.signal } : {}),
|
|
575
|
+
...(r.error ? { error: r.error } : {}),
|
|
576
|
+
...(r.timedOut ? { timedOut: true } : {}),
|
|
577
|
+
...(r.drainTimedOut ? { drainTimedOut: true } : {}),
|
|
578
|
+
...(r.aborted ? { cancelled: true } : {}),
|
|
579
|
+
...(localTermination ? { localTermination } : {}),
|
|
580
|
+
}
|
|
581
|
+
})
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function localTerminationResult(
|
|
585
|
+
kind: FlyaiKind,
|
|
586
|
+
termination: FlyaiLocalTermination,
|
|
587
|
+
r: ShResult,
|
|
588
|
+
latencyMs: number,
|
|
589
|
+
ts: string,
|
|
590
|
+
envNote: { keySource: FlyaiKeySource; endpointDebug?: boolean },
|
|
591
|
+
): FlyaiResult {
|
|
592
|
+
const detail = termination === 'spawn'
|
|
593
|
+
? `启动失败${r.error ? `:${r.error}` : ''}`
|
|
594
|
+
: termination === 'signal'
|
|
595
|
+
? `进程收到本机取消信号${r.signal ? `(${r.signal})` : ''}`
|
|
596
|
+
: termination === 'deadline'
|
|
597
|
+
? '达到本地 deadline'
|
|
598
|
+
: termination === 'drain-timeout'
|
|
599
|
+
? `主进程已退出(code ${r.code})但持有 stderr 的后代在 drain 时限内未释放,整组已终止——结果收集不完整`
|
|
600
|
+
: '进程退出但未返回可判定结果'
|
|
601
|
+
return {
|
|
602
|
+
kind, ...envNote, latencyMs, ok: false, via: 'flyai-error', verdict: 'error',
|
|
603
|
+
retryable: false, localTermination: termination,
|
|
604
|
+
evidence: `[实时API:flyai@error@${ts}] local termination: ${termination}`,
|
|
605
|
+
error: `FlyAI 本地终止(${detail})`,
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const YMD = /^\d{4}-\d{2}-\d{2}$/
|
|
610
|
+
|
|
611
|
+
/** search-poi 闭集类别(官方 references,2026-09 核实) */
|
|
612
|
+
export const FLYAI_POI_CATEGORIES = [
|
|
613
|
+
'自然风光', '山湖田园', '森林丛林', '峡谷瀑布', '沙滩海岛', '沙漠草原',
|
|
614
|
+
'人文古迹', '古镇古村', '历史古迹', '园林花园', '宗教场所', '公园乐园',
|
|
615
|
+
'主题乐园', '水上乐园', '影视基地', '动物园', '植物园', '海洋馆', '体育场馆',
|
|
616
|
+
'演出赛事', '剧院剧场', '博物馆', '纪念馆', '展览馆', '地标建筑', '市集',
|
|
617
|
+
'文创街区', '城市观光', '户外活动', '滑雪', '漂流', '冲浪', '潜水', '露营', '温泉',
|
|
618
|
+
] as const
|
|
619
|
+
|
|
620
|
+
const HOTEL_SORTS = ['distance_asc', 'rate_desc', 'price_asc', 'price_desc', 'no_rank']
|
|
621
|
+
const MARRIOTT_PACKAGE_SORTS = ['price_asc', 'price_desc']
|
|
622
|
+
|
|
623
|
+
function badArg(kind: FlyaiKind, error: string, ts: string): FlyaiResult {
|
|
624
|
+
return {
|
|
625
|
+
kind, ok: false, via: 'flyai-error', verdict: 'error', latencyMs: 0,
|
|
626
|
+
evidence: `[实时API:flyai@error@${ts}] bad args`, error,
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function hourOk(value: number): boolean {
|
|
631
|
+
return Number.isInteger(value) && value >= 0 && value <= 23
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/** 各 kind 参数校验 → CLI args;不合法返回 error 串 */
|
|
635
|
+
function buildCliArgs(q: FlyaiQuery): { args?: string[]; error?: string } {
|
|
636
|
+
const csv = (value?: string): string[] => value ? [value] : []
|
|
637
|
+
switch (q.kind) {
|
|
638
|
+
case 'flight':
|
|
639
|
+
case 'train': {
|
|
640
|
+
if (!(q.origin ?? '').trim()) return { error: 'origin(出发城市/机场)required' }
|
|
641
|
+
for (const d of [q.depDate, q.depDateStart, q.depDateEnd, q.backDate]) {
|
|
642
|
+
if (d && !YMD.test(d)) return { error: `日期需 YYYY-MM-DD,实际 ${d}` }
|
|
643
|
+
}
|
|
644
|
+
if (q.journeyType !== undefined && q.journeyType !== 1 && q.journeyType !== 2) {
|
|
645
|
+
return { error: 'journeyType 只接受 1(直达)/2(中转)' }
|
|
646
|
+
}
|
|
647
|
+
for (const h of [q.depHourStart, q.depHourEnd, q.arrHourStart, q.arrHourEnd]) {
|
|
648
|
+
if (h !== undefined && !hourOk(h)) return { error: '小时范围需 0–23 整数' }
|
|
649
|
+
}
|
|
650
|
+
if (q.maxPrice !== undefined && !(q.maxPrice > 0)) return { error: 'maxPrice 需为正数' }
|
|
651
|
+
if (q.totalDurationHour !== undefined && !(q.totalDurationHour > 0)) return { error: 'totalDurationHour 需为正数' }
|
|
652
|
+
const sub = q.kind === 'flight' ? 'search-flight' : 'search-train'
|
|
653
|
+
return {
|
|
654
|
+
args: [
|
|
655
|
+
sub, '--origin', q.origin!.trim(),
|
|
656
|
+
...(q.destination?.trim() ? ['--destination', q.destination.trim()] : []),
|
|
657
|
+
...(q.depDate ? ['--dep-date', q.depDate] : []),
|
|
658
|
+
...(q.depDateStart ? ['--dep-date-start', q.depDateStart] : []),
|
|
659
|
+
...(q.depDateEnd ? ['--dep-date-end', q.depDateEnd] : []),
|
|
660
|
+
...(q.backDate ? ['--back-date', q.backDate] : []),
|
|
661
|
+
...(q.journeyType !== undefined ? ['--journey-type', String(q.journeyType)] : []),
|
|
662
|
+
...csv(q.seatClassName?.trim()).flatMap(v => ['--seat-class-name', v]),
|
|
663
|
+
...csv(q.transportNo?.trim()) .flatMap(v => ['--transport-no', v]),
|
|
664
|
+
...csv(q.transferCity?.trim()) .flatMap(v => ['--transfer-city', v]),
|
|
665
|
+
...(q.depHourStart !== undefined ? ['--dep-hour-start', String(q.depHourStart)] : []),
|
|
666
|
+
...(q.depHourEnd !== undefined ? ['--dep-hour-end', String(q.depHourEnd)] : []),
|
|
667
|
+
...(q.arrHourStart !== undefined ? ['--arr-hour-start', String(q.arrHourStart)] : []),
|
|
668
|
+
...(q.arrHourEnd !== undefined ? ['--arr-hour-end', String(q.arrHourEnd)] : []),
|
|
669
|
+
...(q.totalDurationHour !== undefined ? ['--total-duration-hour', String(q.totalDurationHour)] : []),
|
|
670
|
+
...(q.maxPrice !== undefined ? ['--max-price', String(q.maxPrice)] : []),
|
|
671
|
+
...(q.sortType?.trim() ? ['--sort-type', q.sortType.trim()] : []),
|
|
672
|
+
],
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
case 'hotel':
|
|
676
|
+
case 'marriott-hotel': {
|
|
677
|
+
const dest = (q.destName ?? '').trim()
|
|
678
|
+
if (!dest) return { error: 'destName(目的地 国家/省/市/区)required' }
|
|
679
|
+
const badPair = (q.checkInDate ? 1 : 0) !== (q.checkOutDate ? 1 : 0)
|
|
680
|
+
|| (q.checkInDate && (!YMD.test(q.checkInDate) || !YMD.test(q.checkOutDate ?? '')))
|
|
681
|
+
if (badPair) return { error: 'checkInDate/checkOutDate 须成对且为 YYYY-MM-DD' }
|
|
682
|
+
if (q.sort && !HOTEL_SORTS.includes(q.sort)) return { error: `sort 取值:${HOTEL_SORTS.join('/')}` }
|
|
683
|
+
if (q.maxPrice !== undefined && !(q.maxPrice > 0)) return { error: 'maxPrice 需为正数' }
|
|
684
|
+
if (q.hotelStars && !/^[1-5](,[1-5])*$/.test(q.hotelStars)) return { error: 'hotelStars 为 1–5 逗号分隔' }
|
|
685
|
+
const sub = q.kind === 'hotel' ? 'search-hotel' : 'search-marriott-hotel'
|
|
686
|
+
return {
|
|
687
|
+
args: [
|
|
688
|
+
sub, '--dest-name', dest,
|
|
689
|
+
...(q.keyWords?.trim() ? ['--key-words', q.keyWords.trim()] : []),
|
|
690
|
+
...(q.poiName?.trim() ? ['--poi-name', q.poiName.trim()] : []),
|
|
691
|
+
...(q.hotelTypes?.trim() ? ['--hotel-types', q.hotelTypes.trim()] : []),
|
|
692
|
+
...(q.sort ? ['--sort', q.sort] : []),
|
|
693
|
+
...(q.checkInDate && q.checkOutDate ? ['--check-in-date', q.checkInDate, '--check-out-date', q.checkOutDate] : []),
|
|
694
|
+
...(q.hotelStars ? ['--hotel-stars', q.hotelStars] : []),
|
|
695
|
+
...(q.hotelBedTypes?.trim() ? ['--hotel-bed-types', q.hotelBedTypes.trim()] : []),
|
|
696
|
+
...(q.maxPrice !== undefined ? ['--max-price', String(q.maxPrice)] : []),
|
|
697
|
+
...(q.kind === 'marriott-hotel'
|
|
698
|
+
? [
|
|
699
|
+
...(q.hotelBrands?.trim() ? ['--hotel-brands', q.hotelBrands.trim()] : []),
|
|
700
|
+
...(q.hotelName?.trim() ? ['--hotel-name', q.hotelName.trim()] : []),
|
|
701
|
+
]
|
|
702
|
+
: []),
|
|
703
|
+
],
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
case 'poi': {
|
|
707
|
+
if (!(q.cityName ?? '').trim()) return { error: 'cityName(景点所在城市)required' }
|
|
708
|
+
if (q.poiLevel !== undefined && !(Number.isInteger(q.poiLevel) && q.poiLevel >= 1 && q.poiLevel <= 5)) {
|
|
709
|
+
return { error: 'poiLevel 为 1–5 整数' }
|
|
710
|
+
}
|
|
711
|
+
if (q.category && !FLYAI_POI_CATEGORIES.includes(q.category as typeof FLYAI_POI_CATEGORIES[number])) {
|
|
712
|
+
return { error: `category 不在官方闭集(共 ${FLYAI_POI_CATEGORIES.length} 类)` }
|
|
713
|
+
}
|
|
714
|
+
return {
|
|
715
|
+
args: [
|
|
716
|
+
'search-poi', '--city-name', q.cityName!.trim(),
|
|
717
|
+
...(q.poiLevel !== undefined ? ['--poi-level', String(q.poiLevel)] : []),
|
|
718
|
+
...(q.keyword?.trim() ? ['--keyword', q.keyword.trim()] : []),
|
|
719
|
+
...(q.category ? ['--category', q.category] : []),
|
|
720
|
+
],
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
case 'keyword':
|
|
724
|
+
if (!(q.query ?? '').trim()) return { error: 'query(关键词)required' }
|
|
725
|
+
return { args: ['keyword-search', '--query', q.query!.trim()] }
|
|
726
|
+
case 'ai':
|
|
727
|
+
if (!(q.query ?? '').trim()) return { error: 'query(完整自然语言)required' }
|
|
728
|
+
return { args: ['ai-search', '--query', q.query!.trim()] }
|
|
729
|
+
case 'marriott-package': {
|
|
730
|
+
if (![q.keyword, q.hotelName, q.provinceOrCity].some(value => value?.trim())) return { error: 'keyword/hotelName/provinceOrCity 至少填写一项' }
|
|
731
|
+
if (q.sortType && !MARRIOTT_PACKAGE_SORTS.includes(q.sortType)) {
|
|
732
|
+
return { error: `sortType 仅接受 ${MARRIOTT_PACKAGE_SORTS.join('/')}` }
|
|
733
|
+
}
|
|
734
|
+
return {
|
|
735
|
+
args: [
|
|
736
|
+
'search-marriott-package',
|
|
737
|
+
...(q.keyword?.trim() ? ['--keyword', q.keyword.trim()] : []),
|
|
738
|
+
...(q.hotelName?.trim() ? ['--hotel-name', q.hotelName.trim()] : []),
|
|
739
|
+
...(q.provinceOrCity?.trim() ? ['--province-or-city', q.provinceOrCity.trim()] : []),
|
|
740
|
+
...(q.sortType ? ['--sort-type', q.sortType] : []),
|
|
741
|
+
],
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const KIND_LABEL: Record<FlyaiKind, string> = {
|
|
748
|
+
flight: 'flight', train: 'train', hotel: 'hotel', poi: 'poi',
|
|
749
|
+
keyword: 'keyword', ai: 'ai', 'marriott-hotel': 'marriott hotel', 'marriott-package': 'marriott package',
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* FlyAI 官方只读检索(8 命令)— 任何失败走降级;不抛错。
|
|
754
|
+
*/
|
|
755
|
+
export async function flyaiSearch(q: FlyaiQuery): Promise<FlyaiResult> {
|
|
756
|
+
const started = Date.now()
|
|
757
|
+
const ts = new Date().toISOString()
|
|
758
|
+
const envContext = await runtimeEnvNote()
|
|
759
|
+
const envNote = envContext.note
|
|
760
|
+
const sensitive = envContext.sensitive
|
|
761
|
+
const safe = <T extends FlyaiResult>(result: T): T => safeFlyaiResult(result, sensitive)
|
|
762
|
+
const built = buildCliArgs(q)
|
|
763
|
+
if (built.error) return { ...badArg(q.kind, built.error, ts), ...envNote }
|
|
764
|
+
|
|
765
|
+
const prefix = q.cliPrefixArgs ?? ['-y', '@fly-ai/flyai-cli@1.0.16']
|
|
766
|
+
const r = await sh(q.cliBin ?? 'npx', [...prefix, ...built.args!], {
|
|
767
|
+
timeoutMs: q.timeoutMs ?? 30_000,
|
|
768
|
+
drainMs: q.drainMs,
|
|
769
|
+
signal: q.signal,
|
|
770
|
+
})
|
|
771
|
+
const latencyMs = Date.now() - started
|
|
772
|
+
const combined = `${r.stderr}\n${r.stdout}`
|
|
773
|
+
|
|
774
|
+
if (r.cancelled) {
|
|
775
|
+
return safe({
|
|
776
|
+
kind: q.kind, ...envNote, latencyMs, ok: false, via: 'flyai-error', verdict: 'cancelled',
|
|
777
|
+
retryable: false, localTermination: 'signal',
|
|
778
|
+
evidence: `[实时API:flyai@error@${ts}] cancelled by caller`,
|
|
779
|
+
error: q.signal?.aborted ? '调用前已取消(未发起检索)' : '检索在途被取消(进程组已终止)',
|
|
780
|
+
})
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
if (r.timedOut) {
|
|
784
|
+
return safe({
|
|
785
|
+
kind: q.kind, ...envNote, latencyMs, ok: false, via: 'flyai-error', verdict: 'timeout',
|
|
786
|
+
retryable: false, localTermination: 'deadline',
|
|
787
|
+
evidence: `[实时API:flyai@error@${ts}] local deadline after ${q.timeoutMs ?? 30_000}ms`,
|
|
788
|
+
error: `FlyAI 本地终止(达到 deadline ${q.timeoutMs ?? 30_000}ms${r.drainTimedOut ? ';stderr 后代 drain 超时' : ''})`,
|
|
789
|
+
setup: '本机 deadline 已终止本次检索;请调整 timeoutMs 或稍后由上层重新发起。',
|
|
790
|
+
})
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// A signal/empty exit/spawn failure carries no trustworthy upstream
|
|
794
|
+
// response. Handle it before HTTP text classification so stale partial
|
|
795
|
+
// stdout (for example a 429 printed before SIGTERM) cannot trigger retry.
|
|
796
|
+
if (r.localTermination) {
|
|
797
|
+
return safe(localTerminationResult(q.kind, r.localTermination, r, latencyMs, ts, envNote))
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
if (r.error || r.code !== 0 || /HTTP\s*(4\d\d|5\d\d)/.test(combined)) {
|
|
801
|
+
return safe(classifyUpstreamFailure(q.kind, r, latencyMs, ts, envNote))
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// ai-search:data 是自由形状,不走 itemList。
|
|
805
|
+
if (q.kind === 'ai') {
|
|
806
|
+
let envelope: Record<string, unknown>
|
|
807
|
+
try {
|
|
808
|
+
envelope = parseFlyaiJsonEnvelope(r.stdout)
|
|
809
|
+
} catch (e) {
|
|
810
|
+
const raw = r.stdout.replace(/\s+/g, ' ').slice(0, 160)
|
|
811
|
+
return safe({
|
|
812
|
+
kind: q.kind, ...envNote, latencyMs, ok: false, via: 'flyai-error', verdict: 'error',
|
|
813
|
+
evidence: `[实时API:flyai@error@${ts}] parse failed: ${raw}`,
|
|
814
|
+
error: `failed to parse flyai ai output (${(e as Error).message}): ${raw}`,
|
|
815
|
+
})
|
|
816
|
+
}
|
|
817
|
+
const systemMessage = nonEmptyText(envelope.systemMessage)
|
|
818
|
+
if (envelope.status !== undefined && envelope.status !== 0) {
|
|
819
|
+
return safe({
|
|
820
|
+
kind: q.kind, ...envNote, latencyMs, ok: false, via: 'flyai-error', verdict: 'error',
|
|
821
|
+
evidence: `[实时API:flyai@error@${ts}] ai status=${String(envelope.status)}`,
|
|
822
|
+
error: nonEmptyText(envelope.message) ?? 'ai-search upstream failure',
|
|
823
|
+
...(systemMessage ? { systemMessage } : {}),
|
|
824
|
+
})
|
|
825
|
+
}
|
|
826
|
+
const aiData = envelope.data
|
|
827
|
+
const empty = aiData === undefined || aiData === null || aiData === ''
|
|
828
|
+
|| (Array.isArray(aiData) && aiData.length === 0)
|
|
829
|
+
return safe(JSON.parse(JSON.stringify({
|
|
830
|
+
kind: q.kind, ...envNote, latencyMs, ok: true, via: 'flyai',
|
|
831
|
+
verdict: empty ? 'miss' : 'hit',
|
|
832
|
+
...(empty ? {} : { aiData }),
|
|
833
|
+
...(systemMessage ? { systemMessage } : {}),
|
|
834
|
+
evidence: `[实时API:flyai@${ts}] ai-search ${empty ? 'empty' : 'data returned'}`,
|
|
835
|
+
})) as FlyaiResult)
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
let items: unknown[]
|
|
839
|
+
try {
|
|
840
|
+
items = parseFlyaiItemList(r.stdout) as RawItem[]
|
|
841
|
+
} catch (e) {
|
|
842
|
+
const raw = r.stdout.replace(/\s+/g, ' ').slice(0, 160)
|
|
843
|
+
// Sentinel 限流:exit 0,stdout 为 {"message":"SentinelBlockException…"}
|
|
844
|
+
if (/Sentinel/i.test(raw)) {
|
|
845
|
+
return safe({
|
|
846
|
+
kind: q.kind, ...envNote, latencyMs, ok: false, via: 'flyai-error', verdict: 'error',
|
|
847
|
+
retryable: false,
|
|
848
|
+
evidence: `[实时API:flyai@error@${ts}] sentinel: ${raw}`,
|
|
849
|
+
error: `FlyAI Sentinel 限流:${raw}`,
|
|
850
|
+
setup: '平台 Sentinel 限流(恢复窗口未公开)——稍后重试,或改走 gotry_session_search(账号会话)。',
|
|
851
|
+
})
|
|
852
|
+
}
|
|
853
|
+
const reason = (e as Error).message
|
|
854
|
+
return safe({
|
|
855
|
+
kind: q.kind, ...envNote, latencyMs, ok: false, via: 'flyai-error', verdict: 'error',
|
|
856
|
+
retryable: false,
|
|
857
|
+
evidence: `[实时API:flyai@error@${ts}] parse failed(${reason}): ${raw}`,
|
|
858
|
+
error: `failed to parse flyai output (${reason}): ${raw}`,
|
|
859
|
+
})
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
return safe(parseItemListResult(q.kind, items, latencyMs, ts, envNote, r.stdout))
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/** 当前进程 key 来源/endpoint 覆盖(仅展示,与 spawn 实际生效同源) */
|
|
866
|
+
async function runtimeEnvNote(): Promise<FlyaiEnvContext> {
|
|
867
|
+
try {
|
|
868
|
+
const key = resolveFlyaiKey()
|
|
869
|
+
const endpoint = resolveFlyaiEndpoint()
|
|
870
|
+
return {
|
|
871
|
+
note: { keySource: key.source, ...(endpoint.debug ? { endpointDebug: true } : {}) },
|
|
872
|
+
sensitive: { ...(key.key ? { key: key.key } : {}), ...(endpoint.debug ? { debugEndpoint: endpoint.url } : {}) },
|
|
873
|
+
}
|
|
874
|
+
} catch {
|
|
875
|
+
return { note: { keySource: 'none' }, sensitive: {} }
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/** 非 0 退出 / HTTP 4xx-5xx 文本 → 故障闭集分类 */
|
|
880
|
+
function classifyUpstreamFailure(
|
|
881
|
+
kind: FlyaiKind,
|
|
882
|
+
r: ShResult,
|
|
883
|
+
latencyMs: number,
|
|
884
|
+
ts: string,
|
|
885
|
+
envNote: { keySource: FlyaiKeySource; endpointDebug?: boolean },
|
|
886
|
+
): FlyaiResult {
|
|
887
|
+
const upstream = `${r.stderr}\n${r.stdout}`.replace(/\s+/g, ' ').trim()
|
|
888
|
+
const tag = (verdict: FlyaiVerdict, evidence: string, extra: Partial<FlyaiResult>): FlyaiResult =>
|
|
889
|
+
({ kind, ...envNote, latencyMs, ok: false, via: 'flyai-error', verdict, evidence, ...extra })
|
|
890
|
+
|
|
891
|
+
if (/Invalid API key|HTTP\s*401|\b401\b/.test(upstream)) {
|
|
892
|
+
return tag('auth-error', `[实时API:flyai@error@${ts}] 401 invalid api key`, {
|
|
893
|
+
retryable: false,
|
|
894
|
+
error: `FlyAI 鉴权失败(401):${upstream.slice(0, 160)}`,
|
|
895
|
+
setup: '当前 key 无效或已吊销——在本机终端运行 `gotry setup flyai` 重新设置并验证(模型无法替你输入 key);`gotry setup flyai --clear` 可清除错误 key,清除后仍可匿名试用。',
|
|
896
|
+
})
|
|
897
|
+
}
|
|
898
|
+
if (/HTTP\s*403|\b403\b/.test(upstream)) {
|
|
899
|
+
return tag('forbidden', `[实时API:flyai@error@${ts}] 403 forbidden`, {
|
|
900
|
+
retryable: false,
|
|
901
|
+
error: `FlyAI 拒绝访问(403):${upstream.slice(0, 160)}`,
|
|
902
|
+
setup: '上游 403:核对该 key 的权限范围与控制台状态;如 endpoint 被 DEBUG_FLYAI_MCP_URL 覆盖,请确认指向。',
|
|
903
|
+
})
|
|
904
|
+
}
|
|
905
|
+
if (/Trial limit reached/.test(upstream)) {
|
|
906
|
+
return tag('needs-setup', `[实时API:flyai@error@${ts}] trial quota exhausted`, {
|
|
907
|
+
retryable: false,
|
|
908
|
+
error: `FlyAI 匿名试用额度已用尽:${upstream.slice(0, 160)}`,
|
|
909
|
+
setup: '打开 https://flyai.open.fliggy.com/console,登录后复制 API Key,本机运行 `gotry setup flyai` 配置并验证;本会话请勿盲重试 gotry_flyai_search,机/火/酒改走 gotry_session_search。',
|
|
910
|
+
})
|
|
911
|
+
}
|
|
912
|
+
if (/HTTP\s*429|\b429\b/.test(upstream)) {
|
|
913
|
+
return tag('rate-limited', `[实时API:flyai@error@${ts}] 429 rate limited`, {
|
|
914
|
+
retryable: true,
|
|
915
|
+
error: `FlyAI 普通限流(非试用达限):${upstream.slice(0, 160)}`,
|
|
916
|
+
setup: '上游普通限流:稍候再查,勿高频连发;仍 429 请稍后或换 gotry_session_search。',
|
|
917
|
+
})
|
|
918
|
+
}
|
|
919
|
+
if (/timeout|timed out|ETIMEDOUT|ECONNRESET|ECONNREFUSED|ECONNABORTED|EHOSTUNREACH|ENETUNREACH|EAI_AGAIN|socket|network|fetch failed|HTTP\s*5\d\d|\b5\d\d\b/i.test(upstream)) {
|
|
920
|
+
return tag('error', `[实时API:flyai@error@${ts}] transient upstream`, {
|
|
921
|
+
retryable: true,
|
|
922
|
+
error: `FlyAI 瞬时网络/上游错误:${upstream.slice(0, 160)}`,
|
|
923
|
+
setup: '网络抖动或上游 5xx:稍后重试;持续失败请检查本机网络或改走 gotry_session_search。',
|
|
924
|
+
})
|
|
925
|
+
}
|
|
926
|
+
return tag('error', `[实时API:flyai@error@${ts}] ${r.error ?? `exit ${r.code}`}`, {
|
|
927
|
+
retryable: false,
|
|
928
|
+
error: r.error ?? upstream.slice(0, 200),
|
|
929
|
+
})
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/** itemList(非 ai 类)→ 对应 typed 结果;空=miss;含 malformed=整体 error */
|
|
933
|
+
function parseItemListResult(
|
|
934
|
+
kind: FlyaiKind,
|
|
935
|
+
items: unknown[],
|
|
936
|
+
latencyMs: number,
|
|
937
|
+
ts: string,
|
|
938
|
+
envNote: { keySource: FlyaiKeySource; endpointDebug?: boolean },
|
|
939
|
+
rawStdout: string,
|
|
940
|
+
): FlyaiResult {
|
|
941
|
+
let parsed: ParsedItems<unknown>
|
|
942
|
+
switch (kind) {
|
|
943
|
+
case 'flight':
|
|
944
|
+
case 'train':
|
|
945
|
+
parsed = parseTransportItems(items)
|
|
946
|
+
break
|
|
947
|
+
case 'hotel':
|
|
948
|
+
parsed = parseHotelItems(items)
|
|
949
|
+
break
|
|
950
|
+
case 'marriott-hotel':
|
|
951
|
+
parsed = parseHotelItems(items)
|
|
952
|
+
break
|
|
953
|
+
case 'poi':
|
|
954
|
+
parsed = parsePoiItems(items)
|
|
955
|
+
break
|
|
956
|
+
case 'keyword':
|
|
957
|
+
parsed = parseKeywordItems(items)
|
|
958
|
+
break
|
|
959
|
+
case 'marriott-package':
|
|
960
|
+
parsed = parsePackageItems(items)
|
|
961
|
+
break
|
|
962
|
+
case 'ai':
|
|
963
|
+
parsed = { options: [], malformedCount: 0 }
|
|
964
|
+
break
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
const systemMessage = (() => {
|
|
968
|
+
try { return nonEmptyText((JSON.parse(rawStdout) as { systemMessage?: unknown }).systemMessage) } catch { return undefined }
|
|
969
|
+
})()
|
|
970
|
+
|
|
971
|
+
if (parsed.malformedCount > 0) {
|
|
972
|
+
const label = KIND_LABEL[kind]
|
|
973
|
+
return {
|
|
974
|
+
kind, ...envNote, latencyMs, ok: false, via: 'flyai-error', verdict: 'error',
|
|
975
|
+
retryable: false,
|
|
976
|
+
evidence: `[实时API:flyai@error@${ts}] ${label} itemList malformed (${parsed.malformedCount}/${items.length} items failed typed validation)`,
|
|
977
|
+
error: `FlyAI ${label} itemList malformed: ${parsed.malformedCount}/${items.length} item(s) failed typed validation`,
|
|
978
|
+
...(systemMessage ? { systemMessage } : {}),
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
const count = parsed.options.length
|
|
983
|
+
const verdict: FlyaiVerdict = count > 0 ? 'hit' : 'miss'
|
|
984
|
+
const evidence = `[实时API:flyai@${ts}] ${count}/${items.length} ${KIND_LABEL[kind]} options`
|
|
985
|
+
const common = { kind, ...envNote, latencyMs, ok: true, via: 'flyai' as const, verdict, evidence, ...(systemMessage ? { systemMessage } : {}) }
|
|
986
|
+
|
|
987
|
+
switch (kind) {
|
|
988
|
+
case 'flight':
|
|
989
|
+
case 'train':
|
|
990
|
+
return { ...common, options: parsed.options as FlyaiOption[] }
|
|
991
|
+
case 'hotel':
|
|
992
|
+
case 'marriott-hotel':
|
|
993
|
+
return { ...common, hotels: parsed.options as FlyaiHotelOption[] }
|
|
994
|
+
case 'poi':
|
|
995
|
+
return { ...common, pois: parsed.options as FlyaiPoiOption[] }
|
|
996
|
+
case 'keyword':
|
|
997
|
+
return { ...common, keywords: parsed.options as FlyaiKeywordOption[] }
|
|
998
|
+
case 'marriott-package':
|
|
999
|
+
return { ...common, packages: parsed.options as FlyaiMarriottPackageOption[] }
|
|
1000
|
+
case 'ai':
|
|
1001
|
+
return { ...common }
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// ── typed parsers ───────────────────────────────────────────────────────────────
|
|
1006
|
+
|
|
1007
|
+
/** 机/火条目 → FlyaiOption;非空列表含任一不完整条目即整体 malformed */
|
|
1008
|
+
function parseTransportItems(items: unknown[]): ParsedItems<FlyaiOption> {
|
|
1009
|
+
const options: FlyaiOption[] = []
|
|
1010
|
+
let malformedCount = 0
|
|
1011
|
+
for (const raw of items) {
|
|
1012
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
1013
|
+
malformedCount += 1
|
|
1014
|
+
continue
|
|
1015
|
+
}
|
|
1016
|
+
const it = raw as RawItem
|
|
1017
|
+
const journey = Array.isArray(it.journeys) ? it.journeys[0] : undefined
|
|
1018
|
+
const segments = journey && typeof journey === 'object' && !Array.isArray(journey) && Array.isArray(journey.segments)
|
|
1019
|
+
? journey.segments
|
|
1020
|
+
: undefined
|
|
1021
|
+
if (!segments || segments.length === 0) {
|
|
1022
|
+
malformedCount += 1
|
|
1023
|
+
continue
|
|
1024
|
+
}
|
|
1025
|
+
const parsedSegments: FlyaiTransportSegment[] = []
|
|
1026
|
+
for (const rawSegment of segments) {
|
|
1027
|
+
if (!rawSegment || typeof rawSegment !== 'object' || Array.isArray(rawSegment)) continue
|
|
1028
|
+
const segment = rawSegment as NonNullable<NonNullable<RawItem['journeys']>[number]['segments']>[number]
|
|
1029
|
+
const no = nonEmptyText(segment.marketingTransportNo)
|
|
1030
|
+
const name = optionalText(segment.marketingTransportName)
|
|
1031
|
+
const depDateTime = nonEmptyText(segment.depDateTime)
|
|
1032
|
+
const arrDateTime = nonEmptyText(segment.arrDateTime)
|
|
1033
|
+
const depStation = nonEmptyText(segment.depStationName)
|
|
1034
|
+
const arrStation = nonEmptyText(segment.arrStationName)
|
|
1035
|
+
const durationMin = positiveFiniteDuration(segment.duration)
|
|
1036
|
+
const seatClass = optionalText(segment.seatClassName)
|
|
1037
|
+
if (!no || !depDateTime || !arrDateTime || !depStation || !arrStation || durationMin === undefined
|
|
1038
|
+
|| (segment.marketingTransportName !== undefined && name === undefined)
|
|
1039
|
+
|| (segment.seatClassName !== undefined && seatClass === undefined)) continue
|
|
1040
|
+
parsedSegments.push({ no, name: name ?? '', depDateTime, arrDateTime, depStation, arrStation, durationMin, seatClass: seatClass ?? undefined })
|
|
1041
|
+
}
|
|
1042
|
+
if (parsedSegments.length !== segments.length) {
|
|
1043
|
+
malformedCount += 1
|
|
1044
|
+
continue
|
|
1045
|
+
}
|
|
1046
|
+
const first = parsedSegments[0]!
|
|
1047
|
+
const last = parsedSegments[parsedSegments.length - 1]!
|
|
1048
|
+
const journeyType = optionalText(journey?.journeyType)
|
|
1049
|
+
const nonstop = parsedSegments.length === 1 && !/^(?:2|中转|transfer)/i.test(journeyType ?? '')
|
|
1050
|
+
const totalDuration = positiveFiniteDuration(journey?.totalDuration)
|
|
1051
|
+
?? parsedSegments.reduce((sum, segment) => sum + segment.durationMin, 0)
|
|
1052
|
+
const rawPriceValue = it.adultPrice !== undefined
|
|
1053
|
+
? it.adultPrice
|
|
1054
|
+
: it.ticketPrice !== undefined ? it.ticketPrice : it.price
|
|
1055
|
+
const parsedPrice = parseTransportPrice(rawPriceValue)
|
|
1056
|
+
const jumpUrl = optionalText(it.jumpUrl)
|
|
1057
|
+
if (!parsedPrice
|
|
1058
|
+
|| (it.jumpUrl !== undefined && jumpUrl === undefined)) {
|
|
1059
|
+
malformedCount += 1
|
|
1060
|
+
continue
|
|
1061
|
+
}
|
|
1062
|
+
options.push({
|
|
1063
|
+
no: first.no, name: first.name, depDateTime: first.depDateTime, arrDateTime: last.arrDateTime,
|
|
1064
|
+
depStation: first.depStation, arrStation: last.arrStation, durationMin: totalDuration,
|
|
1065
|
+
price: parsedPrice.price, priceRaw: parsedPrice.priceRaw,
|
|
1066
|
+
seatClass: first.seatClass, jumpUrl: jumpUrl ?? undefined, nonstop, journeyType: journeyType ?? undefined,
|
|
1067
|
+
segments: parsedSegments,
|
|
1068
|
+
})
|
|
1069
|
+
}
|
|
1070
|
+
return { options, malformedCount }
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
function stringField(parent: RawItem, key: keyof RawItem): string | undefined | null | 'bad' {
|
|
1074
|
+
const value = parent[key]
|
|
1075
|
+
if (value === undefined) return undefined
|
|
1076
|
+
if (value === null) return null
|
|
1077
|
+
return nonEmptyText(value) ?? 'bad'
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/** 酒店/万豪酒店条目 → FlyaiHotelOption(官方两形态字段集同构) */
|
|
1081
|
+
function parseHotelItems(items: unknown[]): ParsedItems<FlyaiHotelOption> {
|
|
1082
|
+
const options: FlyaiHotelOption[] = []
|
|
1083
|
+
let malformedCount = 0
|
|
1084
|
+
for (const raw of items) {
|
|
1085
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
1086
|
+
malformedCount += 1
|
|
1087
|
+
continue
|
|
1088
|
+
}
|
|
1089
|
+
const it = raw as RawItem
|
|
1090
|
+
const name = nonEmptyText(it.name)
|
|
1091
|
+
const star = stringField(it, 'star')
|
|
1092
|
+
const rawPrice = stringField(it, 'price')
|
|
1093
|
+
const rate = it.rate === undefined ? undefined : it.rate === null ? null : nonEmptyText(it.rate) ?? 'bad'
|
|
1094
|
+
const address = stringField(it, 'address')
|
|
1095
|
+
const poiSource = it.interestsPoi !== undefined ? it.interestsPoi : it.nearbyPoi
|
|
1096
|
+
const poi = poiSource === undefined ? undefined : poiSource === null ? null : nonEmptyText(poiSource) ?? 'bad'
|
|
1097
|
+
const hotelIdSource = it.shId !== undefined ? it.shId : it.shid
|
|
1098
|
+
const hotelId = hotelIdSource === undefined ? undefined : nonEmptyText(hotelIdSource) ?? (typeof hotelIdSource === 'number' ? String(hotelIdSource) : 'bad')
|
|
1099
|
+
const jumpUrl = stringField(it, 'detailUrl')
|
|
1100
|
+
const mainPic = stringField(it, 'mainPic')
|
|
1101
|
+
const score = it.score === undefined ? undefined : it.score === null ? null : nonEmptyText(it.score) ?? 'bad'
|
|
1102
|
+
const scoreDesc = stringField(it, 'scoreDesc')
|
|
1103
|
+
const review = stringField(it, 'review')
|
|
1104
|
+
const brandName = stringField(it, 'brandName')
|
|
1105
|
+
const latitude = stringField(it, 'latitude')
|
|
1106
|
+
const longitude = stringField(it, 'longitude')
|
|
1107
|
+
|
|
1108
|
+
const fields = [star, rawPrice, rate, address, poi, hotelId, jumpUrl, mainPic, score, scoreDesc, review, brandName, latitude, longitude]
|
|
1109
|
+
if (!name || fields.includes('bad')) {
|
|
1110
|
+
malformedCount += 1
|
|
1111
|
+
continue
|
|
1112
|
+
}
|
|
1113
|
+
const priceText = (rawPrice ?? '') as string
|
|
1114
|
+
const bare = priceText.replace(/^¥/, '')
|
|
1115
|
+
const numericPrice = Number(bare)
|
|
1116
|
+
options.push({
|
|
1117
|
+
name,
|
|
1118
|
+
star: star ?? undefined,
|
|
1119
|
+
price: /^\d+(\.\d+)?$/.test(bare) && numericPrice > 0 ? numericPrice : 0,
|
|
1120
|
+
priceRaw: priceText || undefined,
|
|
1121
|
+
rate: rate ?? undefined,
|
|
1122
|
+
address: address ?? undefined,
|
|
1123
|
+
poi: poi ?? undefined,
|
|
1124
|
+
hotelId: hotelId ?? undefined,
|
|
1125
|
+
jumpUrl: jumpUrl ?? undefined,
|
|
1126
|
+
mainPic: mainPic ?? undefined,
|
|
1127
|
+
score: score ?? undefined,
|
|
1128
|
+
scoreDesc: scoreDesc ?? undefined,
|
|
1129
|
+
review: review ?? undefined,
|
|
1130
|
+
brandName: brandName ?? undefined,
|
|
1131
|
+
latitude: latitude ?? undefined,
|
|
1132
|
+
longitude: longitude ?? undefined,
|
|
1133
|
+
})
|
|
1134
|
+
}
|
|
1135
|
+
return { options, malformedCount }
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
/** 景点条目 → FlyaiPoiOption */
|
|
1139
|
+
function parsePoiItems(items: unknown[]): ParsedItems<FlyaiPoiOption> {
|
|
1140
|
+
const options: FlyaiPoiOption[] = []
|
|
1141
|
+
let malformedCount = 0
|
|
1142
|
+
for (const raw of items) {
|
|
1143
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
1144
|
+
malformedCount += 1
|
|
1145
|
+
continue
|
|
1146
|
+
}
|
|
1147
|
+
const it = raw as RawItem
|
|
1148
|
+
const name = nonEmptyText(it.name)
|
|
1149
|
+
const poiId = it.id === undefined ? undefined : nonEmptyText(it.id) ?? (typeof it.id === 'number' ? String(it.id) : 'bad')
|
|
1150
|
+
const mainPic = stringField(it, 'mainPic')
|
|
1151
|
+
const jumpUrl = stringField(it, 'jumpUrl')
|
|
1152
|
+
const address = stringField(it, 'address')
|
|
1153
|
+
const freePoiStatus = stringField(it, 'freePoiStatus')
|
|
1154
|
+
let ticketInfo: FlyaiPoiTicketInfo | undefined | 'bad'
|
|
1155
|
+
if (it.ticketInfo !== undefined && it.ticketInfo !== null) {
|
|
1156
|
+
if (typeof it.ticketInfo !== 'object' || Array.isArray(it.ticketInfo)) {
|
|
1157
|
+
ticketInfo = 'bad'
|
|
1158
|
+
} else {
|
|
1159
|
+
const t = it.ticketInfo as Record<string, unknown>
|
|
1160
|
+
const price = t.price === undefined ? undefined : t.price === null ? null : nonEmptyText(t.price) ?? 'bad'
|
|
1161
|
+
const priceDate = t.priceDate === undefined || t.priceDate === null ? undefined : nonEmptyText(t.priceDate) ?? 'bad'
|
|
1162
|
+
const ticketName = t.ticketName === undefined ? undefined : nonEmptyText(t.ticketName) ?? 'bad'
|
|
1163
|
+
if ([price, priceDate, ticketName].includes('bad')) ticketInfo = 'bad'
|
|
1164
|
+
else ticketInfo = { price: price ?? undefined, priceDate, ticketName }
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
if (!name || poiId === 'bad' || [mainPic, jumpUrl, address, freePoiStatus].includes('bad') || ticketInfo === 'bad') {
|
|
1169
|
+
malformedCount += 1
|
|
1170
|
+
continue
|
|
1171
|
+
}
|
|
1172
|
+
options.push({
|
|
1173
|
+
name,
|
|
1174
|
+
poiId: poiId ?? undefined,
|
|
1175
|
+
mainPic: mainPic ?? undefined,
|
|
1176
|
+
jumpUrl: jumpUrl ?? undefined,
|
|
1177
|
+
address: address ?? undefined,
|
|
1178
|
+
freePoiStatus: freePoiStatus ?? undefined,
|
|
1179
|
+
...(ticketInfo ? { ticketInfo } : {}),
|
|
1180
|
+
})
|
|
1181
|
+
}
|
|
1182
|
+
return { options, malformedCount }
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
/** 关键词条目 → FlyaiKeywordOption(官方形状 itemList[].info) */
|
|
1186
|
+
function parseKeywordItems(items: unknown[]): ParsedItems<FlyaiKeywordOption> {
|
|
1187
|
+
const options: FlyaiKeywordOption[] = []
|
|
1188
|
+
let malformedCount = 0
|
|
1189
|
+
for (const raw of items) {
|
|
1190
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
1191
|
+
malformedCount += 1
|
|
1192
|
+
continue
|
|
1193
|
+
}
|
|
1194
|
+
const info = (raw as { info?: unknown }).info
|
|
1195
|
+
if (!info || typeof info !== 'object' || Array.isArray(info)) {
|
|
1196
|
+
malformedCount += 1
|
|
1197
|
+
continue
|
|
1198
|
+
}
|
|
1199
|
+
const it = info as Record<string, unknown>
|
|
1200
|
+
const title = nonEmptyText(it.title)
|
|
1201
|
+
const str = (key: string): string | undefined | 'bad' => {
|
|
1202
|
+
const v = it[key]
|
|
1203
|
+
if (v === undefined || v === null) return undefined
|
|
1204
|
+
return nonEmptyText(v) ?? 'bad'
|
|
1205
|
+
}
|
|
1206
|
+
const jumpUrl = str('jumpUrl')
|
|
1207
|
+
const picUrl = str('picUrl')
|
|
1208
|
+
const price = str('price')
|
|
1209
|
+
const scoreDesc = str('scoreDesc')
|
|
1210
|
+
const star = str('star')
|
|
1211
|
+
let tags: string[] | undefined | 'bad'
|
|
1212
|
+
if (it.tags !== undefined && it.tags !== null) {
|
|
1213
|
+
if (!Array.isArray(it.tags) || it.tags.some(t => nonEmptyText(t) === undefined)) tags = 'bad'
|
|
1214
|
+
else tags = it.tags.map(t => String(t).trim())
|
|
1215
|
+
}
|
|
1216
|
+
if (!title || [jumpUrl, picUrl, price, scoreDesc, star].includes('bad') || tags === 'bad') {
|
|
1217
|
+
malformedCount += 1
|
|
1218
|
+
continue
|
|
1219
|
+
}
|
|
1220
|
+
options.push({
|
|
1221
|
+
title,
|
|
1222
|
+
jumpUrl: jumpUrl ?? undefined,
|
|
1223
|
+
picUrl: picUrl ?? undefined,
|
|
1224
|
+
price: price ?? undefined,
|
|
1225
|
+
scoreDesc: scoreDesc ?? undefined,
|
|
1226
|
+
star: star ?? undefined,
|
|
1227
|
+
...(tags ? { tags } : {}),
|
|
1228
|
+
})
|
|
1229
|
+
}
|
|
1230
|
+
return { options, malformedCount }
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
/** 万豪套餐条目 → FlyaiMarriottPackageOption */
|
|
1234
|
+
function parsePackageItems(items: unknown[]): ParsedItems<FlyaiMarriottPackageOption> {
|
|
1235
|
+
const options: FlyaiMarriottPackageOption[] = []
|
|
1236
|
+
let malformedCount = 0
|
|
1237
|
+
for (const raw of items) {
|
|
1238
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
1239
|
+
malformedCount += 1
|
|
1240
|
+
continue
|
|
1241
|
+
}
|
|
1242
|
+
const it = raw as RawItem
|
|
1243
|
+
const title = stringField(it, 'title')
|
|
1244
|
+
const name = title !== undefined ? title : stringField(it, 'name')
|
|
1245
|
+
const itemId = stringField(it, 'itemId')
|
|
1246
|
+
const mainPicSource = it.picUrl !== undefined ? 'picUrl' : 'mainPic'
|
|
1247
|
+
const sellingPointSource = it.sellPoint !== undefined ? 'sellPoint' : 'sellingPoint'
|
|
1248
|
+
const fields_ = [
|
|
1249
|
+
stringField(it, 'brandName'), stringField(it, 'hotelName'), stringField(it, 'cityName'),
|
|
1250
|
+
stringField(it, 'price'), stringField(it, 'detailUrl'), stringField(it, mainPicSource),
|
|
1251
|
+
stringField(it, sellingPointSource), itemId,
|
|
1252
|
+
]
|
|
1253
|
+
if (!name || fields_.includes('bad')) {
|
|
1254
|
+
malformedCount += 1
|
|
1255
|
+
continue
|
|
1256
|
+
}
|
|
1257
|
+
const [brandName, hotelName, cityName, price, detailUrl, mainPic, sellingPoint] = fields_ as Array<string | null | undefined>
|
|
1258
|
+
options.push({
|
|
1259
|
+
name: name as string,
|
|
1260
|
+
...(title && title !== 'bad' ? { title } : {}),
|
|
1261
|
+
...(itemId && itemId !== 'bad' ? { itemId } : {}),
|
|
1262
|
+
brandName: brandName ?? undefined,
|
|
1263
|
+
hotelName: hotelName ?? undefined,
|
|
1264
|
+
cityName: cityName ?? undefined,
|
|
1265
|
+
price: price ?? undefined,
|
|
1266
|
+
detailUrl: detailUrl ?? undefined,
|
|
1267
|
+
mainPic: mainPic ?? undefined,
|
|
1268
|
+
picUrl: mainPic ?? undefined,
|
|
1269
|
+
sellingPoint: sellingPoint ?? undefined,
|
|
1270
|
+
sellPoint: sellingPoint ?? undefined,
|
|
1271
|
+
})
|
|
1272
|
+
}
|
|
1273
|
+
return { options, malformedCount }
|
|
1274
|
+
}
|