@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,1584 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-map-tools browser half: the bundle's own configuration card.
|
|
3
|
+
*
|
|
4
|
+
* Hand-written lazy-CJS bundle (`window.__ModuleLoader__.load`), no build step,
|
|
5
|
+
* and no imports beyond React. Requiring a `@deepseek-ai/dsh-*` client package
|
|
6
|
+
* would couple this card to a signature that may be absent from the running
|
|
7
|
+
* harness's module table — the sibling dsh-jev-tools card takes the same stance,
|
|
8
|
+
* and a `require` that throws at mount time takes the whole card down with it.
|
|
9
|
+
*
|
|
10
|
+
* **Where it renders.** `plugins.bundle.config`, the seat the sidebar 插件 page
|
|
11
|
+
* declares for a bundle's own configuration. That page keys the seat by the
|
|
12
|
+
* bundle's **package name**, draws the title, the package name, the description,
|
|
13
|
+
* the crumb and the uninstall/switch controls itself, and asks the card only for
|
|
14
|
+
* `view: 'page'` (verified against the running 0.1.6-alpha.2 harness:
|
|
15
|
+
* `ui-plugin-manager` renders
|
|
16
|
+
* `renderSlot('plugins.bundle.config', { view: 'page' }, { entryKey: pkg.name })`).
|
|
17
|
+
* Harnesses older than 0.1.6-alpha.2 declare no such seat and dispatch the
|
|
18
|
+
* settings page's `settings.plugin.item` list seat instead (`summary`, then
|
|
19
|
+
* `page`); that is why the component still answers `summary`, and why both seats
|
|
20
|
+
* are registered. `slots.inject` runs its factory only for a slot the running
|
|
21
|
+
* host declares, so this is a capability probe, not a version check.
|
|
22
|
+
*
|
|
23
|
+
* **Where the data lives.** The host loopback route `/dsh-map-tools/config`,
|
|
24
|
+
* never the DSH settings document. The key lives in
|
|
25
|
+
* `~/.dsh-map-tools/config.json` (0600) and is never echoed back — the route
|
|
26
|
+
* answers `hasAmapKey: boolean` and nothing else — so the input starts blank on
|
|
27
|
+
* every load and a blank left untouched is never written.
|
|
28
|
+
*
|
|
29
|
+
* **How it saves.** Staged edits, one explicit save. Leaving the page discards
|
|
30
|
+
* the draft (the component's state dies with it), so there is no 取消 control;
|
|
31
|
+
* an invalid draft blocks the save instead of being silently repaired; only
|
|
32
|
+
* changed fields ride the request; and after a save the form re-seeds from what
|
|
33
|
+
* the host accepted, because the host is the only authority on whether a write
|
|
34
|
+
* landed.
|
|
35
|
+
*/
|
|
36
|
+
window.__ModuleLoader__.load({
|
|
37
|
+
id: 'dsh-map-tools',
|
|
38
|
+
factory: (require) => {
|
|
39
|
+
var module = { exports: {} }
|
|
40
|
+
var exports = module.exports
|
|
41
|
+
|
|
42
|
+
/** The bundle's package name: the key the Plugins page dispatches this seat by. */
|
|
43
|
+
var BUNDLE = 'dsh-map-tools'
|
|
44
|
+
/** The Plugins page's seat for a bundle's own configuration (keyed by package name). */
|
|
45
|
+
var BUNDLE_SEAT = 'plugins.bundle.config'
|
|
46
|
+
/** Pre-0.1.6 seat: the settings page's plugin list (list kind, `key` = settings namespace). */
|
|
47
|
+
var LEGACY_SEAT = 'settings.plugin.item'
|
|
48
|
+
/** The host loopback route this card reads and writes. */
|
|
49
|
+
var CONFIG_URL = '/dsh-map-tools/config'
|
|
50
|
+
/** Where a user creates an Amap (高德) Web service key — mirrors src/config.ts. */
|
|
51
|
+
var AMAP_URL = 'https://console.amap.com/dev/key/app'
|
|
52
|
+
/** The composition schema's default timeout; the route omits the field while unset. */
|
|
53
|
+
var DEFAULT_TIMEOUT_MS = 15000
|
|
54
|
+
/** Accepted timeout range (ms): below it every upstream call aborts, above it a typo hides. */
|
|
55
|
+
var TIMEOUT_MIN_MS = 100
|
|
56
|
+
var TIMEOUT_MAX_MS = 600000
|
|
57
|
+
var PROVIDERS = [
|
|
58
|
+
{ id: 'amap', label: '高德地图(推荐,公交/POI/中文地址都靠它)' },
|
|
59
|
+
{ id: 'osm', label: '免费 OSM(无需 key,能力有限)' },
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The staged timeout as a number, or null while it is not a usable value.
|
|
64
|
+
*
|
|
65
|
+
* Nothing here coerces a bad draft into a default: silently rewriting `abc`
|
|
66
|
+
* to 15000 would save a value the user never typed.
|
|
67
|
+
*
|
|
68
|
+
* @param text - the staged input text.
|
|
69
|
+
* @returns milliseconds, or null when the text is not a value a save may write.
|
|
70
|
+
*/
|
|
71
|
+
function parseTimeout(text) {
|
|
72
|
+
var value = String(text === null || text === undefined ? '' : text).trim()
|
|
73
|
+
if (!/^\d+$/.test(value)) return null
|
|
74
|
+
var parsed = Number(value)
|
|
75
|
+
if (parsed < TIMEOUT_MIN_MS || parsed > TIMEOUT_MAX_MS) return null
|
|
76
|
+
return parsed
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The effective config as the form works with it: provider and timeout always
|
|
81
|
+
* resolve, because the route omits a field the config file does not hold and
|
|
82
|
+
* the schema default applies then.
|
|
83
|
+
*
|
|
84
|
+
* @param summary - the route's non-secret summary, or null while loading.
|
|
85
|
+
* @returns the values the controls compare a draft against.
|
|
86
|
+
*/
|
|
87
|
+
function effectiveConfig(summary) {
|
|
88
|
+
var s = summary === null || summary === undefined ? {} : summary
|
|
89
|
+
var timeout = parseTimeout(s.timeoutMs)
|
|
90
|
+
return {
|
|
91
|
+
provider: s.provider === 'osm' ? 'osm' : 'amap',
|
|
92
|
+
hasAmapKey: s.hasAmapKey === true,
|
|
93
|
+
timeoutMs: timeout === null ? DEFAULT_TIMEOUT_MS : timeout,
|
|
94
|
+
configPath: typeof s.configPath === 'string' ? s.configPath : '',
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The draft a freshly accepted config seeds. The key field is always blank —
|
|
100
|
+
* the route cannot echo it, and a blank left alone is never written.
|
|
101
|
+
*
|
|
102
|
+
* @param summary - the summary the host just accepted.
|
|
103
|
+
* @returns the staged draft.
|
|
104
|
+
*/
|
|
105
|
+
function draftFrom(summary) {
|
|
106
|
+
var config = effectiveConfig(summary)
|
|
107
|
+
return { provider: config.provider, amapKey: '', timeoutMs: String(config.timeoutMs) }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Whether the staged draft holds only values a save may write.
|
|
112
|
+
*
|
|
113
|
+
* @param draft - the staged draft.
|
|
114
|
+
* @returns true when every field is usable.
|
|
115
|
+
*/
|
|
116
|
+
function draftValid(draft) {
|
|
117
|
+
if (draft === null || draft === undefined) return false
|
|
118
|
+
if (draft.provider !== 'amap' && draft.provider !== 'osm') return false
|
|
119
|
+
return parseTimeout(draft.timeoutMs) !== null
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The patch a save would send: only fields the user actually changed, never
|
|
124
|
+
* an unusable one. An empty result means the save is a no-op, which keeps
|
|
125
|
+
* saving idempotent — clicking twice writes once.
|
|
126
|
+
*
|
|
127
|
+
* @param draft - the staged draft.
|
|
128
|
+
* @param summary - the config the host currently holds.
|
|
129
|
+
* @returns the JSON body a save would post.
|
|
130
|
+
*/
|
|
131
|
+
function configPatch(draft, summary) {
|
|
132
|
+
var patch = {}
|
|
133
|
+
if (draft === null || draft === undefined) return patch
|
|
134
|
+
var config = effectiveConfig(summary)
|
|
135
|
+
if (draft.provider !== config.provider) patch.provider = draft.provider
|
|
136
|
+
if (draft.amapKey !== '') patch.amapKey = draft.amapKey
|
|
137
|
+
var timeout = parseTimeout(draft.timeoutMs)
|
|
138
|
+
if (timeout !== null && timeout !== config.timeoutMs) patch.timeoutMs = timeout
|
|
139
|
+
return patch
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Whether the draft carries an edit the host does not already hold.
|
|
144
|
+
*
|
|
145
|
+
* @param draft - the staged draft.
|
|
146
|
+
* @param summary - the config the host currently holds.
|
|
147
|
+
* @returns true when a save would write something.
|
|
148
|
+
*/
|
|
149
|
+
function isDirty(draft, summary) {
|
|
150
|
+
return Object.keys(configPatch(draft, summary)).length > 0
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* One line describing the current state: the page's status row, and the whole
|
|
155
|
+
* one-liner the legacy list seat asks a card for.
|
|
156
|
+
*
|
|
157
|
+
* @param summary - the route's summary, or null while loading.
|
|
158
|
+
* @returns the status text.
|
|
159
|
+
*/
|
|
160
|
+
function statusLine(summary) {
|
|
161
|
+
if (summary === null || summary === undefined) return '读取中…'
|
|
162
|
+
var config = effectiveConfig(summary)
|
|
163
|
+
if (config.provider === 'osm') return '免费 OSM · 无需 key(公交/POI/中文地址不可用)'
|
|
164
|
+
return config.hasAmapKey ? '高德 · 已配置 key' : '高德 · 未配置 key(公交/POI/中文地址需先填 key)'
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Register the card into every configuration seat the running host declares.
|
|
169
|
+
*
|
|
170
|
+
* @param ctx - the client plugin context.
|
|
171
|
+
*/
|
|
172
|
+
function registerCard(ctx) {
|
|
173
|
+
if (!ctx || typeof ctx.inject !== 'function') return
|
|
174
|
+
ctx.inject(['slots'], function (scope) {
|
|
175
|
+
if (!scope.slots || typeof scope.slots.inject !== 'function') return
|
|
176
|
+
var react
|
|
177
|
+
try {
|
|
178
|
+
react = require('react')
|
|
179
|
+
} catch (error) {
|
|
180
|
+
console.error(`[dsh-map-tools] config card skipped: ${error}`)
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
var Card = ConfigCard(react)
|
|
184
|
+
registerOne(scope, Card, {
|
|
185
|
+
// 0.1.6-alpha.2+: the bundle's page in the sidebar 插件 page, keyed by package name.
|
|
186
|
+
seat: BUNDLE_SEAT,
|
|
187
|
+
options: { name: BUNDLE_SEAT, key: BUNDLE },
|
|
188
|
+
})
|
|
189
|
+
registerOne(scope, Card, {
|
|
190
|
+
// Older hosts: the settings page's plugin list. `id` is required there and
|
|
191
|
+
// `key` is the settings namespace the host serves for this card.
|
|
192
|
+
seat: LEGACY_SEAT,
|
|
193
|
+
options: { name: LEGACY_SEAT, id: 'map-tools', key: BUNDLE, order: 25, label: '地图引擎 (dsh-map-tools)' },
|
|
194
|
+
})
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Register one seat, never letting a failure escape into the host.
|
|
200
|
+
*
|
|
201
|
+
* The guard sits **inside** the factory: `slots.inject` only stores the
|
|
202
|
+
* factory and calls it later (synchronously when the slot is already declared,
|
|
203
|
+
* otherwise once the declaration arrives), so a throw from the registration
|
|
204
|
+
* would surface outside any try/catch wrapped around the `inject` call — the
|
|
205
|
+
* failures this plugin's turn-tail registration already hit.
|
|
206
|
+
*
|
|
207
|
+
* @param scope - the client context carrying `slots`.
|
|
208
|
+
* @param Card - the component to register.
|
|
209
|
+
* @param seat - the slot name and its registration options.
|
|
210
|
+
*/
|
|
211
|
+
function registerOne(scope, Card, seat) {
|
|
212
|
+
try {
|
|
213
|
+
scope.slots.inject(seat.seat, function () {
|
|
214
|
+
try {
|
|
215
|
+
return scope.slots.register(seat.options, Card)
|
|
216
|
+
} catch (error) {
|
|
217
|
+
console.error(`[dsh-map-tools] config card skipped on ${seat.seat}: ${error}`)
|
|
218
|
+
return function () {}
|
|
219
|
+
}
|
|
220
|
+
})
|
|
221
|
+
} catch (error) {
|
|
222
|
+
console.error(`[dsh-map-tools] config card skipped on ${seat.seat}: ${error}`)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* The bundle's configuration card.
|
|
228
|
+
*
|
|
229
|
+
* The owner draws the page's title, breadcrumb and uninstall control, so this
|
|
230
|
+
* component draws only the configuration itself — in the vocabulary the
|
|
231
|
+
* shipped settings surfaces use (label / control / hint and capsule buttons),
|
|
232
|
+
* so a third-party page reads as the same product.
|
|
233
|
+
*
|
|
234
|
+
* @param react - the browser module table's React.
|
|
235
|
+
* @returns the card component.
|
|
236
|
+
*/
|
|
237
|
+
function ConfigCard(react) {
|
|
238
|
+
var h = react.createElement
|
|
239
|
+
|
|
240
|
+
// Every name below is defined by the running theme (`ui-theme`'s
|
|
241
|
+
// design-platform.css, cross-checked against the live theme token table).
|
|
242
|
+
// A name the theme does not define falls back silently and stops following
|
|
243
|
+
// light/dark — which is exactly what `--dsw-alias-accent`,
|
|
244
|
+
// `--dsw-alias-border` and `--dsw-alias-bg-elevated` did in this card's
|
|
245
|
+
// previous version. Fallbacks stay as a last resort, not as the plan.
|
|
246
|
+
var TEXT = 'var(--dsw-alias-label-primary, inherit)'
|
|
247
|
+
var MUTED = 'var(--dsw-alias-label-tertiary, rgba(127,127,127,0.8))'
|
|
248
|
+
var HINT = 'var(--dsw-alias-label-secondary, rgba(127,127,127,0.9))'
|
|
249
|
+
var BORDER = 'var(--dsw-alias-border-l4, rgba(127,127,127,0.3))'
|
|
250
|
+
var SEPARATOR = '0.5px solid var(--dsw-alias-border-l2, rgba(127,127,127,0.2))'
|
|
251
|
+
var LINK = 'var(--dsw-alias-link, #4f8cff)'
|
|
252
|
+
var ERROR = 'var(--dsw-alias-state-error-primary, #e05c5c)'
|
|
253
|
+
var OK = 'var(--dsw-alias-state-success-primary, #22a06b)'
|
|
254
|
+
var FIELD_BG = 'var(--dsw-alias-bg-layer-3, rgba(127,127,127,0.08))'
|
|
255
|
+
var PRIMARY_FILL = 'var(--dsw-alias-button-primary-fill, #4f8cff)'
|
|
256
|
+
var PRIMARY_TEXT = 'var(--dsw-alias-label-primary-foreground, #fff)'
|
|
257
|
+
|
|
258
|
+
var FIELD = { display: 'flex', flexDirection: 'column', gap: '6px', padding: '12px 0', borderTop: SEPARATOR }
|
|
259
|
+
var FIRST_FIELD = { display: 'flex', flexDirection: 'column', gap: '6px', padding: '12px 0' }
|
|
260
|
+
var LABEL = { fontSize: '13px', fontWeight: 500, lineHeight: 1.5, color: TEXT }
|
|
261
|
+
var CONTROL = { width: '100%', boxSizing: 'border-box', height: '34px', padding: '0 12px', border: '0.5px solid ' + BORDER, borderRadius: '8px', background: FIELD_BG, font: 'inherit', fontSize: '13px', lineHeight: 1.5, color: TEXT }
|
|
262
|
+
var HINT_STYLE = { margin: 0, fontSize: '12px', lineHeight: 1.5, color: HINT }
|
|
263
|
+
var CAPTION_STYLE = { margin: 0, fontSize: '12px', lineHeight: 1.5, color: MUTED }
|
|
264
|
+
/** The Amap application link sits on its own line: a key is the one thing a
|
|
265
|
+
* first-time user must go elsewhere to get, so it may not hide in a label row. */
|
|
266
|
+
var LINK_LINE = { margin: 0, fontSize: '13px', lineHeight: 1.5 }
|
|
267
|
+
var BUTTON = { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', height: '28px', padding: '0 12px', borderRadius: '14px', border: '0.5px solid ' + BORDER, background: 'transparent', font: 'inherit', fontSize: '12px', lineHeight: '18px', color: TEXT, cursor: 'pointer' }
|
|
268
|
+
var PRIMARY_BUTTON = Object.assign({}, BUTTON, { border: 'none', background: PRIMARY_FILL, color: PRIMARY_TEXT })
|
|
269
|
+
var DISABLED = { opacity: 0.4, cursor: 'not-allowed' }
|
|
270
|
+
|
|
271
|
+
/** A label/value line (the status row, in the shape the shipped cards use). */
|
|
272
|
+
function row(label, value, valueStyle) {
|
|
273
|
+
return h('div', { style: { display: 'flex', alignItems: 'baseline', gap: '8px', fontSize: '12px', lineHeight: '18px' } },
|
|
274
|
+
h('span', { style: { flex: 'none', minWidth: '56px', color: MUTED } }, label),
|
|
275
|
+
h('span', { style: Object.assign({ minWidth: 0, color: TEXT }, valueStyle || {}) }, value))
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* A link in the theme's link color.
|
|
280
|
+
*
|
|
281
|
+
* `--dsw-alias-link` (`deepseek-500` light / `deepseek-400` dark) is the token
|
|
282
|
+
* the design system keeps for clickable text, and the one Markdown links use
|
|
283
|
+
* in the shipped UI. The previous version reached for
|
|
284
|
+
* `--dsw-alias-brand-primary`, which is a *neutral* near-black (light) /
|
|
285
|
+
* near-white (dark) — i.e. the same color as body text, which is why the
|
|
286
|
+
* Amap application link was invisible on the card.
|
|
287
|
+
*/
|
|
288
|
+
function link(href, text) {
|
|
289
|
+
return h('a', { href: href, target: '_blank', rel: 'noreferrer', style: { color: LINK, fontSize: '13px', lineHeight: 1.5, textDecoration: 'none' } }, text)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** A capsule button; `primary` fills it, `disabled` dims it. */
|
|
293
|
+
function button(label, onClick, options) {
|
|
294
|
+
var opts = options || {}
|
|
295
|
+
return h('button', {
|
|
296
|
+
type: 'button',
|
|
297
|
+
onClick: onClick,
|
|
298
|
+
disabled: opts.disabled === true,
|
|
299
|
+
style: Object.assign({}, opts.primary === true ? PRIMARY_BUTTON : BUTTON, opts.disabled === true ? DISABLED : {}),
|
|
300
|
+
}, label)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Hide characters without turning the field into a password field, so the
|
|
305
|
+
* browser's password manager does not offer to store the key as a
|
|
306
|
+
* credential. `type="password"` is the fallback where `text-security` is
|
|
307
|
+
* unsupported; `autoComplete: 'off'` applies either way.
|
|
308
|
+
*
|
|
309
|
+
* @returns the extra style, or null when the standard password type must be used.
|
|
310
|
+
*/
|
|
311
|
+
function maskStyle() {
|
|
312
|
+
if (typeof CSS !== 'undefined' && 'textSecurity' in document.documentElement.style) {
|
|
313
|
+
// Both spellings: browsers expose the property as `textSecurity` or
|
|
314
|
+
// `WebkitTextSecurity` depending on the engine.
|
|
315
|
+
return { textSecurity: 'disc', WebkitTextSecurity: 'disc' }
|
|
316
|
+
}
|
|
317
|
+
return null
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** A thrown value as note text. */
|
|
321
|
+
function describe(error) {
|
|
322
|
+
var message = error !== null && error !== undefined && error.message !== undefined ? error.message : error
|
|
323
|
+
return String(message)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* The card.
|
|
328
|
+
*
|
|
329
|
+
* @param props - the owner's props: `view: 'summary'` asks for the one-liner
|
|
330
|
+
* alone (the legacy list seat does), `view: 'page'` for the form with its
|
|
331
|
+
* own save control (the bundle seat only ever asks for this one).
|
|
332
|
+
* @returns the card.
|
|
333
|
+
*/
|
|
334
|
+
return function MapToolsBundleCard(props) {
|
|
335
|
+
var view = props !== null && props !== undefined && props.view === 'summary' ? 'summary' : 'page'
|
|
336
|
+
var stateHook = react.useState({ status: 'loading', config: null, draft: null, note: '', tone: 'plain', saving: false })
|
|
337
|
+
var state = stateHook[0]
|
|
338
|
+
var setState = stateHook[1]
|
|
339
|
+
var merge = react.useCallback(function (next) {
|
|
340
|
+
setState(function (previous) { return Object.assign({}, previous, next) })
|
|
341
|
+
}, [])
|
|
342
|
+
|
|
343
|
+
/** Read the summary again; every load re-seeds the draft from the host. */
|
|
344
|
+
var load = react.useCallback(function () {
|
|
345
|
+
return fetch(CONFIG_URL, { headers: { accept: 'application/json' } })
|
|
346
|
+
.then(function (response) {
|
|
347
|
+
if (!response.ok) throw new Error('HTTP ' + response.status)
|
|
348
|
+
return response.json()
|
|
349
|
+
})
|
|
350
|
+
.then(function (data) {
|
|
351
|
+
setState({ status: 'ready', config: data, draft: draftFrom(data), note: '', tone: 'plain', saving: false })
|
|
352
|
+
})
|
|
353
|
+
.catch(function (error) {
|
|
354
|
+
// The route is the only way in. With it missing, controls would take
|
|
355
|
+
// input nothing could accept, so the form is replaced by a line that
|
|
356
|
+
// says why rather than by fields that cannot save.
|
|
357
|
+
setState({ status: 'unavailable', config: null, draft: null, note: describe(error), tone: 'error', saving: false })
|
|
358
|
+
})
|
|
359
|
+
}, [])
|
|
360
|
+
|
|
361
|
+
react.useEffect(function () { void load() }, [load])
|
|
362
|
+
|
|
363
|
+
/** Write the staged patch, then re-seed from what the host accepted. */
|
|
364
|
+
var save = function () {
|
|
365
|
+
if (state.config === null || !draftValid(state.draft)) return
|
|
366
|
+
var body = configPatch(state.draft, state.config)
|
|
367
|
+
if (Object.keys(body).length === 0) {
|
|
368
|
+
// Nothing changed: saving stays idempotent instead of writing again.
|
|
369
|
+
merge({ note: '没有需要保存的改动', tone: 'plain' })
|
|
370
|
+
return
|
|
371
|
+
}
|
|
372
|
+
merge({ saving: true, note: '', tone: 'plain' })
|
|
373
|
+
fetch(CONFIG_URL, {
|
|
374
|
+
method: 'POST',
|
|
375
|
+
headers: { 'content-type': 'application/json' },
|
|
376
|
+
body: JSON.stringify(body),
|
|
377
|
+
})
|
|
378
|
+
.then(function (response) {
|
|
379
|
+
return response.json().catch(function () { return {} }).then(function (data) {
|
|
380
|
+
if (!response.ok) throw new Error(data && data.error ? String(data.error) : 'HTTP ' + response.status)
|
|
381
|
+
return data
|
|
382
|
+
})
|
|
383
|
+
})
|
|
384
|
+
.then(function (accepted) {
|
|
385
|
+
// The host is the only authority on whether a write landed, so the
|
|
386
|
+
// form re-seeds from its answer instead of promoting the draft; the
|
|
387
|
+
// key input returns to blank because the key now lives in the file.
|
|
388
|
+
var merged = Object.assign({}, state.config, accepted)
|
|
389
|
+
setState({ status: 'ready', config: merged, draft: draftFrom(merged), note: '已保存 — 工具已重建,直接可用', tone: 'ok', saving: false })
|
|
390
|
+
})
|
|
391
|
+
.catch(function (error) {
|
|
392
|
+
// Keep **every** staged edit: a failed save is fixed in place rather
|
|
393
|
+
// than retyped, even the fields that may already have landed.
|
|
394
|
+
merge({ saving: false, note: '保存失败:' + describe(error), tone: 'error' })
|
|
395
|
+
})
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** Open the config file in the OS editor (the route's `?open=true`). */
|
|
399
|
+
var openFile = function () {
|
|
400
|
+
fetch(CONFIG_URL + '?open=true', { headers: { accept: 'application/json' } })
|
|
401
|
+
.then(function (response) {
|
|
402
|
+
if (!response.ok) throw new Error('HTTP ' + response.status)
|
|
403
|
+
return response.json()
|
|
404
|
+
})
|
|
405
|
+
.then(function (data) {
|
|
406
|
+
merge({ config: Object.assign({}, state.config, data), note: '已在系统默认编辑器中打开配置文件', tone: 'plain' })
|
|
407
|
+
})
|
|
408
|
+
.catch(function (error) {
|
|
409
|
+
merge({ note: '打开配置文件失败:' + describe(error), tone: 'error' })
|
|
410
|
+
})
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// The bundle seat's page never asks for `summary`; the legacy list seat
|
|
414
|
+
// does, and renders this line as the card's one-liner under its title.
|
|
415
|
+
if (view === 'summary') return h('span', { style: HINT_STYLE }, statusLine(state.config))
|
|
416
|
+
|
|
417
|
+
if (state.status === 'loading') return h('p', { style: HINT_STYLE, role: 'status' }, '读取配置中…')
|
|
418
|
+
|
|
419
|
+
if (state.status === 'unavailable') {
|
|
420
|
+
return h('div', { style: { display: 'flex', flexDirection: 'column', gap: '6px' } },
|
|
421
|
+
h('p', { style: Object.assign({}, HINT_STYLE, { color: ERROR }), role: 'status' },
|
|
422
|
+
'宿主路由 ' + CONFIG_URL + ' 当前不可用,无法在这里保存配置'
|
|
423
|
+
+ (state.note === '' ? '' : '(' + state.note + ')') + '。'),
|
|
424
|
+
h('p', { style: CAPTION_STYLE }, '插件宿主半未加载时会这样;可直接编辑 ~/.dsh-map-tools/config.json(0600)。'))
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
var config = state.config
|
|
428
|
+
var draft = state.draft
|
|
429
|
+
var invalid = !draftValid(draft)
|
|
430
|
+
// 无改动可存、有非法值不可存、正在存不可重复提交——按钮状态就是这三条。
|
|
431
|
+
var blocked = !isDirty(draft, config) || invalid || state.saving
|
|
432
|
+
var noteColor = state.tone === 'error' ? ERROR : state.tone === 'ok' ? OK : HINT
|
|
433
|
+
var timeoutHint = invalid
|
|
434
|
+
? '超时要是 ' + TIMEOUT_MIN_MS + '–' + TIMEOUT_MAX_MS + ' 之间的整数毫秒值;不合法时保存会被阻止,不会替你改写'
|
|
435
|
+
: '单次上游请求的超时,默认 ' + DEFAULT_TIMEOUT_MS + ' 毫秒'
|
|
436
|
+
var mask = maskStyle()
|
|
437
|
+
var keyStyle = mask === null ? CONTROL : Object.assign({}, CONTROL, mask)
|
|
438
|
+
|
|
439
|
+
return h('div', { style: { display: 'flex', flexDirection: 'column', paddingTop: '4px' } },
|
|
440
|
+
h('div', { style: FIRST_FIELD },
|
|
441
|
+
row('状态', statusLine(config), { color: config.provider === 'amap' && config.hasAmapKey ? OK : HINT }),
|
|
442
|
+
),
|
|
443
|
+
h('div', { style: FIELD },
|
|
444
|
+
h('label', { style: LABEL, htmlFor: 'dsh-map-tools-provider' }, '数据源'),
|
|
445
|
+
h('select', {
|
|
446
|
+
id: 'dsh-map-tools-provider',
|
|
447
|
+
value: draft.provider,
|
|
448
|
+
disabled: state.saving,
|
|
449
|
+
onChange: function (event) { merge({ draft: Object.assign({}, draft, { provider: event.target.value }) }) },
|
|
450
|
+
style: CONTROL,
|
|
451
|
+
}, PROVIDERS.map(function (provider) { return h('option', { key: provider.id, value: provider.id }, provider.label) })),
|
|
452
|
+
h('p', { style: CAPTION_STYLE }, '高德提供公交/POI/中文地址(需 key);免费 OSM 只兜底驾车/步行/骑行路线,中文地址解析不可靠。'),
|
|
453
|
+
),
|
|
454
|
+
h('div', { style: FIELD },
|
|
455
|
+
h('label', { style: LABEL, htmlFor: 'dsh-map-tools-key' }, '高德 key(Web 服务)'),
|
|
456
|
+
// 还没有 key 的人必须一眼看到"去哪拿":一句引导 + 独立成行的链接。
|
|
457
|
+
config.hasAmapKey ? null : h('p', { style: CAPTION_STYLE }, '还没有 key。到高德控制台创建一个「Web 服务」类型的 key(免费),再粘贴到下面。'),
|
|
458
|
+
h('p', { style: LINK_LINE }, link(AMAP_URL, config.hasAmapKey ? '获取 / 更换高德 Key →' : '获取高德 Key →')),
|
|
459
|
+
h('input', {
|
|
460
|
+
id: 'dsh-map-tools-key',
|
|
461
|
+
type: mask === null ? 'password' : 'text',
|
|
462
|
+
autoComplete: 'off',
|
|
463
|
+
value: draft.amapKey,
|
|
464
|
+
placeholder: config.hasAmapKey ? '已配置(留空即保持不变)' : '粘贴高德 Web 服务 key',
|
|
465
|
+
disabled: state.saving,
|
|
466
|
+
onChange: function (event) { merge({ draft: Object.assign({}, draft, { amapKey: event.target.value }) }) },
|
|
467
|
+
style: keyStyle,
|
|
468
|
+
}),
|
|
469
|
+
h('p', { style: CAPTION_STYLE }, 'key 只写进本机的 ' + (config.configPath || '~/.dsh-map-tools/config.json') + ',不回显、不进日志、不下发到页面。'),
|
|
470
|
+
),
|
|
471
|
+
h('div', { style: FIELD },
|
|
472
|
+
h('label', { style: LABEL, htmlFor: 'dsh-map-tools-timeout' }, '超时(毫秒)'),
|
|
473
|
+
h('input', {
|
|
474
|
+
id: 'dsh-map-tools-timeout',
|
|
475
|
+
inputMode: 'numeric',
|
|
476
|
+
value: draft.timeoutMs,
|
|
477
|
+
disabled: state.saving,
|
|
478
|
+
'aria-invalid': invalid ? 'true' : 'false',
|
|
479
|
+
onChange: function (event) { merge({ draft: Object.assign({}, draft, { timeoutMs: event.target.value }) }) },
|
|
480
|
+
style: invalid ? Object.assign({}, CONTROL, { borderColor: ERROR }) : CONTROL,
|
|
481
|
+
}),
|
|
482
|
+
h('p', { style: invalid ? Object.assign({}, HINT_STYLE, { color: ERROR }) : CAPTION_STYLE }, timeoutHint),
|
|
483
|
+
),
|
|
484
|
+
h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px', paddingTop: '12px', borderTop: SEPARATOR } },
|
|
485
|
+
button('打开配置文件', openFile, { disabled: state.saving }),
|
|
486
|
+
h('div', { style: { display: 'flex', alignItems: 'center', gap: '10px', minWidth: 0 } },
|
|
487
|
+
state.note === '' ? null : h('span', { role: 'status', style: { fontSize: '12px', lineHeight: 1.5, color: noteColor } }, state.note),
|
|
488
|
+
button(state.saving ? '保存中…' : '保存', save, { primary: true, disabled: blocked }),
|
|
489
|
+
),
|
|
490
|
+
),
|
|
491
|
+
)
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// ---- 路线卡片(方向 A / 第二档:自绘示意图,无底图、无外部网络) --------
|
|
496
|
+
//
|
|
497
|
+
// 数据来源:工具侧 output.presentationMeta 投影出的 tool/result.data.meta,
|
|
498
|
+
// 客户端通过 block.meta 读到(内置 Web 客户端不消费 presentResult)。
|
|
499
|
+
// 渲染入口:官方 keyed 槽位 tool.call.toolview,按线级工具名分派。
|
|
500
|
+
// 认领 key 即接管该工具卡片的全部形态,所以运行中/成功/失败/无几何
|
|
501
|
+
// 四种情况都在这里兜住。
|
|
502
|
+
|
|
503
|
+
var ROUTE_TOOLS = {
|
|
504
|
+
map_driving_route: { mode: 'car', label: '驾车' },
|
|
505
|
+
map_transit_route: { mode: 'bus', label: '公交' },
|
|
506
|
+
map_walking_route: { mode: 'walk', label: '步行' },
|
|
507
|
+
map_bicycling_route: { mode: 'ride', label: '骑行' },
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// 这一区的主题名同样必须**真实存在**:卡片底色 / 描边 / 链接色一度用的是
|
|
511
|
+
// 宿主里根本没有的别名,于是三者全部静默落到硬编码 fallback,深浅色不跟随。
|
|
512
|
+
// 配置卡那一份在 0.7.0 修过,这里是同一批名字的漏网。下面每个名字都对运行中的
|
|
513
|
+
// ui-theme 令牌表逐个复核过(DeepSeek Harness Desktop 0.1.7-rc.2)。
|
|
514
|
+
var TEXT = 'var(--dsw-alias-label-primary, inherit)'
|
|
515
|
+
var CARD_BG = 'var(--dsw-alias-bg-layer-1, rgba(127,127,127,0.06))'
|
|
516
|
+
var CARD_BORDER = 'var(--dsw-alias-border-l2, rgba(127,127,127,0.2))'
|
|
517
|
+
var MUTED = 'var(--dsw-alias-label-secondary, rgba(127,127,127,0.9))'
|
|
518
|
+
var ACCENT = 'var(--dsw-alias-link, #4f8cff)'
|
|
519
|
+
var START_COLOR = '#22a06b'
|
|
520
|
+
var END_COLOR = '#e05c5c'
|
|
521
|
+
var MAP_BG = 'rgba(127,127,127,0.10)'
|
|
522
|
+
/** 画布 viewBox 的单位宽度(只给自绘示意图用)。 */
|
|
523
|
+
var MAP_W = 100
|
|
524
|
+
/** viewBox 高度的宽松上下限(只挡病态输入,不做审美钳制)。 */
|
|
525
|
+
var MAP_H_MIN = 8
|
|
526
|
+
var MAP_H_MAX = 400
|
|
527
|
+
/**
|
|
528
|
+
* 真地图画框的显示比例(高 / 宽)区间。
|
|
529
|
+
*
|
|
530
|
+
* 真地图与自绘示意图的取舍不同:示意图的留白是"白框",所以画框要贴着
|
|
531
|
+
* 路线;真地图的留白是**真实地图内容**,画框给大一点,路线反而更清楚。
|
|
532
|
+
* 所以这里把路线比例钳到 [0.42, 0.68],画框宽度吃满卡片(用 CSS
|
|
533
|
+
* aspectRatio),整条路线由高德按请求尺寸自适应装进来。
|
|
534
|
+
*
|
|
535
|
+
* 下限 0.42 是特意抬高的:真实路线常常又长又扁(20 公里城市路线的高/宽
|
|
536
|
+
* 只有 0.3 左右),若按真实比例出图就是一条又矮又长的窄条,卡片里几乎
|
|
537
|
+
* 看不出是张地图。抬高下限会让画框变高——**路线本身的像素尺寸不变**
|
|
538
|
+
* (它受宽度限制),多出来的是上下两边的地图上下文。
|
|
539
|
+
*/
|
|
540
|
+
var MAP_ASPECT_MIN = 0.42
|
|
541
|
+
var MAP_ASPECT_MAX = 0.68
|
|
542
|
+
/** 请求给高德的图片宽度(2x 出图,缩小显示更清晰);高度按显示比例换算。 */
|
|
543
|
+
var MAP_REQUEST_W = 1024
|
|
544
|
+
var MAP_PAD = 7
|
|
545
|
+
/** 底纹网格间距(px)——只有自绘兜底图用得到。 */
|
|
546
|
+
var MAP_GRID_PX = 26
|
|
547
|
+
/**
|
|
548
|
+
* 静态地图 URL 的版本号:宿主给图片回的是 `max-age=86400`,浏览器会缓存
|
|
549
|
+
* 一整天。修掉"有底图、没路线"那个 bug 后必须换一次 URL,否则用户看到的
|
|
550
|
+
* 还是缓存里那张坏图。改动出图逻辑时把它 +1。
|
|
551
|
+
*/
|
|
552
|
+
var MAP_URL_VERSION = 2
|
|
553
|
+
/**
|
|
554
|
+
* 回合尾部最多逐条渲染几条路线。模型一轮里可能发多条路线调用(主路线 +
|
|
555
|
+
* 用于核对/备选的探测),每条都要有自己的地图才不会"张冠李戴";但也不能
|
|
556
|
+
* 无限堆,超过这个数就只显示前几条并注明总数。
|
|
557
|
+
*/
|
|
558
|
+
var MAX_TURN_ROUTES = 3
|
|
559
|
+
|
|
560
|
+
/** 运行中的调用没有 `kind` 字段;已结算的结果节点有。 */
|
|
561
|
+
function isRunning(block) {
|
|
562
|
+
return !(block && typeof block === 'object' && 'kind' in block)
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/** 解析调用参数(原始 JSON 串),失败返回 null。 */
|
|
566
|
+
function parseArgsRaw(raw) {
|
|
567
|
+
if (typeof raw !== 'string' || raw === '') return null
|
|
568
|
+
try {
|
|
569
|
+
var parsed = JSON.parse(raw)
|
|
570
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null
|
|
571
|
+
} catch (error) {
|
|
572
|
+
return null
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/** 解析调用参数(从 block 里取原始 JSON 串)。 */
|
|
577
|
+
function parseArgs(block) {
|
|
578
|
+
if (!block || typeof block !== 'object') return null
|
|
579
|
+
return parseArgsRaw(isRunning(block) ? block.argsRaw : (block.call && block.call.argsRaw))
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/** 参数派的摘要文本:"北京南站 → 首都机场"。 */
|
|
583
|
+
function summaryOf(fromText, toText) {
|
|
584
|
+
var from = typeof fromText === 'string' ? fromText : ''
|
|
585
|
+
var to = typeof toText === 'string' ? toText : ''
|
|
586
|
+
var text = from && to ? from + ' → ' + to : from || to
|
|
587
|
+
return text.length > 60 ? text.slice(0, 59) + '…' : text
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/** 参数派的摘要文本(从 block 里取,未做地名反查时的兜底)。 */
|
|
591
|
+
function callSummary(block) {
|
|
592
|
+
var args = parseArgs(block)
|
|
593
|
+
if (!args) return ''
|
|
594
|
+
return summaryOf(args.origin, args.destination)
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/** 已结算结果的文本内容(兜底卡片与错误态显示用)。 */
|
|
598
|
+
function flattenContent(block) {
|
|
599
|
+
if (!block || !Array.isArray(block.content)) return ''
|
|
600
|
+
var parts = []
|
|
601
|
+
for (var i = 0; i < block.content.length; i += 1) {
|
|
602
|
+
var part = block.content[i]
|
|
603
|
+
if (part && part.type === 'text' && typeof part.text === 'string') parts.push(part.text)
|
|
604
|
+
}
|
|
605
|
+
return parts.join('\n')
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/** 失败原因的短文本。 */
|
|
609
|
+
function errorText(block) {
|
|
610
|
+
var text = flattenContent(block)
|
|
611
|
+
if (text) return text
|
|
612
|
+
if (block && block.error && typeof block.error === 'object') {
|
|
613
|
+
var name = typeof block.error.name === 'string' ? block.error.name : ''
|
|
614
|
+
var code = typeof block.error.code === 'string' ? block.error.code : ''
|
|
615
|
+
if (name || code) return (name + (code ? ': ' + code : '')) || '调用失败'
|
|
616
|
+
}
|
|
617
|
+
return '调用失败'
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* 窄化持久化的卡片元数据。历史日志、手工改过的日志都会到这里,
|
|
622
|
+
* 任何不符预期都返回 null(由调用方退回文本卡片),不抛错。
|
|
623
|
+
*/
|
|
624
|
+
function routeMeta(meta) {
|
|
625
|
+
if (!meta || typeof meta !== 'object' || Array.isArray(meta)) return null
|
|
626
|
+
if (meta.kind !== 'route' || meta.v !== 1) return null
|
|
627
|
+
var line = []
|
|
628
|
+
if (Array.isArray(meta.line)) {
|
|
629
|
+
for (var i = 0; i < meta.line.length; i += 1) {
|
|
630
|
+
var point = meta.line[i]
|
|
631
|
+
if (!Array.isArray(point) || point.length < 2) continue
|
|
632
|
+
var lng = Number(point[0])
|
|
633
|
+
var lat = Number(point[1])
|
|
634
|
+
if (!isFinite(lng) || !isFinite(lat)) continue
|
|
635
|
+
line.push([lng, lat])
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return {
|
|
639
|
+
provider: meta.provider === 'osrm' ? 'OSRM' : '高德',
|
|
640
|
+
distanceM: typeof meta.distanceM === 'number' && isFinite(meta.distanceM) ? meta.distanceM : 0,
|
|
641
|
+
durationS: typeof meta.durationS === 'number' && isFinite(meta.durationS) ? meta.durationS : 0,
|
|
642
|
+
stepCount: typeof meta.stepCount === 'number' && isFinite(meta.stepCount) ? meta.stepCount : 0,
|
|
643
|
+
alternatives: typeof meta.alternatives === 'number' && isFinite(meta.alternatives) ? meta.alternatives : 0,
|
|
644
|
+
// 起点/终点的可读名称(坐标入参时由宿主反查得到)。
|
|
645
|
+
fromName: typeof meta.fromName === 'string' ? meta.fromName : '',
|
|
646
|
+
toName: typeof meta.toName === 'string' ? meta.toName : '',
|
|
647
|
+
line: line,
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/** 一维取整到 0.1(压缩 SVG 里的数字串)。 */
|
|
652
|
+
function round1(value) {
|
|
653
|
+
return Math.round(value * 10) / 10
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* 端点标签的摆放:靠近右边缘就放到点的左侧,靠近下边缘就放到点的上方,
|
|
658
|
+
* 免得标签被画布裁掉。纯函数,可单测。
|
|
659
|
+
* @param point - 端点屏幕坐标。
|
|
660
|
+
* @param width - 画布宽。
|
|
661
|
+
* @param height - 画布高。
|
|
662
|
+
* @param fontSize - 字号。
|
|
663
|
+
* @returns 标签的 x / y 与 textAnchor。
|
|
664
|
+
*/
|
|
665
|
+
function labelPlacement(point, width, height, fontSize) {
|
|
666
|
+
var flipX = point[0] > width * 0.72
|
|
667
|
+
var flipY = point[1] > height - fontSize * 1.2
|
|
668
|
+
return {
|
|
669
|
+
x: round1(flipX ? point[0] - 3 : point[0] + 3),
|
|
670
|
+
y: round1(flipY ? point[1] - 2.5 : point[1] + fontSize * 0.95),
|
|
671
|
+
anchor: flipX ? 'end' : 'start',
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* 把经纬度路线投到 SVG 画布上。
|
|
677
|
+
*
|
|
678
|
+
* 等距近似(x 按纬度 cos 收缩)→ 等比缩放居中,保证路线不变形。
|
|
679
|
+
* **画布高度按路线长宽比推算**(钳制在 MAP_H_MIN..MAP_H_MAX),返回的
|
|
680
|
+
* `viewBox` 因此与路线形状同比例:组件只要设 `width:100%; height:auto`,
|
|
681
|
+
* 地图就不会是一大片空白里的一条细线。
|
|
682
|
+
*
|
|
683
|
+
* 纯函数,可单测。
|
|
684
|
+
*/
|
|
685
|
+
function projectLine(line) {
|
|
686
|
+
var n = line.length
|
|
687
|
+
if (n === 0) return null
|
|
688
|
+
var latSum = 0
|
|
689
|
+
for (var i = 0; i < n; i += 1) latSum += line[i][1]
|
|
690
|
+
var scaleX = Math.cos(((latSum / n) * Math.PI) / 180)
|
|
691
|
+
if (!(scaleX > 0.01)) scaleX = 0.01
|
|
692
|
+
var xs = []
|
|
693
|
+
var ys = []
|
|
694
|
+
for (var j = 0; j < n; j += 1) {
|
|
695
|
+
xs.push(line[j][0] * scaleX)
|
|
696
|
+
ys.push(-line[j][1])
|
|
697
|
+
}
|
|
698
|
+
var minX = Math.min.apply(null, xs)
|
|
699
|
+
var maxX = Math.max.apply(null, xs)
|
|
700
|
+
var minY = Math.min.apply(null, ys)
|
|
701
|
+
var maxY = Math.max.apply(null, ys)
|
|
702
|
+
var dx = maxX - minX
|
|
703
|
+
var dy = maxY - minY
|
|
704
|
+
if (dx < 1e-12 && dy < 1e-12) {
|
|
705
|
+
// 所有点重合:给一个最小画布,中心一个点,避免除零。
|
|
706
|
+
var mid = [MAP_W / 2, MAP_H_MIN / 2]
|
|
707
|
+
return {
|
|
708
|
+
viewBox: '0 0 ' + MAP_W + ' ' + MAP_H_MIN,
|
|
709
|
+
width: MAP_W,
|
|
710
|
+
height: MAP_H_MIN,
|
|
711
|
+
points: mid[0] + ',' + mid[1],
|
|
712
|
+
start: mid,
|
|
713
|
+
end: mid,
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
// viewBox 比例 = 路线外包框比例(不做审美钳制,否则路线会在框内
|
|
717
|
+
// 再被 letterbox 一次,画出来就是"大空框里一条线")。
|
|
718
|
+
var height = dx < 1e-12 ? MAP_H_MAX : MAP_W * (dy / dx)
|
|
719
|
+
if (!isFinite(height) || height < MAP_H_MIN) height = MAP_H_MIN
|
|
720
|
+
if (height > MAP_H_MAX) height = MAP_H_MAX
|
|
721
|
+
var innerW = Math.max(1, MAP_W - 2 * MAP_PAD)
|
|
722
|
+
var innerH = Math.max(1, height - 2 * MAP_PAD)
|
|
723
|
+
var scale = Math.min(
|
|
724
|
+
dx < 1e-12 ? Infinity : innerW / dx,
|
|
725
|
+
dy < 1e-12 ? Infinity : innerH / dy,
|
|
726
|
+
)
|
|
727
|
+
if (!isFinite(scale)) scale = 1
|
|
728
|
+
var offsetX = (MAP_W - dx * scale) / 2
|
|
729
|
+
var offsetY = (height - dy * scale) / 2
|
|
730
|
+
var projected = []
|
|
731
|
+
for (var k = 0; k < n; k += 1) {
|
|
732
|
+
projected.push([
|
|
733
|
+
round1((xs[k] - minX) * scale + offsetX),
|
|
734
|
+
round1((ys[k] - minY) * scale + offsetY),
|
|
735
|
+
])
|
|
736
|
+
}
|
|
737
|
+
var text = []
|
|
738
|
+
for (var m = 0; m < projected.length; m += 1) text.push(projected[m][0] + ',' + projected[m][1])
|
|
739
|
+
return {
|
|
740
|
+
viewBox: '0 0 ' + MAP_W + ' ' + height,
|
|
741
|
+
width: MAP_W,
|
|
742
|
+
height: height,
|
|
743
|
+
aspect: height / MAP_W,
|
|
744
|
+
points: text.join(' '),
|
|
745
|
+
start: projected[0],
|
|
746
|
+
end: projected[projected.length - 1],
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* 真地图画框:把路线比例钳到 MAP_ASPECT_MIN..MAP_ASPECT_MAX(**高 / 宽**)
|
|
752
|
+
* 后,换算出 CSS `aspect-ratio`(**宽 / 高**)与要请求的图片尺寸。
|
|
753
|
+
*
|
|
754
|
+
* 宽度交给 CSS(100%),高度由 aspect-ratio 推出来,所以不需要知道卡片
|
|
755
|
+
* 实际宽度;图片与画框比例天然一致(既不裁掉两端,也不留黑边)。
|
|
756
|
+
* 真地图的留白是**真实地图内容**,所以画框给大一点、路线反而更清楚。
|
|
757
|
+
*
|
|
758
|
+
* @param routeAspect - 路线外包框的高 / 宽(`projectLine().aspect`)。
|
|
759
|
+
* @returns `cssAspect` 是 CSS 用的宽高比,`requestW/H` 是要高德出图的像素。
|
|
760
|
+
*/
|
|
761
|
+
function mapFrame(routeAspect) {
|
|
762
|
+
var aspect = Number(routeAspect)
|
|
763
|
+
if (!isFinite(aspect) || aspect <= 0) aspect = MAP_ASPECT_MIN
|
|
764
|
+
if (aspect < MAP_ASPECT_MIN) aspect = MAP_ASPECT_MIN
|
|
765
|
+
if (aspect > MAP_ASPECT_MAX) aspect = MAP_ASPECT_MAX
|
|
766
|
+
var requestW = MAP_REQUEST_W
|
|
767
|
+
var requestH = Math.max(64, Math.min(1024, Math.round(requestW * aspect)))
|
|
768
|
+
return { cssAspect: requestW / requestH, requestW: requestW, requestH: requestH }
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/** 等间隔抽稀(客户端侧),用于把折线塞进静态地图的 URL。 */
|
|
772
|
+
function decimateLine(line, maxPoints) {
|
|
773
|
+
if (!Array.isArray(line) || line.length <= maxPoints) return Array.isArray(line) ? line : []
|
|
774
|
+
var out = []
|
|
775
|
+
var step = (line.length - 1) / (maxPoints - 1)
|
|
776
|
+
for (var i = 0; i < maxPoints; i += 1) out.push(line[Math.round(i * step)])
|
|
777
|
+
return out
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* 向宿主回环路由要静态地图的 URL。
|
|
782
|
+
*
|
|
783
|
+
* 真地图(含高德绘制的路线与起终点标注)由**宿主**去取——key 不出服务端,
|
|
784
|
+
* 浏览器只加载一张本地图片。宿主没配 key、配额超限、或老版本没有这个
|
|
785
|
+
* 路由时,图片加载失败 → 卡片自动退回自绘示意图(见 RouteBody)。
|
|
786
|
+
*/
|
|
787
|
+
function staticMapUrl(line, frame) {
|
|
788
|
+
var compact = decimateLine(line, 120).map(function (p) { return p[0] + ',' + p[1] }).join(';')
|
|
789
|
+
return '/dsh-map-tools/staticmap?w=' + frame.requestW + '&h=' + frame.requestH
|
|
790
|
+
+ '&v=' + MAP_URL_VERSION
|
|
791
|
+
+ '&line=' + encodeURIComponent(compact)
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/** 距离文案:>=1 公里用公里,否则用米。 */
|
|
795
|
+
function formatDistance(meters) {
|
|
796
|
+
if (!(meters > 0)) return ''
|
|
797
|
+
return meters >= 1000 ? (meters / 1000).toFixed(1) + ' 公里' : Math.round(meters) + ' 米'
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/** 耗时文案;为 0 时返回空串(高德公交不返回耗时)。 */
|
|
801
|
+
function formatDuration(seconds) {
|
|
802
|
+
if (!(seconds > 0)) return ''
|
|
803
|
+
var mins = Math.round(seconds / 60)
|
|
804
|
+
if (mins < 60) return '约 ' + mins + ' 分钟'
|
|
805
|
+
return '约 ' + Math.floor(mins / 60) + ' 小时 ' + (mins % 60) + ' 分钟'
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* 高德 URI 深链(官方 uri.amap.com 协议)。
|
|
810
|
+
* callnative=0 让它走网页版,避免在浏览器里尝试唤起客户端。
|
|
811
|
+
* ⚠️ 未经实机验证——首版测试时请人工点一次确认。
|
|
812
|
+
*/
|
|
813
|
+
function amapUri(mode, line, fromText, toText) {
|
|
814
|
+
if (!Array.isArray(line) || line.length < 2) return null
|
|
815
|
+
var from = line[0]
|
|
816
|
+
var to = line[line.length - 1]
|
|
817
|
+
var params = [
|
|
818
|
+
'from=' + from[0] + ',' + from[1] + ',' + encodeURIComponent(fromText || '起点'),
|
|
819
|
+
'to=' + to[0] + ',' + to[1] + ',' + encodeURIComponent(toText || '终点'),
|
|
820
|
+
'mode=' + (mode || 'car'),
|
|
821
|
+
'coordinate=gaode',
|
|
822
|
+
'callnative=0',
|
|
823
|
+
'src=dsh-map-tools',
|
|
824
|
+
]
|
|
825
|
+
return 'https://uri.amap.com/navigation?' + params.join('&')
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function routeCardModel(block, toolName) {
|
|
829
|
+
var tool = ROUTE_TOOLS[toolName] || { mode: 'car', label: '路线' }
|
|
830
|
+
var args = parseArgs(block)
|
|
831
|
+
var running = isRunning(block)
|
|
832
|
+
var failed = !running && block.isError === true
|
|
833
|
+
var meta = running ? null : routeMeta(block && block.meta)
|
|
834
|
+
var line = meta && meta.line && meta.line.length >= 2 ? meta.line : null
|
|
835
|
+
// 名称优先用 meta 里的(坐标入参时宿主反查出来的地名),其次才用参数原文。
|
|
836
|
+
var fromText = (meta && meta.fromName) || (args && typeof args.origin === 'string' ? args.origin : '')
|
|
837
|
+
var toText = (meta && meta.toName) || (args && typeof args.destination === 'string' ? args.destination : '')
|
|
838
|
+
return {
|
|
839
|
+
label: tool.label,
|
|
840
|
+
mode: tool.mode,
|
|
841
|
+
summary: summaryOf(fromText, toText),
|
|
842
|
+
fromText: fromText,
|
|
843
|
+
toText: toText,
|
|
844
|
+
running: running,
|
|
845
|
+
failed: failed,
|
|
846
|
+
meta: meta,
|
|
847
|
+
line: line,
|
|
848
|
+
// 模型可见的结果文本:画了图之后仍然显示(可滚动),
|
|
849
|
+
// 这样卡片不会把"分段指引"这份信息藏起来。
|
|
850
|
+
resultText: running || failed ? '' : flattenContent(block),
|
|
851
|
+
errorText: failed ? errorText(block) : '',
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* 用"回合尾部"折叠出来的数据构造同一套卡片模型。
|
|
857
|
+
*
|
|
858
|
+
* 回合尾部只有(工具名 / 调用参数 / 持久化 meta),没有 ToolCallBlock;
|
|
859
|
+
* 这里把它归一成与 routeCardModel 相同的形状,好让 RouteBody 复用。
|
|
860
|
+
* @param entry - 折叠定义里记录的一条路线调用。
|
|
861
|
+
* @returns 卡片模型;meta 不可用时返回 null。
|
|
862
|
+
*/
|
|
863
|
+
function turnRouteModel(entry) {
|
|
864
|
+
if (!entry) return null
|
|
865
|
+
var meta = routeMeta(entry.meta)
|
|
866
|
+
if (!meta) return null
|
|
867
|
+
var tool = ROUTE_TOOLS[entry.toolName] || { mode: 'car', label: '路线' }
|
|
868
|
+
var args = parseArgsRaw(entry.argsRaw)
|
|
869
|
+
var fromText = meta.fromName || (args && typeof args.origin === 'string' ? args.origin : '')
|
|
870
|
+
var toText = meta.toName || (args && typeof args.destination === 'string' ? args.destination : '')
|
|
871
|
+
return {
|
|
872
|
+
label: tool.label,
|
|
873
|
+
mode: tool.mode,
|
|
874
|
+
summary: summaryOf(fromText, toText),
|
|
875
|
+
fromText: fromText,
|
|
876
|
+
toText: toText,
|
|
877
|
+
running: false,
|
|
878
|
+
failed: false,
|
|
879
|
+
meta: meta,
|
|
880
|
+
line: meta.line && meta.line.length >= 2 ? meta.line : null,
|
|
881
|
+
// 回合尾部拿不到模型可见文本(那是工具结果的 content);这里不重复显示。
|
|
882
|
+
resultText: '',
|
|
883
|
+
errorText: '',
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* 自绘示意图(无底图):静态地图拿不到时的兜底,也是"没有 key"的用户
|
|
889
|
+
* 唯一能看到的图形。纯 SVG,不请求任何外部资源。
|
|
890
|
+
*/
|
|
891
|
+
function sketchSvg(h, projected) {
|
|
892
|
+
var startLabel = labelPlacement(projected.start, projected.width, projected.height, 7)
|
|
893
|
+
var endLabel = labelPlacement(projected.end, projected.width, projected.height, 7)
|
|
894
|
+
// 先画一条更粗的半透明底线当"光晕",路线在浅色/深色主题下都清晰。
|
|
895
|
+
var shapes = [
|
|
896
|
+
h('polyline', {
|
|
897
|
+
key: 'halo',
|
|
898
|
+
points: projected.points,
|
|
899
|
+
fill: 'none',
|
|
900
|
+
stroke: ACCENT,
|
|
901
|
+
strokeOpacity: 0.22,
|
|
902
|
+
strokeWidth: 3.2,
|
|
903
|
+
strokeLinejoin: 'round',
|
|
904
|
+
strokeLinecap: 'round',
|
|
905
|
+
}),
|
|
906
|
+
h('polyline', {
|
|
907
|
+
key: 'line',
|
|
908
|
+
points: projected.points,
|
|
909
|
+
fill: 'none',
|
|
910
|
+
stroke: ACCENT,
|
|
911
|
+
strokeWidth: 1.3,
|
|
912
|
+
strokeLinejoin: 'round',
|
|
913
|
+
strokeLinecap: 'round',
|
|
914
|
+
}),
|
|
915
|
+
h('circle', { key: 'start', cx: projected.start[0], cy: projected.start[1], r: 2, fill: START_COLOR }),
|
|
916
|
+
h('circle', { key: 'end', cx: projected.end[0], cy: projected.end[1], r: 2, fill: END_COLOR }),
|
|
917
|
+
h('text', {
|
|
918
|
+
key: 'startLabel',
|
|
919
|
+
x: startLabel.x,
|
|
920
|
+
y: startLabel.y,
|
|
921
|
+
textAnchor: startLabel.anchor,
|
|
922
|
+
fontSize: 7,
|
|
923
|
+
fill: START_COLOR,
|
|
924
|
+
}, '起'),
|
|
925
|
+
h('text', {
|
|
926
|
+
key: 'endLabel',
|
|
927
|
+
x: endLabel.x,
|
|
928
|
+
y: endLabel.y,
|
|
929
|
+
textAnchor: endLabel.anchor,
|
|
930
|
+
fontSize: 7,
|
|
931
|
+
fill: END_COLOR,
|
|
932
|
+
}, '终'),
|
|
933
|
+
]
|
|
934
|
+
return h('svg', {
|
|
935
|
+
viewBox: projected.viewBox,
|
|
936
|
+
preserveAspectRatio: 'xMidYMid meet',
|
|
937
|
+
style: { display: 'block', width: '100%', height: '100%' },
|
|
938
|
+
role: 'img',
|
|
939
|
+
'aria-label': '路线示意图(非真实底图,仅示意走向)',
|
|
940
|
+
}, shapes)
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/**
|
|
944
|
+
* 卡片主体:地图/示意图 + 数据行 + 折叠的路书 + 深链。
|
|
945
|
+
*
|
|
946
|
+
* @param h - createElement。
|
|
947
|
+
* @param model - 卡片模型。
|
|
948
|
+
* @param opts.withMap - 是否渲染地图。过程区("思考"块)传 false:
|
|
949
|
+
* 工具卡片会随过程反复重绘,在那里再拉一张大图既没必要、也和
|
|
950
|
+
* 回合尾部的卡片重复;地图只在"最终结果"处出现。
|
|
951
|
+
*/
|
|
952
|
+
function RouteBody(h, model, opts) {
|
|
953
|
+
var withMap = !opts || opts.withMap !== false
|
|
954
|
+
var rows = []
|
|
955
|
+
var distance = formatDistance(model.meta.distanceM)
|
|
956
|
+
var duration = formatDuration(model.meta.durationS)
|
|
957
|
+
var stats = [distance, duration, model.meta.provider].filter(Boolean).join(' · ')
|
|
958
|
+
if (model.meta.stepCount > 0) stats += ' · 共 ' + model.meta.stepCount + ' 步'
|
|
959
|
+
if (model.meta.alternatives > 0) stats += ' · 另有 ' + model.meta.alternatives + ' 条备选'
|
|
960
|
+
|
|
961
|
+
if (withMap && model.line) {
|
|
962
|
+
var projected = projectLine(model.line)
|
|
963
|
+
if (projected) {
|
|
964
|
+
// 真地图画框:宽度吃满卡片(CSS aspectRatio 推高度),整条路线由
|
|
965
|
+
// 高德按请求尺寸自适应装进来——比"按路线比例做小缩略图"大得多。
|
|
966
|
+
var frame = mapFrame(projected.aspect)
|
|
967
|
+
var content = model.imageFailed
|
|
968
|
+
? sketchSvg(h, projected)
|
|
969
|
+
: h('img', {
|
|
970
|
+
src: staticMapUrl(model.line, frame),
|
|
971
|
+
alt: '路线地图',
|
|
972
|
+
onError: model.onImageError,
|
|
973
|
+
style: { display: 'block', width: '100%', height: '100%', objectFit: 'cover' },
|
|
974
|
+
})
|
|
975
|
+
rows.push(h('div', {
|
|
976
|
+
key: 'map',
|
|
977
|
+
style: {
|
|
978
|
+
margin: '0 0 8px',
|
|
979
|
+
borderRadius: '6px',
|
|
980
|
+
overflow: 'hidden',
|
|
981
|
+
border: '1px solid ' + CARD_BORDER,
|
|
982
|
+
// 真地图自带底图;自绘兜底图铺细网格,免得像"空框"。
|
|
983
|
+
background: model.imageFailed
|
|
984
|
+
? 'repeating-linear-gradient(0deg, rgba(127,127,127,0.13) 0 1px, transparent 1px '
|
|
985
|
+
+ MAP_GRID_PX + 'px), repeating-linear-gradient(90deg, rgba(127,127,127,0.13) 0 1px, transparent 1px '
|
|
986
|
+
+ MAP_GRID_PX + 'px), ' + MAP_BG
|
|
987
|
+
: MAP_BG,
|
|
988
|
+
width: '100%',
|
|
989
|
+
// 与请求给高德的图片同比例 ⇒ 既不裁掉两端,也不留黑边。
|
|
990
|
+
aspectRatio: String(frame.cssAspect),
|
|
991
|
+
},
|
|
992
|
+
}, content))
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
if (stats) rows.push(h('div', { key: 'stats', style: { color: MUTED, fontSize: '12px' } }, stats))
|
|
997
|
+
|
|
998
|
+
if (model.resultText) {
|
|
999
|
+
// 路书默认折叠:以前它是个占半屏、带独立滚动条的大框,
|
|
1000
|
+
// 把卡片主体(图)挤成了配角。
|
|
1001
|
+
var lines = model.resultText.split('\n')
|
|
1002
|
+
var expanded = model.expanded === true
|
|
1003
|
+
var preview = expanded ? model.resultText : lines.slice(0, 3).join('\n')
|
|
1004
|
+
var hiddenCount = Math.max(0, lines.length - 3)
|
|
1005
|
+
rows.push(h('pre', {
|
|
1006
|
+
key: 'text',
|
|
1007
|
+
style: {
|
|
1008
|
+
margin: '6px 0 0',
|
|
1009
|
+
padding: '6px 8px',
|
|
1010
|
+
maxHeight: expanded ? '320px' : 'none',
|
|
1011
|
+
overflow: expanded ? 'auto' : 'hidden',
|
|
1012
|
+
whiteSpace: 'pre-wrap',
|
|
1013
|
+
borderRadius: '6px',
|
|
1014
|
+
background: 'rgba(127,127,127,0.08)',
|
|
1015
|
+
fontSize: '12px',
|
|
1016
|
+
lineHeight: '1.5',
|
|
1017
|
+
color: MUTED,
|
|
1018
|
+
},
|
|
1019
|
+
}, preview))
|
|
1020
|
+
if (hiddenCount > 0 || expanded) {
|
|
1021
|
+
rows.push(h('button', {
|
|
1022
|
+
key: 'toggle',
|
|
1023
|
+
type: 'button',
|
|
1024
|
+
onClick: model.onToggle,
|
|
1025
|
+
style: {
|
|
1026
|
+
marginTop: '4px',
|
|
1027
|
+
padding: '2px 8px',
|
|
1028
|
+
border: '1px solid ' + CARD_BORDER,
|
|
1029
|
+
borderRadius: '5px',
|
|
1030
|
+
background: 'none',
|
|
1031
|
+
color: MUTED,
|
|
1032
|
+
fontSize: '12px',
|
|
1033
|
+
cursor: 'pointer',
|
|
1034
|
+
},
|
|
1035
|
+
}, expanded ? '收起分段指引' : '展开分段指引(还有 ' + hiddenCount + ' 行)'))
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// 署名与「在高德打开 ↗」**同一行**:它接在深链后面,而不是另起一行压在卡片
|
|
1040
|
+
// 底部,所以两者共用一个 flex 行、按基线对齐(窄卡片上再换行)。
|
|
1041
|
+
var uri = amapUri(model.mode, model.line, model.fromText, model.toText)
|
|
1042
|
+
var withCredit = !!(opts && opts.credit === true)
|
|
1043
|
+
if (uri || withCredit) {
|
|
1044
|
+
var linkRow = []
|
|
1045
|
+
if (uri) {
|
|
1046
|
+
// 字号不在这里定:整行统一 12px(与上面的统计行同号),深链只负责颜色,
|
|
1047
|
+
// 否则深链 12px、署名继承外层更大的字号,同一行会明显不等大。
|
|
1048
|
+
linkRow.push(h('a', {
|
|
1049
|
+
key: 'amap',
|
|
1050
|
+
href: uri,
|
|
1051
|
+
target: '_blank',
|
|
1052
|
+
rel: 'noreferrer',
|
|
1053
|
+
style: { color: ACCENT, textDecoration: 'none' },
|
|
1054
|
+
}, '在高德打开 ↗'))
|
|
1055
|
+
}
|
|
1056
|
+
// 没有几何(老日志 / 高德公交无折线)时深链是空的,署名照样在。
|
|
1057
|
+
if (withCredit) linkRow = linkRow.concat(creditNodes(h))
|
|
1058
|
+
rows.push(h('div', {
|
|
1059
|
+
key: 'link',
|
|
1060
|
+
style: {
|
|
1061
|
+
marginTop: '8px',
|
|
1062
|
+
display: 'flex',
|
|
1063
|
+
alignItems: 'baseline',
|
|
1064
|
+
flexWrap: 'wrap',
|
|
1065
|
+
gap: '8px',
|
|
1066
|
+
// 这一行的唯一字号来源:深链与署名同号,差别只在颜色 / 字重。
|
|
1067
|
+
fontSize: '12px',
|
|
1068
|
+
lineHeight: '18px',
|
|
1069
|
+
color: MUTED,
|
|
1070
|
+
},
|
|
1071
|
+
}, linkRow))
|
|
1072
|
+
}
|
|
1073
|
+
return rows
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
/** 卡片组件:认领 key 后要覆盖运行中/成功/失败/无几何全部形态。 */
|
|
1077
|
+
function RouteCard(props) {
|
|
1078
|
+
var react = require('react')
|
|
1079
|
+
var h = react.createElement
|
|
1080
|
+
var state = useCardState(react)
|
|
1081
|
+
var model = applyCardState(routeCardModel(props.block, props.toolName), state)
|
|
1082
|
+
var head = [model.label]
|
|
1083
|
+
if (model.summary) head.push(model.summary)
|
|
1084
|
+
var children = [
|
|
1085
|
+
h('div', {
|
|
1086
|
+
key: 'head',
|
|
1087
|
+
style: { fontSize: '12px', color: MUTED, marginBottom: '6px' },
|
|
1088
|
+
}, head.join(' · ')),
|
|
1089
|
+
]
|
|
1090
|
+
if (model.running) {
|
|
1091
|
+
children.push(h('div', { key: 'running', style: { fontSize: '12px', color: MUTED } }, '规划中…'))
|
|
1092
|
+
} else if (model.failed) {
|
|
1093
|
+
children.push(h('pre', {
|
|
1094
|
+
key: 'error',
|
|
1095
|
+
style: { margin: 0, whiteSpace: 'pre-wrap', fontSize: '12px', color: '#e05c5c' },
|
|
1096
|
+
}, model.errorText))
|
|
1097
|
+
} else if (model.line || model.meta) {
|
|
1098
|
+
// 过程区(工具卡片)**不渲染地图**:这里会随过程反复重绘,而且地图
|
|
1099
|
+
// 已经在回合尾部(最终结果处)给过了。过程区只做紧凑摘要 + 路书。
|
|
1100
|
+
children.push(h('div', { key: 'body' }, RouteBody(h, model, { withMap: false })))
|
|
1101
|
+
} else {
|
|
1102
|
+
children.push(h('pre', {
|
|
1103
|
+
key: 'plain',
|
|
1104
|
+
style: { margin: 0, whiteSpace: 'pre-wrap', fontSize: '12px', color: MUTED },
|
|
1105
|
+
}, model.resultText || '(无卡片数据)'))
|
|
1106
|
+
}
|
|
1107
|
+
return h('div', {
|
|
1108
|
+
style: {
|
|
1109
|
+
padding: '8px 10px',
|
|
1110
|
+
borderRadius: '8px',
|
|
1111
|
+
border: '1px solid ' + CARD_BORDER,
|
|
1112
|
+
background: CARD_BG,
|
|
1113
|
+
},
|
|
1114
|
+
}, children)
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/** 注册四个路线工具的卡片;槽位声明未就绪时 inject 会排队等待。 */
|
|
1118
|
+
function registerRouteCards(scope) {
|
|
1119
|
+
if (!scope || !scope.slots || typeof scope.slots.inject !== 'function') return
|
|
1120
|
+
scope.slots.inject('tool.call.toolview', function* () {
|
|
1121
|
+
var names = Object.keys(ROUTE_TOOLS)
|
|
1122
|
+
for (var i = 0; i < names.length; i += 1) {
|
|
1123
|
+
yield scope.slots.register({ name: 'tool.call.toolview', key: names[i] }, RouteCard)
|
|
1124
|
+
}
|
|
1125
|
+
})
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// ---- 回合尾部("最终结果"处)的路线卡片 ------------------------------
|
|
1129
|
+
//
|
|
1130
|
+
// 工具卡片渲染在回合的**过程区**("思考"块里,随时可能被折叠),所以只看
|
|
1131
|
+
// 最终答案的人看不到地图。这里按 ui-deliverables 的做法自己折叠本轮的
|
|
1132
|
+
// 路线结果(注册一个 conversation node definition),再注册到回合尾部的
|
|
1133
|
+
// 槽位 conversation.chat.turnTail —— 地图就会出现在收尾正文之后。
|
|
1134
|
+
//
|
|
1135
|
+
// 该槽位的**类型随宿主版本变过**(见 docs/dsh-map-tools-0.1.6兼容性说明.md):
|
|
1136
|
+
// · ≤ 0.1.6-alpha.1:kind 'chain' —— 必填 `select`,组件从 props.matched 取数;
|
|
1137
|
+
// · ≥ 0.1.6-alpha.2:kind 'list' —— 必填 `id`,不注入 matched,组件自推导。
|
|
1138
|
+
// 所以下面读槽位声明决定注册形状,一份 bundle 同时吃两种宿主。
|
|
1139
|
+
|
|
1140
|
+
/** 本轮路线的 Turn 级数据键(Definition 与选择器共用)。 */
|
|
1141
|
+
var ROUTE_TURN_KEY = 'map-routes'
|
|
1142
|
+
|
|
1143
|
+
/** 回合尾部槽位名(0.1.6-alpha.2 起由链式改为列表式)。 */
|
|
1144
|
+
var TURN_TAIL_SLOT = 'conversation.chat.turnTail'
|
|
1145
|
+
|
|
1146
|
+
/** 列表式槽位必填的注册 id:稳定、全局唯一;链式宿主上被忽略。 */
|
|
1147
|
+
var TURN_TAIL_ID = 'dsh-map-tools-turn-routes'
|
|
1148
|
+
|
|
1149
|
+
/** 线级工具名是否是我们的路线工具。 */
|
|
1150
|
+
function isRouteTool(name) {
|
|
1151
|
+
return typeof name === 'string' && Object.prototype.hasOwnProperty.call(ROUTE_TOOLS, name)
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
/**
|
|
1155
|
+
* 折叠"本轮成功的路线调用":tool/call 记名字与参数,tool/result 里带
|
|
1156
|
+
* `meta.kind === 'route'` 且不是失败结果时落一条。
|
|
1157
|
+
* 只发布数据、不产生视图节点(没有 target),与 ui-deliverables 同构。
|
|
1158
|
+
*/
|
|
1159
|
+
function routeTurnDefinition() {
|
|
1160
|
+
return {
|
|
1161
|
+
kind: ROUTE_TURN_KEY,
|
|
1162
|
+
match: function (event) {
|
|
1163
|
+
if (!event || typeof event !== 'object') return null
|
|
1164
|
+
if (event.type === 'turn/start') return { id: String(event.data.turn), role: 'start' }
|
|
1165
|
+
if (event.type === 'tool/call') return { id: String(event.data.turn), role: 'update' }
|
|
1166
|
+
if (event.type === 'tool/result') return { id: String(event.data.turn), role: 'update' }
|
|
1167
|
+
// 只累加"带 turn 的事件":user/message 没有 turn 字段(官方定义用
|
|
1168
|
+
// message id 另起一个节点),所以回合级累加器拿不到用户原话——这也是
|
|
1169
|
+
// 放弃"猜哪条是主路线"、改为把本轮所有路线都画出来的原因。
|
|
1170
|
+
return null
|
|
1171
|
+
},
|
|
1172
|
+
start: function (context, match) {
|
|
1173
|
+
return { turn: match.event.data.turn, calls: new Map(), routes: [] }
|
|
1174
|
+
},
|
|
1175
|
+
update: function (context, match) {
|
|
1176
|
+
var event = match.event
|
|
1177
|
+
var state = context.state
|
|
1178
|
+
if (event.type === 'tool/call') {
|
|
1179
|
+
var calls = new Map(state.calls)
|
|
1180
|
+
calls.set(String(event.data.callId), { name: event.data.name, argsRaw: event.data.arguments })
|
|
1181
|
+
return { turn: state.turn, calls: calls, routes: state.routes }
|
|
1182
|
+
}
|
|
1183
|
+
if (event.type !== 'tool/result') return state
|
|
1184
|
+
var message = event.data && event.data.message
|
|
1185
|
+
if (!message || message.isError === true) return state
|
|
1186
|
+
var meta = event.data.meta
|
|
1187
|
+
if (!meta || meta.kind !== 'route' || meta.v !== 1) return state
|
|
1188
|
+
var callId = String(message.source && message.source.callId)
|
|
1189
|
+
var call = state.calls.get(callId)
|
|
1190
|
+
if (!call || !isRouteTool(call.name)) return state
|
|
1191
|
+
var routes = state.routes.concat([{
|
|
1192
|
+
callId: callId,
|
|
1193
|
+
toolName: call.name,
|
|
1194
|
+
argsRaw: call.argsRaw,
|
|
1195
|
+
meta: meta,
|
|
1196
|
+
seq: event.seq,
|
|
1197
|
+
}])
|
|
1198
|
+
return { turn: state.turn, calls: state.calls, routes: routes }
|
|
1199
|
+
},
|
|
1200
|
+
buildLocationData: function (context, scope, previous) {
|
|
1201
|
+
if (scope !== 'turn' || !context.state) return null
|
|
1202
|
+
var state = context.state
|
|
1203
|
+
if (previous && previous.kind === 'turn' && previous.turn === state.turn
|
|
1204
|
+
&& previous.key === ROUTE_TURN_KEY && previous.value.routes === state.routes) return previous
|
|
1205
|
+
return { kind: 'turn', turn: state.turn, key: ROUTE_TURN_KEY, value: { routes: state.routes } }
|
|
1206
|
+
},
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
/**
|
|
1211
|
+
* 本轮"产出的文件"数量(ui-deliverables 的 Turn 数据,与本插件无关的那个 key)。
|
|
1212
|
+
*
|
|
1213
|
+
* 回合尾部是**单选**席位:我们用了更低的 `priority`(先试),所以只要本轮有
|
|
1214
|
+
* 路线就由我们占用这一行,"本轮产出文件"那行本轮就不会渲染。把数量带进卡片,
|
|
1215
|
+
* 免得那份信息彻底消失(文件本身在过程区的工具卡里仍然看得到)。
|
|
1216
|
+
*/
|
|
1217
|
+
function producedFileCount(owner, seq) {
|
|
1218
|
+
try {
|
|
1219
|
+
var data = owner && owner.turn && owner.turn.data && typeof owner.turn.data.get === 'function'
|
|
1220
|
+
? owner.turn.data.get('deliverables')
|
|
1221
|
+
: undefined
|
|
1222
|
+
var list = data && Array.isArray(data.produced) ? data.produced : []
|
|
1223
|
+
var count = 0
|
|
1224
|
+
for (var i = 0; i < list.length; i += 1) {
|
|
1225
|
+
if (typeof list[i].seq === 'number' && list[i].seq >= seq) continue
|
|
1226
|
+
count += 1
|
|
1227
|
+
}
|
|
1228
|
+
return count
|
|
1229
|
+
} catch (error) {
|
|
1230
|
+
// 别的插件的 Turn 数据可能是任何形状:读不到就当 0,绝不影响地图卡。
|
|
1231
|
+
return 0
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
/** 本轮在收尾正文之前落定的路线(选择器:没有就放弃这个槽位)。 */
|
|
1236
|
+
function selectTurnRoutes(owner) {
|
|
1237
|
+
var data = owner && owner.turn && owner.turn.data && typeof owner.turn.data.get === 'function'
|
|
1238
|
+
? owner.turn.data.get(ROUTE_TURN_KEY)
|
|
1239
|
+
: undefined
|
|
1240
|
+
var all = data && Array.isArray(data.routes) ? data.routes : []
|
|
1241
|
+
var seq = typeof owner.seq === 'number' ? owner.seq : Number.POSITIVE_INFINITY
|
|
1242
|
+
var routes = []
|
|
1243
|
+
for (var i = 0; i < all.length; i += 1) {
|
|
1244
|
+
if (typeof all[i].seq === 'number' && all[i].seq > seq) continue
|
|
1245
|
+
routes.push(all[i])
|
|
1246
|
+
}
|
|
1247
|
+
if (routes.length === 0) return null
|
|
1248
|
+
// **不再挑"最后一条"**:模型常在同一轮里发多条路线调用(主路线 + 若干用于
|
|
1249
|
+
// 核对/备选的探测,有些直接传坐标、反查出来的地名与用户问的毫不相干)。
|
|
1250
|
+
// 实测踩坑:正文讲南艳湖→蜀山,地图却画了中间那条"望江西路欣塘家园→蜀山"。
|
|
1251
|
+
// 任何"猜哪条是主路线"的启发式都试过并否掉了(收尾正文会引用探测路线的
|
|
1252
|
+
// 坐标,最长公共子串反而给探测更高分)。现在按时间顺序全部交给卡片,
|
|
1253
|
+
// 由卡片逐条渲染 —— 用户问的那条一定在里面。
|
|
1254
|
+
return { routes: routes, produced: producedFileCount(owner, seq) }
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
/**
|
|
1258
|
+
* 卡片模型(纯函数、不碰 react):收出本轮要渲染的路线数、产出文件数与
|
|
1259
|
+
* "是否挤掉了官方产出文件行"。
|
|
1260
|
+
*
|
|
1261
|
+
* 这里就是两种槽位语义的**唯一适配点**:
|
|
1262
|
+
* · chain(≤ 0.1.6-alpha.1):框架把选择器结果当 `props.matched` 送进来;
|
|
1263
|
+
* 那是单选举席,我们当选就挤掉了官方"本轮产出文件"行 → displaced。
|
|
1264
|
+
* · list(≥ 0.1.6-alpha.2):没有 `matched`,从 ownerProps(turn / seq)
|
|
1265
|
+
* 自己推导(每次渲染都会跑一次,选择器保持纯函数);官方产出文件卡
|
|
1266
|
+
* 与我们同时出现,没有挤掉谁 → 不 displaced。
|
|
1267
|
+
*
|
|
1268
|
+
* @returns 模型对象;本轮没有可画的路线时返回 null(卡片整块不渲染)。
|
|
1269
|
+
*/
|
|
1270
|
+
function turnRouteCardModel(props) {
|
|
1271
|
+
var owner = props || {}
|
|
1272
|
+
var chainMatched = owner.matched && Array.isArray(owner.matched.routes) ? owner.matched : null
|
|
1273
|
+
var selected = chainMatched || selectTurnRoutes(owner)
|
|
1274
|
+
if (!selected) return null
|
|
1275
|
+
var routes = Array.isArray(selected.routes) ? selected.routes : []
|
|
1276
|
+
if (routes.length === 0) return null
|
|
1277
|
+
// 一轮里可能有多条路线(主路线 + 模型用于核对/备选的探测),**逐条渲染**,
|
|
1278
|
+
// 每条都有自己的地图。之前只画最后一条,导致"正文讲 A→B、地图画 C→B"。
|
|
1279
|
+
var models = []
|
|
1280
|
+
for (var i = 0; i < routes.length && models.length < MAX_TURN_ROUTES; i += 1) {
|
|
1281
|
+
var model = turnRouteModel(routes[i])
|
|
1282
|
+
if (model) models.push(model)
|
|
1283
|
+
}
|
|
1284
|
+
if (models.length === 0) return null
|
|
1285
|
+
return {
|
|
1286
|
+
models: models,
|
|
1287
|
+
total: routes.length,
|
|
1288
|
+
produced: typeof selected.produced === 'number' ? selected.produced : 0,
|
|
1289
|
+
displaced: chainMatched !== null,
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
/** 插件仓库:署名行给截图读者的唯一去处。 */
|
|
1294
|
+
var REPO_URL = 'https://github.com/HorusJiang/dsh-map-tools'
|
|
1295
|
+
|
|
1296
|
+
/**
|
|
1297
|
+
* 署名内容(包名 + GitHub 链接):接在「在高德打开 ↗」**同一行的后面**。
|
|
1298
|
+
*
|
|
1299
|
+
* 存在的理由只有**截图传播**:地图卡会被截图、转发到聊天和朋友圈里,而那一张
|
|
1300
|
+
* 图里没有 README、没有 npm 页——只有这行字能说明"这张地图卡是 dsh-map-tools
|
|
1301
|
+
* 画的、它是个 DSH 插件"。所以包名用正文色 + 中等字重(缩略图里也认得出),
|
|
1302
|
+
* 其余收敛成次要色——**字号由所在行统一给**(与「在高德打开 ↗」同号 12px),
|
|
1303
|
+
* 这里绝不自带 fontSize,否则同一行里两截会不等大。
|
|
1304
|
+
*
|
|
1305
|
+
* 纯客户端渲染,宿主半边没有一行改动:不进 `presentationMeta`、不进模型上下文、
|
|
1306
|
+
* 不占 token,也不参与回合尾部席位的单选语义。
|
|
1307
|
+
*
|
|
1308
|
+
* @param h - createElement。
|
|
1309
|
+
* @returns 该行的内容节点数组(调用方把它接在深链后面)。
|
|
1310
|
+
*/
|
|
1311
|
+
function creditNodes(h) {
|
|
1312
|
+
return [
|
|
1313
|
+
h('span', { key: 'credit' },
|
|
1314
|
+
'本卡片由 DeepSeek Harness 插件 ',
|
|
1315
|
+
h('span', { key: 'name', style: { color: TEXT, fontWeight: 500 } }, BUNDLE),
|
|
1316
|
+
' 生成'),
|
|
1317
|
+
h('a', {
|
|
1318
|
+
key: 'repo',
|
|
1319
|
+
href: REPO_URL,
|
|
1320
|
+
target: '_blank',
|
|
1321
|
+
rel: 'noreferrer',
|
|
1322
|
+
style: { color: ACCENT, textDecoration: 'none' },
|
|
1323
|
+
}, 'GitHub ↗'),
|
|
1324
|
+
]
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
/**
|
|
1328
|
+
* 回合尾部的路线卡片:本轮算了几条就逐条画(每条一张地图),
|
|
1329
|
+
* 与工具卡片复用同一套 body 与降级逻辑。
|
|
1330
|
+
*/
|
|
1331
|
+
function TurnRouteCard(props) {
|
|
1332
|
+
var react = require('react')
|
|
1333
|
+
var h = react.createElement
|
|
1334
|
+
var state = useCardState(react)
|
|
1335
|
+
var card = turnRouteCardModel(props)
|
|
1336
|
+
if (!card) return null
|
|
1337
|
+
var models = card.models
|
|
1338
|
+
// 局部状态(折叠/图片失败)挂在第一条上:其余条目的地图仍按各自 URL 加载,
|
|
1339
|
+
// 加载失败时各自退回自绘示意图由 RouteBody 内部处理。
|
|
1340
|
+
applyCardState(models[0], state)
|
|
1341
|
+
var children = []
|
|
1342
|
+
for (var k = 0; k < models.length; k += 1) {
|
|
1343
|
+
var head = []
|
|
1344
|
+
if (models.length > 1) head.push('第 ' + (k + 1) + '/' + models.length + ' 条')
|
|
1345
|
+
head.push(models[k].label)
|
|
1346
|
+
if (models[k].summary) head.push(models[k].summary)
|
|
1347
|
+
if (k > 0) {
|
|
1348
|
+
children.push(h('div', {
|
|
1349
|
+
key: 'sep' + k,
|
|
1350
|
+
style: { margin: '10px 0 6px', borderTop: '1px solid ' + CARD_BORDER },
|
|
1351
|
+
}))
|
|
1352
|
+
}
|
|
1353
|
+
children.push(h('div', {
|
|
1354
|
+
key: 'head' + k,
|
|
1355
|
+
style: { fontSize: '12px', color: MUTED, marginBottom: '6px' },
|
|
1356
|
+
}, head.join(' · ')))
|
|
1357
|
+
// 署名只接在**最后一条路线**的「在高德打开 ↗」后面(整张卡出现一次,
|
|
1358
|
+
// 不是每条路线一次):它是给截图读者的落款,重复 N 遍只会变成噪音。
|
|
1359
|
+
children.push(h('div', { key: 'body' + k }, RouteBody(h, models[k], { credit: k === models.length - 1 })))
|
|
1360
|
+
}
|
|
1361
|
+
if (card.total > MAX_TURN_ROUTES) {
|
|
1362
|
+
children.push(h('div', {
|
|
1363
|
+
key: 'more',
|
|
1364
|
+
style: { marginTop: '6px', fontSize: '12px', color: MUTED },
|
|
1365
|
+
}, '本轮共 ' + card.total + ' 条路线,这里显示前 ' + MAX_TURN_ROUTES + ' 条'))
|
|
1366
|
+
}
|
|
1367
|
+
if (card.displaced && card.produced > 0) {
|
|
1368
|
+
// 只在链式槽位下说这句:那是单选举席,我们把"本轮产出文件"那行挤掉了
|
|
1369
|
+
// (代价),至少把数量交代清楚。列表式槽位下官方那行照常渲染,无需多言。
|
|
1370
|
+
children.push(h('div', {
|
|
1371
|
+
key: 'produced',
|
|
1372
|
+
style: { marginTop: '6px', fontSize: '12px', color: MUTED },
|
|
1373
|
+
}, '本轮另有 ' + card.produced + ' 个产出文件(地图卡占用此行,文件见过程区)'))
|
|
1374
|
+
}
|
|
1375
|
+
return h('div', {
|
|
1376
|
+
style: {
|
|
1377
|
+
marginTop: '8px',
|
|
1378
|
+
padding: '8px 10px',
|
|
1379
|
+
borderRadius: '8px',
|
|
1380
|
+
border: '1px solid ' + CARD_BORDER,
|
|
1381
|
+
background: CARD_BG,
|
|
1382
|
+
},
|
|
1383
|
+
}, children)
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
/** 卡片的两处局部状态(折叠路书 / 静态地图是否加载失败)。 */
|
|
1387
|
+
function useCardState(react) {
|
|
1388
|
+
return { expanded: react.useState(false), imageFailed: react.useState(false) }
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
/** 把局部状态接到模型上(组件体里无条件调用,遵守 hooks 规则)。 */
|
|
1392
|
+
function applyCardState(model, state) {
|
|
1393
|
+
model.expanded = state.expanded[0]
|
|
1394
|
+
model.onToggle = function () { state.expanded[1](!state.expanded[0]) }
|
|
1395
|
+
model.imageFailed = state.imageFailed[0]
|
|
1396
|
+
// 静态地图加载失败(没 key / 配额超限 / 宿主无此路由)→ 退回自绘示意图。
|
|
1397
|
+
model.onImageError = function () { state.imageFailed[1](true) }
|
|
1398
|
+
return model
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
/**
|
|
1402
|
+
* 读槽位当前的声明类型(`chain` / `list` / `single` / `keyed`)。
|
|
1403
|
+
*
|
|
1404
|
+
* 这是"插件跟着宿主版本走"的关键:0.1.6-alpha.2 把 turnTail 从 chain 改成了
|
|
1405
|
+
* list,两者要求的注册字段互斥(chain 要 `select`、list 要 `id`,缺了就抛),
|
|
1406
|
+
* 而**读声明**比猜版本号可靠——宿主小版本号无法枚举,同一份 bundle 却能靠它
|
|
1407
|
+
* 在两个宿主上都注册成功。
|
|
1408
|
+
*
|
|
1409
|
+
* @returns 类型字符串;读不到(老宿主无 spec()、槽位未声明、服务形状变了)返回 undefined。
|
|
1410
|
+
*/
|
|
1411
|
+
function slotKind(scope, name) {
|
|
1412
|
+
try {
|
|
1413
|
+
var slots = scope && scope.slots
|
|
1414
|
+
if (!slots || typeof slots.spec !== 'function') return undefined
|
|
1415
|
+
var spec = slots.spec(name)
|
|
1416
|
+
return spec && typeof spec.kind === 'string' ? spec.kind : undefined
|
|
1417
|
+
} catch (error) {
|
|
1418
|
+
return undefined
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
/** 列表式注册:`id` 必填、同 id 同 priority 唯一;组件自己推导数据。 */
|
|
1423
|
+
function registerTurnTailAsList(scope) {
|
|
1424
|
+
// priority -1:列表式按 priority 升序渲染,排到官方"本轮文件改动"卡
|
|
1425
|
+
// (默认 0)前面,地图卡仍然紧贴正文。
|
|
1426
|
+
return scope.slots.register({
|
|
1427
|
+
name: TURN_TAIL_SLOT,
|
|
1428
|
+
id: TURN_TAIL_ID,
|
|
1429
|
+
priority: -1,
|
|
1430
|
+
}, TurnRouteCard)
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
/** 链式注册:`select` 必填;框架把选择器结果作为 props.matched 送进组件。 */
|
|
1434
|
+
function registerTurnTailAsChain(scope) {
|
|
1435
|
+
// 回合尾部是**单选**链式槽位(第一个非空 select 当选;同优先级按注册
|
|
1436
|
+
// 顺序)。ui-deliverables 在 web 组合里先注册,所以用默认优先级时,
|
|
1437
|
+
// 只要本轮产出了文件它就当选,地图卡永远轮不到(实测:我的回合几乎
|
|
1438
|
+
// 必然写文件,于是地图一直不显示)。用更低的 priority 让我们**先试**:
|
|
1439
|
+
// · 本轮有路线 → 我们当选,并在卡里注明"另有 N 个产出文件";
|
|
1440
|
+
// · 本轮没路线 → 我们返回 null 弃权,官方的产出文件行照常渲染。
|
|
1441
|
+
return scope.slots.register({
|
|
1442
|
+
name: TURN_TAIL_SLOT,
|
|
1443
|
+
priority: -1,
|
|
1444
|
+
select: selectTurnRoutes,
|
|
1445
|
+
}, TurnRouteCard)
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
/**
|
|
1449
|
+
* 按宿主**实际**的槽位类型注册回合尾部卡片。
|
|
1450
|
+
*
|
|
1451
|
+
* 类型未知时按 list → chain 依次试:list 槽位缺 `id` 会抛、chain 槽位缺
|
|
1452
|
+
* `select` 也会抛,而两者的校验都发生在写入账本之前(ui-slots register 的
|
|
1453
|
+
* 类型检查在最前),所以失败的尝试不留副作用。
|
|
1454
|
+
*/
|
|
1455
|
+
function registerTurnTail(scope) {
|
|
1456
|
+
var kind = slotKind(scope, TURN_TAIL_SLOT)
|
|
1457
|
+
if (kind === 'chain') return registerTurnTailAsChain(scope)
|
|
1458
|
+
if (kind === 'list') return registerTurnTailAsList(scope)
|
|
1459
|
+
try {
|
|
1460
|
+
return registerTurnTailAsList(scope)
|
|
1461
|
+
} catch (listError) {
|
|
1462
|
+
try {
|
|
1463
|
+
return registerTurnTailAsChain(scope)
|
|
1464
|
+
} catch (chainError) {
|
|
1465
|
+
// 两条路都堵上时把两个原因都带出来,别让回退把 list 的真实错误吞掉
|
|
1466
|
+
// (例如同 id 同 priority 的重复注册,HMR 重载时最容易撞)。
|
|
1467
|
+
throw new Error('neither list nor chain registration worked — list: '
|
|
1468
|
+
+ listError + '; chain: ' + chainError)
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
/**
|
|
1474
|
+
* 注册回合尾部的路线卡片。
|
|
1475
|
+
*
|
|
1476
|
+
* 两处注册相互独立、各自兜错:回合折叠定义失败不该影响工具卡片,
|
|
1477
|
+
* 反之亦然(同一个 bundle 还挂着 bundle 配置卡,不能一起崩)。
|
|
1478
|
+
*/
|
|
1479
|
+
function registerTurnRouteCard(ctx) {
|
|
1480
|
+
if (!ctx || typeof ctx.inject !== 'function') return
|
|
1481
|
+
ctx.inject(['uiConversation', 'slots'], function (scope) {
|
|
1482
|
+
try {
|
|
1483
|
+
var events = scope.uiConversation && scope.uiConversation.events
|
|
1484
|
+
if (events && typeof events.register === 'function') events.register(routeTurnDefinition())
|
|
1485
|
+
} catch (error) {
|
|
1486
|
+
console.error('[dsh-map-tools] route turn definition skipped: ' + error)
|
|
1487
|
+
}
|
|
1488
|
+
try {
|
|
1489
|
+
scope.slots.inject(TURN_TAIL_SLOT, function () {
|
|
1490
|
+
// 兜错**必须写在工厂体里**:`slots.inject()` 只登记这个工厂,它由框架
|
|
1491
|
+
// 在槽位声明之后调用(声明已存在时同步、否则异步),抛错发生在包住
|
|
1492
|
+
// inject 的那个 try/catch 之外——早先版本正是因此在控制台留下 page error。
|
|
1493
|
+
try {
|
|
1494
|
+
return registerTurnTail(scope)
|
|
1495
|
+
} catch (error) {
|
|
1496
|
+
console.error('[dsh-map-tools] route turn tail skipped: ' + error)
|
|
1497
|
+
return function () {}
|
|
1498
|
+
}
|
|
1499
|
+
})
|
|
1500
|
+
} catch (error) {
|
|
1501
|
+
console.error('[dsh-map-tools] route turn tail skipped: ' + error)
|
|
1502
|
+
}
|
|
1503
|
+
})
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
function apply(ctx) {
|
|
1507
|
+
registerCard(ctx)
|
|
1508
|
+
if (typeof ctx.inject !== 'function') return
|
|
1509
|
+
ctx.inject(['slots'], function (scope) {
|
|
1510
|
+
try {
|
|
1511
|
+
registerRouteCards(scope)
|
|
1512
|
+
} catch (error) {
|
|
1513
|
+
console.error('[dsh-map-tools] route card skipped: ' + error)
|
|
1514
|
+
}
|
|
1515
|
+
})
|
|
1516
|
+
try {
|
|
1517
|
+
registerTurnRouteCard(ctx)
|
|
1518
|
+
} catch (error) {
|
|
1519
|
+
console.error('[dsh-map-tools] route turn tail skipped: ' + error)
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
exports.apply = apply
|
|
1524
|
+
exports.inject = []
|
|
1525
|
+
// 纯函数出口:供单元测试直接验证随包发布的这段代码(无构建步骤,
|
|
1526
|
+
// 所以测试跑的就是浏览器加载的同一份字节)。
|
|
1527
|
+
exports.__card = {
|
|
1528
|
+
ConfigCard: ConfigCard,
|
|
1529
|
+
registerCard: registerCard,
|
|
1530
|
+
registerOne: registerOne,
|
|
1531
|
+
BUNDLE_SEAT: BUNDLE_SEAT,
|
|
1532
|
+
LEGACY_SEAT: LEGACY_SEAT,
|
|
1533
|
+
BUNDLE: BUNDLE,
|
|
1534
|
+
CONFIG_URL: CONFIG_URL,
|
|
1535
|
+
PROVIDERS: PROVIDERS,
|
|
1536
|
+
DEFAULT_TIMEOUT_MS: DEFAULT_TIMEOUT_MS,
|
|
1537
|
+
TIMEOUT_MIN_MS: TIMEOUT_MIN_MS,
|
|
1538
|
+
TIMEOUT_MAX_MS: TIMEOUT_MAX_MS,
|
|
1539
|
+
parseTimeout: parseTimeout,
|
|
1540
|
+
effectiveConfig: effectiveConfig,
|
|
1541
|
+
draftFrom: draftFrom,
|
|
1542
|
+
draftValid: draftValid,
|
|
1543
|
+
configPatch: configPatch,
|
|
1544
|
+
isDirty: isDirty,
|
|
1545
|
+
statusLine: statusLine,
|
|
1546
|
+
}
|
|
1547
|
+
exports.__route = {
|
|
1548
|
+
ROUTE_TOOLS: ROUTE_TOOLS,
|
|
1549
|
+
parseArgs: parseArgs,
|
|
1550
|
+
callSummary: callSummary,
|
|
1551
|
+
flattenContent: flattenContent,
|
|
1552
|
+
routeMeta: routeMeta,
|
|
1553
|
+
projectLine: projectLine,
|
|
1554
|
+
mapFrame: mapFrame,
|
|
1555
|
+
labelPlacement: labelPlacement,
|
|
1556
|
+
decimateLine: decimateLine,
|
|
1557
|
+
staticMapUrl: staticMapUrl,
|
|
1558
|
+
sketchSvg: sketchSvg,
|
|
1559
|
+
turnRouteModel: turnRouteModel,
|
|
1560
|
+
turnRouteCardModel: turnRouteCardModel,
|
|
1561
|
+
routeTurnDefinition: routeTurnDefinition,
|
|
1562
|
+
selectTurnRoutes: selectTurnRoutes,
|
|
1563
|
+
slotKind: slotKind,
|
|
1564
|
+
registerTurnTail: registerTurnTail,
|
|
1565
|
+
registerTurnTailAsList: registerTurnTailAsList,
|
|
1566
|
+
registerTurnTailAsChain: registerTurnTailAsChain,
|
|
1567
|
+
TURN_TAIL_SLOT: TURN_TAIL_SLOT,
|
|
1568
|
+
TURN_TAIL_ID: TURN_TAIL_ID,
|
|
1569
|
+
formatDistance: formatDistance,
|
|
1570
|
+
formatDuration: formatDuration,
|
|
1571
|
+
amapUri: amapUri,
|
|
1572
|
+
routeCardModel: routeCardModel,
|
|
1573
|
+
registerRouteCards: registerRouteCards,
|
|
1574
|
+
registerTurnRouteCard: registerTurnRouteCard,
|
|
1575
|
+
producedFileCount: producedFileCount,
|
|
1576
|
+
MAX_TURN_ROUTES: MAX_TURN_ROUTES,
|
|
1577
|
+
creditNodes: creditNodes,
|
|
1578
|
+
REPO_URL: REPO_URL,
|
|
1579
|
+
TurnRouteCard: TurnRouteCard,
|
|
1580
|
+
RouteCard: RouteCard,
|
|
1581
|
+
}
|
|
1582
|
+
return module.exports
|
|
1583
|
+
},
|
|
1584
|
+
})
|