@danceiny/gotry 0.0.1-rc.9 → 0.2.0-rc.27
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 +1744 -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 +135 -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 +546 -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 +509 -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/resilience.js +191 -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-tests.js +116 -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 +123 -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 +1479 -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/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 +564 -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-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 +131 -0
- package/dist/scripts/g5-guard.js +189 -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/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 +161 -0
- package/dist/scripts/nightly-evidence.js +286 -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/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 +20 -18
- package/dist/scripts/replay-real.js +18 -18
- package/dist/scripts/replay.js +52 -82
- 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-zones-tests.js +358 -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 +241 -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 +550 -199
- 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 +210 -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 +23 -19
- 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 +41 -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/companions.js +48 -74
- package/dist/src/contracts.js +21 -43
- package/dist/src/dsh-llm.js +97 -111
- 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 +2227 -882
- 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 +319 -160
- 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 +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-zones.js +507 -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 +435 -516
- 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 +535 -359
- 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 +255 -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 +568 -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 +13 -3
- package/ts/src/dsh-llm.ts +44 -11
- package/ts/src/flyai-setup-tool.ts +77 -0
- package/ts/src/index.ts +1948 -198
- 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 +223 -25
- 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 +912 -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 +354 -88
- package/ts/src/wish-pool.ts +79 -0
- package/dist/scripts/session-attach-wait.js +0 -41
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" width="36" height="36" role="img" aria-label="dsh-map-tools">
|
|
2
|
+
<!-- 小篆「径」— the traced outline carried in assets/banner.svg for the line
|
|
3
|
+
「湖山千里 · 一径相通」, cleaned to one non-overlapping path. -->
|
|
4
|
+
<path d="M13.8 0.72L13.73 0.85L13.64 3.11L13.52 7.49L12.6 10.57L10.86 12.36L9.99 13.31L9.97 13.43L9.99 13.51L10.04 13.56L10.14 13.57L10.29 13.54L11.18 12.96L12.82 11.85L13.84 11.04L14.27 10.54L14.44 8.02L14.37 3.49L14.27 1.09L14.14 0.82L14.03 0.67L13.94 0.66L13.89 0.65L13.85 0.65ZM17 5.84L16.67 5.9L16.49 6.01L16.47 6.14L16.44 6.22L16.39 6.26L16.37 6.33L16.39 6.45L16.45 6.52L16.55 6.54L16.62 6.58L16.67 6.65L16.96 6.7L17.48 6.73L18.11 7.08L18.83 7.76L19.19 8.26L19.19 8.6L18.88 9.15L18.26 9.91L17.82 10.73L17.57 11.62L17.44 12.15L17.44 12.3L17.68 12.79L18.17 13.64L18.74 14.31L19.38 14.82L19.66 15.09L19.6 15.14L19.05 15.22L18.02 15.32L17.46 15.45L17.36 15.6L17.31 15.73L17.31 15.83L17.33 15.9L17.38 15.95L17.52 15.99L17.74 16L18.58 16.04L20.04 16.09L20.83 16.16L20.93 16.26L20.98 19.23L20.98 25.06L20.27 28.01L18.86 28.07L17.91 28.21L17.44 28.41L17.2 28.57L17.18 28.69L17.2 28.77L17.25 28.82L19.45 28.85L23.82 28.85L26.02 28.78L26.07 28.65L25.89 28.46L25.47 28.23L24.38 28.14L22.63 28.21L21.69 28.19L21.55 28.09L21.54 27.64L21.66 26.83L21.69 23.9L21.62 18.84L21.64 16.26L21.74 16.16L21.84 16.1L21.94 16.09L22.85 16.03L24.57 15.93L25.52 15.84L25.7 15.78L25.78 15.68L25.75 15.54L25.12 15.11L23.91 14.39L23.04 13.72L22.52 13.11L22.23 12.34L22.18 11.4L22.53 10.42L23.27 9.41L23.57 8.49L23.44 7.67L23.25 7.13L23.02 6.9L23.24 6.76L23.91 6.73L24.27 6.69L24.32 6.64L24.34 6.55L24.32 6.44L24.36 6.38L24.44 6.38L24.5 6.44L24.53 6.55L24.61 6.62L24.73 6.64L24.84 6.56L24.96 6.39L25.02 6.27L25.02 6.18L24.93 6.08L24.74 5.96L23.09 5.88L19.98 5.83L18.39 5.83L18.34 5.88L18.34 5.97L18.39 6.11L19.55 6.21L21.83 6.28L22.83 6.32L22.56 6.33L22.38 6.39L22.28 6.49L22.16 6.49L22.02 6.39L21.53 6.34L20.67 6.34L20.17 6.39L20.03 6.49L19.92 6.49L19.82 6.39L19.4 6.34L18.68 6.34L18.29 6.37L18.24 6.42L18.15 6.44L18.01 6.44L17.9 6.41L17.82 6.34L17.68 6.3L17.5 6.28L17.45 6.22L17.53 6.12L17.58 6.03L17.58 5.95L17.55 5.88L17.5 5.83L17.42 5.8L17.3 5.8L17.22 5.8L17.19 5.8ZM19.05 6.79L19.57 6.87L20.09 7.16L20.62 7.65L20.89 8L20.93 8.22L20.85 8.54L20.67 8.96L20.23 9.62L19.54 10.51L19.19 11.3L19.19 11.99L19.37 12.66L19.72 13.3L20.33 14L21.19 14.76L21.44 15.15L21.09 15.19L20.35 14.74L19.24 13.81L18.6 12.9L18.43 11.99L18.75 10.91L19.56 9.66L19.97 8.83L19.97 8.41L19.86 7.98L19.64 7.54L19.34 7.19L18.95 6.92L18.76 6.77L18.78 6.76ZM21.06 6.79L21.7 6.87L22.25 7.13L22.7 7.55L22.95 7.89L22.98 8.16L22.92 8.47L22.75 8.8L22.31 9.43L21.6 10.34L21.25 11.18L21.25 11.95L21.49 12.73L21.98 13.5L22.61 14.2L23.39 14.82L23.56 15.15L23.12 15.19L22.8 15.15L22.6 15.05L21.96 14.52L20.88 13.56L20.28 12.61L20.16 11.67L20.47 10.66L21.21 9.6L21.55 8.68L21.48 7.91L21.26 7.35L20.87 7.02L20.68 6.83L20.7 6.8L20.72 6.77L20.73 6.76ZM13.46 13.43L11.69 15.26L10.81 16.24L10.83 16.36L10.91 16.42L11.06 16.42L11.86 15.84L13.31 14.67L14.1 14.11L14.21 14.14L14.27 14.23L14.27 14.39L14.17 16.87L13.97 21.69L13.53 25.52L12.86 28.37L12.03 31.06L11.06 33.61L10.57 34.98L10.57 35.18L10.59 35.31L10.64 35.36L10.74 35.38L10.87 35.36L11.01 35.25L11.17 35.05L11.7 33.76L12.63 31.38L13.51 28.49L14.33 25.07L14.81 21.05L14.95 16.43L14.92 13.75L14.74 13.01L14.59 12.61L14.47 12.54L14.39 12.51L14.36 12.51Z" fill="#B33325" fill-rule="nonzero"/>
|
|
5
|
+
</svg>
|
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
/** Amap (高德) Web Service API client. */
|
|
2
|
+
import { formatLngLat } from '../types.js';
|
|
3
|
+
import { decodeAmapPolyline, dedupeGeometry, compactLine } from '../geo.js';
|
|
4
|
+
const REST = 'https://restapi.amap.com/';
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// 配额保护层
|
|
7
|
+
//
|
|
8
|
+
// 高德个人开发者 key 的 QPS 上限极低(常见 3 QPS/秒,超限返回错误码 10021
|
|
9
|
+
// CUQPS_HAS_EXCEEDED_THE_LIMIT),而一次工具调用(尤其 map_transit_route)
|
|
10
|
+
// 内部可能连发 1~5 个请求;多个工具并行时瞬时请求数很容易击穿配额,导致
|
|
11
|
+
// 用户看到一串 10021 报错。这里做三层防护:
|
|
12
|
+
//
|
|
13
|
+
// 1. 限速排队:RateLimiter 把任意两次请求的最小间隔钳制在 1000/maxQps
|
|
14
|
+
// (默认 2 QPS,低于高德常见上限、留余量),并发请求排队等待而不是
|
|
15
|
+
// 同时发出;
|
|
16
|
+
// 2. 结果缓存:geocode / route / POI 按参数做 TTL 内存缓存,同一会话中
|
|
17
|
+
// 相同请求直接命中缓存,不再消耗配额(transit 的 resolveCity 会命中
|
|
18
|
+
// resolve 刚写下的 geocode 缓存,省掉重复请求);
|
|
19
|
+
// 3. 错误分类 + 重试:10020/10021(QPS 超限)标记为可重试,在客户端内
|
|
20
|
+
// 退避重试;10022/10023(日配额超限)重试无意义,直接给出友好中文
|
|
21
|
+
// 提示。上层工具仍可捕获 AmapQuotaError 做降级(见 tools/routes.ts)。
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
/** 配额类错误(QPS 或日配额超限)。retryable=true 时短期内重试可能成功
|
|
24
|
+
* (QPS 超限),false 表示重试无意义(日配额已用尽)。 */
|
|
25
|
+
export class AmapQuotaError extends Error {
|
|
26
|
+
infocode;
|
|
27
|
+
retryable;
|
|
28
|
+
constructor(infocode, message, retryable) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.infocode = infocode;
|
|
31
|
+
this.retryable = retryable;
|
|
32
|
+
this.name = 'AmapQuotaError';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Sleep that honors an AbortSignal (rejects with the abort reason). */
|
|
36
|
+
function sleep(ms, signal) {
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
let timer;
|
|
39
|
+
const cleanup = () => signal.removeEventListener('abort', onAbort);
|
|
40
|
+
const onAbort = () => {
|
|
41
|
+
if (timer)
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
cleanup();
|
|
44
|
+
reject(signal.reason instanceof Error ? signal.reason : new Error(String(signal.reason ?? 'aborted')));
|
|
45
|
+
};
|
|
46
|
+
if (signal.aborted) {
|
|
47
|
+
onAbort();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
51
|
+
timer = setTimeout(() => {
|
|
52
|
+
cleanup();
|
|
53
|
+
resolve();
|
|
54
|
+
}, ms);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 串行链 + 最小间隔限速器:所有请求排成一条 promise 链,每个请求在拿到
|
|
59
|
+
* 自己的发送窗口前等待(与前一个请求至少间隔 intervalMs)。并发调用
|
|
60
|
+
* acquire 时天然排队,不会同时发出请求。
|
|
61
|
+
*/
|
|
62
|
+
class RateLimiter {
|
|
63
|
+
intervalMs;
|
|
64
|
+
tail = Promise.resolve();
|
|
65
|
+
lastSend = 0;
|
|
66
|
+
constructor(intervalMs) {
|
|
67
|
+
this.intervalMs = intervalMs;
|
|
68
|
+
}
|
|
69
|
+
/** 排队等待自己的发送窗口;signal 中断时抛错。 */
|
|
70
|
+
acquire(signal) {
|
|
71
|
+
const run = this.tail.then(async () => {
|
|
72
|
+
const now = Date.now();
|
|
73
|
+
const wait = Math.max(0, this.lastSend + this.intervalMs - now);
|
|
74
|
+
if (wait > 0)
|
|
75
|
+
await sleep(wait, signal);
|
|
76
|
+
this.lastSend = Date.now();
|
|
77
|
+
});
|
|
78
|
+
// 链继续推进:某个请求失败(如被 abort)不阻塞后续请求。
|
|
79
|
+
this.tail = run.catch(() => undefined);
|
|
80
|
+
return run;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** 简单的 TTL + 容量上限内存缓存(插入序近似 LRU 淘汰)。 */
|
|
84
|
+
class TtlCache {
|
|
85
|
+
ttlMs;
|
|
86
|
+
maxEntries;
|
|
87
|
+
store = new Map();
|
|
88
|
+
constructor(ttlMs, maxEntries = 300) {
|
|
89
|
+
this.ttlMs = ttlMs;
|
|
90
|
+
this.maxEntries = maxEntries;
|
|
91
|
+
}
|
|
92
|
+
get(key) {
|
|
93
|
+
const entry = this.store.get(key);
|
|
94
|
+
if (!entry)
|
|
95
|
+
return undefined;
|
|
96
|
+
if (entry.expiresAt < Date.now()) {
|
|
97
|
+
this.store.delete(key);
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
// LRU touch:删了重插,保持最近使用的在末尾。
|
|
101
|
+
this.store.delete(key);
|
|
102
|
+
this.store.set(key, entry);
|
|
103
|
+
return entry.value;
|
|
104
|
+
}
|
|
105
|
+
set(key, value) {
|
|
106
|
+
if (this.store.size >= this.maxEntries) {
|
|
107
|
+
const oldest = this.store.keys().next().value;
|
|
108
|
+
if (oldest !== undefined)
|
|
109
|
+
this.store.delete(oldest);
|
|
110
|
+
}
|
|
111
|
+
this.store.set(key, { value, expiresAt: Date.now() + this.ttlMs });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** 各请求类型的缓存 TTL。 */
|
|
115
|
+
const CACHE_TTL = {
|
|
116
|
+
geo: 24 * 60 * 60 * 1000, // 地理编码结果基本不变
|
|
117
|
+
route: 60 * 60 * 1000, // 路线/路况 1 小时内可复用
|
|
118
|
+
poi: 60 * 60 * 1000,
|
|
119
|
+
};
|
|
120
|
+
/** 解析高德的 `"lng,lat"` 坐标字段(`location`、`taxi.origin` 等)。 */
|
|
121
|
+
function parseAmapLocation(text) {
|
|
122
|
+
if (typeof text !== 'string')
|
|
123
|
+
return undefined;
|
|
124
|
+
const parts = text.split(',');
|
|
125
|
+
if (parts.length !== 2)
|
|
126
|
+
return undefined;
|
|
127
|
+
const lng = Number(parts[0]);
|
|
128
|
+
const lat = Number(parts[1]);
|
|
129
|
+
if (!Number.isFinite(lng) || !Number.isFinite(lat))
|
|
130
|
+
return undefined;
|
|
131
|
+
if (lng < -180 || lng > 180 || lat < -90 || lat > 90)
|
|
132
|
+
return undefined;
|
|
133
|
+
return [lng, lat];
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* 读方案/分段的耗时(秒)。
|
|
137
|
+
* v5 放在 `cost.duration`,v3 放在 `duration`——两个都读,v5 优先。
|
|
138
|
+
*/
|
|
139
|
+
function readDuration(node) {
|
|
140
|
+
const value = Number(node?.cost?.duration ?? node?.duration ?? 0);
|
|
141
|
+
return Number.isFinite(value) ? value : 0;
|
|
142
|
+
}
|
|
143
|
+
/** 读分段距离(米):v5 是 `step_distance`,v3 是 `distance`。 */
|
|
144
|
+
function readStepDistance(step) {
|
|
145
|
+
const value = Number(step.step_distance ?? step.distance ?? 0);
|
|
146
|
+
return Number.isFinite(value) ? value : 0;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* 从公交分段里尽力拼出路线几何。
|
|
150
|
+
*
|
|
151
|
+
* 公交响应没有"整条线"的折线,只有各段的碎片:步行段有 `steps[].polyline`,
|
|
152
|
+
* 公交段有线路 `polyline`,火车/打车段只有站点坐标。这里按分段顺序拼接;
|
|
153
|
+
* 缺折线的分段用两端站点坐标连一条直线(示意图够用,且不会画错方向)。
|
|
154
|
+
* 拿不到任何可用碎片时返回空数组,由调用方回退成起终点直线。
|
|
155
|
+
*
|
|
156
|
+
* @param segments - 高德公交响应的一条方案的分段数组。
|
|
157
|
+
* @returns 拼接后的几何点(可能为空)。
|
|
158
|
+
*/
|
|
159
|
+
function transitGeometry(segments) {
|
|
160
|
+
const line = [];
|
|
161
|
+
const push = (points) => {
|
|
162
|
+
for (const point of points)
|
|
163
|
+
line.push(point);
|
|
164
|
+
};
|
|
165
|
+
const straight = (from, to) => {
|
|
166
|
+
const a = parseAmapLocation(from);
|
|
167
|
+
const b = parseAmapLocation(to);
|
|
168
|
+
if (a)
|
|
169
|
+
push([a]);
|
|
170
|
+
if (b)
|
|
171
|
+
push([b]);
|
|
172
|
+
};
|
|
173
|
+
for (const segment of segments) {
|
|
174
|
+
const walking = segment.walking;
|
|
175
|
+
if (walking) {
|
|
176
|
+
const fromSteps = (walking.steps ?? []).flatMap((step) => decodeAmapPolyline(step.polyline));
|
|
177
|
+
if (fromSteps.length >= 2)
|
|
178
|
+
push(fromSteps);
|
|
179
|
+
// v5 的步行 steps 折线实测为空,退成步行段自身的起终点直线。
|
|
180
|
+
else
|
|
181
|
+
straight(walking.origin, walking.destination);
|
|
182
|
+
}
|
|
183
|
+
const busline = segment.bus?.buslines?.[0];
|
|
184
|
+
if (busline) {
|
|
185
|
+
const decoded = decodeAmapPolyline(busline.polyline);
|
|
186
|
+
if (decoded.length > 0)
|
|
187
|
+
push(decoded);
|
|
188
|
+
else
|
|
189
|
+
straight(busline.departure_stop?.location, busline.arrival_stop?.location);
|
|
190
|
+
}
|
|
191
|
+
if (segment.railway) {
|
|
192
|
+
straight(segment.railway.departure_stop?.location, segment.railway.arrival_stop?.location);
|
|
193
|
+
}
|
|
194
|
+
if (segment.taxi)
|
|
195
|
+
straight(segment.taxi.origin, segment.taxi.destination);
|
|
196
|
+
const entrance = parseAmapLocation(segment.entrance?.location);
|
|
197
|
+
if (entrance)
|
|
198
|
+
push([entrance]);
|
|
199
|
+
const exit = parseAmapLocation(segment.exit?.location);
|
|
200
|
+
if (exit)
|
|
201
|
+
push([exit]);
|
|
202
|
+
}
|
|
203
|
+
return line;
|
|
204
|
+
}
|
|
205
|
+
export class AmapClient {
|
|
206
|
+
opts;
|
|
207
|
+
limiter;
|
|
208
|
+
geoCache = new TtlCache(CACHE_TTL.geo);
|
|
209
|
+
routeCache = new TtlCache(CACHE_TTL.route);
|
|
210
|
+
poiCache = new TtlCache(CACHE_TTL.poi);
|
|
211
|
+
/** 静态地图字节缓存:同一张图不重复计费。 */
|
|
212
|
+
mapCache = new TtlCache(CACHE_TTL.poi);
|
|
213
|
+
constructor(opts) {
|
|
214
|
+
this.opts = opts;
|
|
215
|
+
// 钳制在 [0.2, 100] QPS:避免配置了 0/负数,也避免测试设极大值时的计时噪声。
|
|
216
|
+
const qps = Math.max(0.2, Math.min(100, opts.maxQps ?? 2));
|
|
217
|
+
this.limiter = new RateLimiter(1000 / qps);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* 带配额保护的统一请求入口:限速排队 → 缓存命中 → 请求 → 可重试错误退避
|
|
221
|
+
* 重试 → 写入缓存。
|
|
222
|
+
*/
|
|
223
|
+
async request(path, params, signal, opts = {}) {
|
|
224
|
+
await this.limiter.acquire(signal);
|
|
225
|
+
if (opts.cache && opts.cacheKey) {
|
|
226
|
+
const hit = opts.cache.get(opts.cacheKey);
|
|
227
|
+
if (hit !== undefined)
|
|
228
|
+
return hit;
|
|
229
|
+
}
|
|
230
|
+
const retries = opts.retries ?? 0;
|
|
231
|
+
let attempt = 0;
|
|
232
|
+
for (;;) {
|
|
233
|
+
try {
|
|
234
|
+
const body = await this.rawGet(path, params, signal);
|
|
235
|
+
if (opts.cache && opts.cacheKey)
|
|
236
|
+
opts.cache.set(opts.cacheKey, body);
|
|
237
|
+
return body;
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
if (err instanceof AmapQuotaError && err.retryable && attempt < retries) {
|
|
241
|
+
attempt++;
|
|
242
|
+
// 简单线性退避:250ms / 500ms / 750ms …
|
|
243
|
+
await sleep(250 * attempt, signal);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
throw err;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/** 裸 GET:拼 URL、带 key、超时 + abort、错误分类。 */
|
|
251
|
+
async rawGet(path, params, signal) {
|
|
252
|
+
const url = new URL(`${REST}${path}`);
|
|
253
|
+
url.searchParams.set('key', this.opts.key);
|
|
254
|
+
for (const [k, v] of Object.entries(params)) {
|
|
255
|
+
if (v !== undefined && v !== '')
|
|
256
|
+
url.searchParams.set(k, v);
|
|
257
|
+
}
|
|
258
|
+
// Combine the caller's abort signal with a per-request timeout.
|
|
259
|
+
const controller = new AbortController();
|
|
260
|
+
const timer = setTimeout(() => controller.abort(new Error(`Amap request timed out after ${this.opts.timeoutMs}ms`)), this.opts.timeoutMs);
|
|
261
|
+
const onAbort = () => controller.abort(signal.reason);
|
|
262
|
+
if (signal.aborted)
|
|
263
|
+
controller.abort(signal.reason);
|
|
264
|
+
else
|
|
265
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
266
|
+
let res;
|
|
267
|
+
try {
|
|
268
|
+
res = await fetch(url, { signal: controller.signal, headers: { Accept: 'application/json' } });
|
|
269
|
+
}
|
|
270
|
+
catch (err) {
|
|
271
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
272
|
+
throw new Error(`高德地图服务请求失败(${reason})。请检查 amapKey 是否有效:https://console.amap.com/dev/key/app`);
|
|
273
|
+
}
|
|
274
|
+
finally {
|
|
275
|
+
clearTimeout(timer);
|
|
276
|
+
signal.removeEventListener('abort', onAbort);
|
|
277
|
+
}
|
|
278
|
+
if (!res.ok)
|
|
279
|
+
throw new Error(`Amap HTTP ${res.status}: ${res.statusText}`);
|
|
280
|
+
const body = (await res.json());
|
|
281
|
+
if (body.status === '1')
|
|
282
|
+
return body;
|
|
283
|
+
this.throwAmapError(body.infocode ?? '?', body.info ?? 'unknown');
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* 把高德返回的错误码翻译成面向用户的异常(key / 配额 / 其它)。
|
|
287
|
+
* @param infocode - 高德 `infocode`。
|
|
288
|
+
* @param info - 高德 `info` 文案。
|
|
289
|
+
*/
|
|
290
|
+
throwAmapError(infocode, info) {
|
|
291
|
+
// Key-related errors are the most common user mistake — guide them to fix it.
|
|
292
|
+
if (infocode === '10001' || infocode === '10003' || /INVALID_USER_KEY|USER_KEY_PLAT_NOMATCH/i.test(info)) {
|
|
293
|
+
throw new Error(`高德 key 无效或未生效(${infocode}: ${info})。请检查插件配置中的 amapKey 是否正确,或前往 https://console.amap.com/dev/key/app 重新申请。`);
|
|
294
|
+
}
|
|
295
|
+
// 配额类错误:QPS 超限(10020/10021)可重试,日配额超限(10022/10023)不可。
|
|
296
|
+
if (infocode === '10020' || infocode === '10021' || /QPS_HAS_EXCEEDED/i.test(info)) {
|
|
297
|
+
throw new AmapQuotaError(infocode, `高德请求过于频繁(QPS 超限:${info})。插件已自动限速排队并重试;若持续出现,可在插件配置中调低 maxQps 减少并发。`, true);
|
|
298
|
+
}
|
|
299
|
+
if (infocode === '10022' || infocode === '10023' || /QUOTA_HAS_EXCEEDED/i.test(info)) {
|
|
300
|
+
throw new AmapQuotaError(infocode, `高德今日配额已用尽(${info})。请明天再试,或前往 https://console.amap.com/dev/key/app 查看当前配额。`, false);
|
|
301
|
+
}
|
|
302
|
+
throw new Error(`Amap API error ${infocode}: ${info}`);
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* 裸 GET,返回二进制(静态地图)。高德出错时返回 JSON 而不是图片,
|
|
306
|
+
* 所以响应不是 image/* 就按错误体解析。
|
|
307
|
+
*/
|
|
308
|
+
async rawGetBytes(path, params, signal) {
|
|
309
|
+
const url = new URL(`${REST}${path}`);
|
|
310
|
+
url.searchParams.set('key', this.opts.key);
|
|
311
|
+
for (const [k, v] of Object.entries(params)) {
|
|
312
|
+
if (v !== undefined && v !== '')
|
|
313
|
+
url.searchParams.set(k, v);
|
|
314
|
+
}
|
|
315
|
+
const controller = new AbortController();
|
|
316
|
+
const timer = setTimeout(() => controller.abort(new Error(`Amap request timed out after ${this.opts.timeoutMs}ms`)), this.opts.timeoutMs);
|
|
317
|
+
const onAbort = () => controller.abort(signal.reason);
|
|
318
|
+
if (signal.aborted)
|
|
319
|
+
controller.abort(signal.reason);
|
|
320
|
+
else
|
|
321
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
322
|
+
let res;
|
|
323
|
+
try {
|
|
324
|
+
res = await fetch(url, { signal: controller.signal, headers: { Accept: 'image/png,application/json' } });
|
|
325
|
+
}
|
|
326
|
+
catch (err) {
|
|
327
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
328
|
+
throw new Error(`高德地图服务请求失败(${reason})。请检查 amapKey 是否有效:https://console.amap.com/dev/key/app`);
|
|
329
|
+
}
|
|
330
|
+
finally {
|
|
331
|
+
clearTimeout(timer);
|
|
332
|
+
signal.removeEventListener('abort', onAbort);
|
|
333
|
+
}
|
|
334
|
+
if (!res.ok)
|
|
335
|
+
throw new Error(`Amap HTTP ${res.status}: ${res.statusText}`);
|
|
336
|
+
const type = res.headers.get('content-type') ?? '';
|
|
337
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
338
|
+
if (/image\//i.test(type))
|
|
339
|
+
return bytes;
|
|
340
|
+
let body = {};
|
|
341
|
+
try {
|
|
342
|
+
body = JSON.parse(new TextDecoder().decode(bytes));
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
// 非 JSON 也非图片:当作协议异常。
|
|
346
|
+
}
|
|
347
|
+
this.throwAmapError(body.infocode ?? '?', body.info ?? `unexpected content-type: ${type}`);
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* 静态地图:一张**真地图** PNG,路线折线与起终点标注由高德绘制。
|
|
351
|
+
*
|
|
352
|
+
* 取景框**交给高德自己适配**——不传 `location`,不传 `zoom`。这里踩过
|
|
353
|
+
* 一个很贵的坑:高德静态地图的 `zoom` 与标准 Web Mercator 差一级
|
|
354
|
+
* (它的 12 级才等于 256px 瓦片的 13 级),于是按 Mercator 公式"算准了
|
|
355
|
+
* 装得下"的 zoom 实际被放大一倍,路线溢出画布;而高德**不报错**,只是
|
|
356
|
+
* 静默把整条 `paths` 丢掉——现象正是"有底图、没路线"。
|
|
357
|
+
*
|
|
358
|
+
* 只给 `paths`(+`markers`)时高德按外包框自适应取景,实测 0.8km 步行、
|
|
359
|
+
* 20.9km 驾车、1205km 跨省、公交链各种尺度都完整居中、不裁切。
|
|
360
|
+
*
|
|
361
|
+
* 结果按参数缓存,同一张图不会被反复计费。
|
|
362
|
+
*
|
|
363
|
+
* @param line - 路线几何(GCJ-02,与高德出图坐标系一致)。
|
|
364
|
+
* @param opts - 画布尺寸与是否画起终点标注。
|
|
365
|
+
* @param signal - 取消信号。
|
|
366
|
+
* @returns PNG 字节。
|
|
367
|
+
*/
|
|
368
|
+
async staticMap(line, opts, signal) {
|
|
369
|
+
if (line.length < 2)
|
|
370
|
+
throw new Error('静态地图至少需要两个坐标点');
|
|
371
|
+
const width = Math.max(64, Math.min(1024, Math.round(opts.width)));
|
|
372
|
+
const height = Math.max(64, Math.min(1024, Math.round(opts.height)));
|
|
373
|
+
const encoded = compactLine(line, 160);
|
|
374
|
+
const cacheKey = `staticmap:${width}x${height}:${encoded}`;
|
|
375
|
+
const hit = this.mapCache.get(cacheKey);
|
|
376
|
+
if (hit !== undefined)
|
|
377
|
+
return hit;
|
|
378
|
+
await this.limiter.acquire(signal);
|
|
379
|
+
const params = { size: `${width}*${height}` };
|
|
380
|
+
// 所有点重合的退化"路线"没有外包框,高德无从适配;退回定点取景。
|
|
381
|
+
let minLng = Infinity;
|
|
382
|
+
let maxLng = -Infinity;
|
|
383
|
+
let minLat = Infinity;
|
|
384
|
+
let maxLat = -Infinity;
|
|
385
|
+
for (const [lng, lat] of line) {
|
|
386
|
+
if (lng < minLng)
|
|
387
|
+
minLng = lng;
|
|
388
|
+
if (lng > maxLng)
|
|
389
|
+
maxLng = lng;
|
|
390
|
+
if (lat < minLat)
|
|
391
|
+
minLat = lat;
|
|
392
|
+
if (lat > maxLat)
|
|
393
|
+
maxLat = lat;
|
|
394
|
+
}
|
|
395
|
+
if (maxLng - minLng < 1e-6 && maxLat - minLat < 1e-6) {
|
|
396
|
+
params.location = `${line[0][0]},${line[0][1]}`;
|
|
397
|
+
params.zoom = '16';
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
// 官方格式:weight,color,transparency,fillcolor,fillTransparency:坐标;
|
|
401
|
+
// (用竖线分隔是错的,实测返回 20003 UNKNOWN_ERROR)
|
|
402
|
+
params.paths = `5,0x4F8CFF,1,,:${encoded}`;
|
|
403
|
+
}
|
|
404
|
+
if (opts.markers !== false) {
|
|
405
|
+
const start = line[0];
|
|
406
|
+
const end = line[line.length - 1];
|
|
407
|
+
params.markers = `mid,0x22A06B,起:${start[0]},${start[1]}|mid,0xE05C5C,终:${end[0]},${end[1]}`;
|
|
408
|
+
}
|
|
409
|
+
const bytes = await this.rawGetBytes('v3/staticmap', params, signal);
|
|
410
|
+
this.mapCache.set(cacheKey, bytes);
|
|
411
|
+
return bytes;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Plan a route. `mode` maps to the Amap endpoint.
|
|
415
|
+
* Transit requires city1/city2 (origin/destination city names).
|
|
416
|
+
*/
|
|
417
|
+
async route(origin, destination, mode, opts = {}, signal) {
|
|
418
|
+
if (mode === 'transit') {
|
|
419
|
+
return this.transitRoute(origin, destination, opts, signal);
|
|
420
|
+
}
|
|
421
|
+
const path = `v5/direction/${mode}`;
|
|
422
|
+
const cacheKey = `route:${mode}:${formatLngLat(origin)}:${formatLngLat(destination)}`;
|
|
423
|
+
const body = await this.request(path, {
|
|
424
|
+
origin: formatLngLat(origin),
|
|
425
|
+
destination: formatLngLat(destination),
|
|
426
|
+
// show_fields 是 v5 的必填开关:不请求就拿不到折线(几何会退化成直线)
|
|
427
|
+
// 和 cost.duration(方案耗时)。这两样都是卡片要用的。
|
|
428
|
+
show_fields: 'polyline,cost',
|
|
429
|
+
}, signal, { cache: this.routeCache, cacheKey, retries: 1 });
|
|
430
|
+
const path0 = body.route.paths[0];
|
|
431
|
+
if (!path0)
|
|
432
|
+
throw new Error('Amap returned no route path');
|
|
433
|
+
const steps = (path0.steps ?? []).map((s) => ({
|
|
434
|
+
instruction: s.instruction ?? '',
|
|
435
|
+
distanceM: readStepDistance(s),
|
|
436
|
+
durationS: readDuration(s),
|
|
437
|
+
}));
|
|
438
|
+
// 真实路线几何:高德把折线拆在每一步里,按步序拼接;缺失时退成起终点直线。
|
|
439
|
+
// 相邻步的首尾点通常重复,交给 dedupeGeometry 去掉。
|
|
440
|
+
const line = [];
|
|
441
|
+
for (const s of path0.steps ?? [])
|
|
442
|
+
line.push(...decodeAmapPolyline(s.polyline));
|
|
443
|
+
const geometry = dedupeGeometry(line);
|
|
444
|
+
return {
|
|
445
|
+
provider: 'amap',
|
|
446
|
+
distanceM: Number(path0.distance ?? 0),
|
|
447
|
+
durationS: readDuration(path0),
|
|
448
|
+
polyline: steps.map((s) => s.instruction).join(' → '),
|
|
449
|
+
points: [origin, destination],
|
|
450
|
+
geometry: geometry.length >= 2 ? geometry : [origin, destination],
|
|
451
|
+
steps,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
async transitRoute(origin, destination, opts, signal) {
|
|
455
|
+
// Amap transit requires city1 (and ideally city2). If resolution failed,
|
|
456
|
+
// surface a readable error instead of a bare INVALID_PARAMS.
|
|
457
|
+
if (!opts.city1 || !opts.city2) {
|
|
458
|
+
const missing = !opts.city1 ? 'city1(起点城市)' : 'city2(终点城市)';
|
|
459
|
+
throw new Error(`公交路径规划需要 ${missing},但城市解析失败。请改用更具体的地址,或直接提供 "lng,lat" 坐标。`);
|
|
460
|
+
}
|
|
461
|
+
const cacheKey = `route:transit:${formatLngLat(origin)}:${formatLngLat(destination)}:${opts.city1}:${opts.city2}`;
|
|
462
|
+
const body = await this.request('v5/direction/transit/integrated', {
|
|
463
|
+
origin: formatLngLat(origin),
|
|
464
|
+
destination: formatLngLat(destination),
|
|
465
|
+
city1: opts.city1 ?? '',
|
|
466
|
+
city2: opts.city2 ?? '',
|
|
467
|
+
// v5 公交同样需要 show_fields 才返回 cost.duration(方案耗时)。
|
|
468
|
+
show_fields: 'polyline,cost',
|
|
469
|
+
}, signal, { cache: this.routeCache, cacheKey, retries: 1 });
|
|
470
|
+
const transit0 = body.route.transits[0];
|
|
471
|
+
if (!transit0)
|
|
472
|
+
throw new Error('Amap returned no transit plan');
|
|
473
|
+
const steps = (transit0.segments ?? []).map((seg, i) => {
|
|
474
|
+
if (seg.bus?.buslines?.length) {
|
|
475
|
+
const line = seg.bus.buslines[0];
|
|
476
|
+
const from = line.departure_stop?.name ?? '';
|
|
477
|
+
const to = line.arrival_stop?.name ?? '';
|
|
478
|
+
return {
|
|
479
|
+
instruction: `乘坐 ${line.name ?? '公交'}(${from} → ${to})`,
|
|
480
|
+
distanceM: 0,
|
|
481
|
+
durationS: 0,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
if (seg.walking) {
|
|
485
|
+
return {
|
|
486
|
+
instruction: `步行 ${i + 1}`,
|
|
487
|
+
distanceM: Number(seg.walking.distance ?? 0),
|
|
488
|
+
durationS: readDuration(seg.walking),
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
return { instruction: '换乘', distanceM: 0, durationS: 0 };
|
|
492
|
+
});
|
|
493
|
+
const line = dedupeGeometry(transitGeometry(transit0.segments ?? []));
|
|
494
|
+
return {
|
|
495
|
+
provider: 'amap',
|
|
496
|
+
distanceM: Number(transit0.distance ?? 0),
|
|
497
|
+
// v5 公交把方案耗时放在 cost.duration;旧代码只读 duration,导致
|
|
498
|
+
// 公交永远显示"耗时未知"(渲染层用 durationS > 0 判断)。
|
|
499
|
+
durationS: readDuration(transit0),
|
|
500
|
+
polyline: steps.map((s) => s.instruction).join(' → '),
|
|
501
|
+
points: [origin, destination],
|
|
502
|
+
geometry: line.length >= 2 ? line : [origin, destination],
|
|
503
|
+
steps,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
/** Forward geocode: address → coordinates. */
|
|
507
|
+
async geocode(address, signal) {
|
|
508
|
+
const cacheKey = `geo:${address}`;
|
|
509
|
+
const body = await this.request('v3/geocode/geo', { address }, signal, { cache: this.geoCache, cacheKey });
|
|
510
|
+
const first = body.geocodes?.[0];
|
|
511
|
+
if (!first)
|
|
512
|
+
throw new Error(`Amap could not geocode address: ${address}`);
|
|
513
|
+
const [lng, lat] = first.location.split(',').map(Number);
|
|
514
|
+
if (!Number.isFinite(lng) || !Number.isFinite(lat))
|
|
515
|
+
throw new Error('Amap geocode returned invalid location');
|
|
516
|
+
return {
|
|
517
|
+
provider: 'amap',
|
|
518
|
+
formatted: first.formatted_address ?? address,
|
|
519
|
+
location: [lng, lat],
|
|
520
|
+
city: normalizeCity(first.city, first.province),
|
|
521
|
+
district: first.district,
|
|
522
|
+
adcode: first.adcode,
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
/** Reverse geocode: coordinates → address. */
|
|
526
|
+
async reverseGeocode(location, signal) {
|
|
527
|
+
const cacheKey = `regeo:${formatLngLat(location)}`;
|
|
528
|
+
const body = await this.request('v3/geocode/regeo', { location: formatLngLat(location) }, signal, { cache: this.geoCache, cacheKey });
|
|
529
|
+
const re = body.regeocode;
|
|
530
|
+
if (!re)
|
|
531
|
+
throw new Error('Amap reverse geocode returned no result');
|
|
532
|
+
const comp = re.addressComponent;
|
|
533
|
+
return {
|
|
534
|
+
provider: 'amap',
|
|
535
|
+
formatted: re.formatted_address ?? formatLngLat(location),
|
|
536
|
+
location,
|
|
537
|
+
city: normalizeCity(comp?.city, comp?.province),
|
|
538
|
+
district: comp?.district,
|
|
539
|
+
province: comp?.province,
|
|
540
|
+
township: comp?.township,
|
|
541
|
+
adcode: comp?.adcode,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
/** POI text search. */
|
|
545
|
+
async poiSearch(keywords, opts, signal) {
|
|
546
|
+
const region = opts.region ?? '';
|
|
547
|
+
const cacheKey = `poi:text:${keywords}:${region}:${opts.cityLimit ? '1' : '0'}`;
|
|
548
|
+
const body = await this.request('v5/place/text', { keywords, region, city_limit: opts.cityLimit ? 'true' : 'false' }, signal, { cache: this.poiCache, cacheKey });
|
|
549
|
+
return (body.pois ?? []).map((p) => {
|
|
550
|
+
const [lng, lat] = p.location.split(',').map(Number);
|
|
551
|
+
return {
|
|
552
|
+
name: p.name,
|
|
553
|
+
location: [lng, lat],
|
|
554
|
+
type: p.type,
|
|
555
|
+
address: p.address,
|
|
556
|
+
tel: p.tel,
|
|
557
|
+
};
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
/** POI around search (nearest first by default). */
|
|
561
|
+
async poiAround(location, keywords, opts, signal) {
|
|
562
|
+
const radius = opts.radiusM ? String(opts.radiusM) : '';
|
|
563
|
+
const types = opts.types ?? '';
|
|
564
|
+
const cacheKey = `poi:around:${formatLngLat(location)}:${keywords}:${radius}:${types}`;
|
|
565
|
+
const body = await this.request('v5/place/around', {
|
|
566
|
+
location: formatLngLat(location),
|
|
567
|
+
keywords,
|
|
568
|
+
radius,
|
|
569
|
+
types,
|
|
570
|
+
}, signal, { cache: this.poiCache, cacheKey });
|
|
571
|
+
return (body.pois ?? []).map((p) => {
|
|
572
|
+
const [lng, lat] = p.location.split(',').map(Number);
|
|
573
|
+
return {
|
|
574
|
+
name: p.name,
|
|
575
|
+
location: [lng, lat],
|
|
576
|
+
type: p.type,
|
|
577
|
+
address: p.address,
|
|
578
|
+
tel: p.tel,
|
|
579
|
+
distanceM: p.distance ? Number(p.distance) : undefined,
|
|
580
|
+
};
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Normalize Amap's `city` field to a plain string.
|
|
586
|
+
*
|
|
587
|
+
* 高德对直辖市(北京/上海/天津/重庆)的 addressComponent.city 返回空数组 [],
|
|
588
|
+
* 城市名实际在 province 字段里。数组取首元素,空值回退 province。
|
|
589
|
+
*/
|
|
590
|
+
function normalizeCity(city, province) {
|
|
591
|
+
const value = Array.isArray(city) ? city[0] : city;
|
|
592
|
+
if (typeof value === 'string' && value)
|
|
593
|
+
return value;
|
|
594
|
+
return typeof province === 'string' && province ? province : '';
|
|
595
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** Nominatim (OpenStreetMap) geocoding client — free fallback, 1 QPS policy. */
|
|
2
|
+
const PUBLIC_NOMINATIM = 'https://nominatim.openstreetmap.org';
|
|
3
|
+
export class NominatimClient {
|
|
4
|
+
opts;
|
|
5
|
+
constructor(opts) {
|
|
6
|
+
this.opts = opts;
|
|
7
|
+
}
|
|
8
|
+
async get(path, params, signal) {
|
|
9
|
+
const url = new URL(`${this.opts.baseUrl ?? PUBLIC_NOMINATIM}${path}`);
|
|
10
|
+
for (const [k, v] of Object.entries(params))
|
|
11
|
+
url.searchParams.set(k, v);
|
|
12
|
+
let res;
|
|
13
|
+
try {
|
|
14
|
+
res = await fetch(url, {
|
|
15
|
+
signal,
|
|
16
|
+
headers: { Accept: 'application/json', 'User-Agent': this.opts.userAgent },
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
// Network-layer failure (DNS, timeout, blocked host). On CN networks
|
|
21
|
+
// nominatim.openstreetmap.org is often unreachable — guide the user to
|
|
22
|
+
// the Amap key instead of surfacing a bare `fetch failed`.
|
|
23
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
24
|
+
throw new Error(`无法访问 Nominatim 免费地理编码服务(${reason})。部分网络环境下该服务不可达,建议在插件配置中设置高德 amapKey(https://console.amap.com/dev/key/app)以获得稳定的地理编码。`);
|
|
25
|
+
}
|
|
26
|
+
if (!res.ok)
|
|
27
|
+
throw new Error(`Nominatim HTTP ${res.status}: ${res.statusText}`);
|
|
28
|
+
return res.json();
|
|
29
|
+
}
|
|
30
|
+
/** Forward geocode. */
|
|
31
|
+
async geocode(query, signal) {
|
|
32
|
+
const body = (await this.get('/search', { format: 'jsonv2', q: query, limit: '1' }, signal));
|
|
33
|
+
const first = body?.[0];
|
|
34
|
+
if (!first)
|
|
35
|
+
throw new Error(`Nominatim could not geocode: ${query}`);
|
|
36
|
+
const lat = Number(first.lat);
|
|
37
|
+
const lng = Number(first.lon);
|
|
38
|
+
if (!Number.isFinite(lat) || !Number.isFinite(lng))
|
|
39
|
+
throw new Error('Nominatim returned invalid coordinates');
|
|
40
|
+
const city = first.address?.city ?? first.address?.town ?? first.address?.village;
|
|
41
|
+
return {
|
|
42
|
+
provider: 'nominatim',
|
|
43
|
+
formatted: first.display_name ?? query,
|
|
44
|
+
location: [lng, lat],
|
|
45
|
+
city,
|
|
46
|
+
district: first.address?.district,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** Reverse geocode. */
|
|
50
|
+
async reverseGeocode(location, signal) {
|
|
51
|
+
const body = (await this.get('/reverse', { format: 'jsonv2', lat: String(location[1]), lon: String(location[0]) }, signal));
|
|
52
|
+
if (!body?.display_name)
|
|
53
|
+
throw new Error('Nominatim reverse geocode returned no result');
|
|
54
|
+
const city = body.address?.city ?? body.address?.town ?? body.address?.village;
|
|
55
|
+
return {
|
|
56
|
+
provider: 'nominatim',
|
|
57
|
+
formatted: body.display_name,
|
|
58
|
+
location,
|
|
59
|
+
city,
|
|
60
|
+
district: body.address?.district,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|