@danceiny/gotry 0.0.1-rc.9 → 0.2.0-rc.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +172 -219
- package/README.zh-CN.md +222 -0
- package/bin/gotry-backend.js +53 -0
- package/bin/gotry-booking-copilot.js +53 -0
- package/bin/gotry-bootstrap.js +1744 -0
- package/bin/gotry-inner.js +733 -83
- package/bin/gotry-process-liveness.d.ts +27 -0
- package/bin/gotry-process-liveness.js +97 -0
- package/bin/gotry-runtime-resolution.d.ts +27 -0
- package/bin/gotry-runtime-resolution.js +49 -0
- package/bin/gotry.js +1 -1
- package/client/client.js +203 -0
- package/cordis.gotry-patch.yml +80 -56
- package/data/airline-airports.json +39 -0
- package/data/stations-12306-verify.json +524 -0
- package/dist/capabilities/agent-reach-deep.js +45 -122
- package/dist/capabilities/agent-reach.js +101 -146
- package/dist/capabilities/anything.js +135 -94
- package/dist/capabilities/artifacts.js +370 -0
- package/dist/capabilities/channel-health.js +131 -0
- package/dist/capabilities/channel-registry.js +256 -0
- package/dist/capabilities/doctor.js +546 -0
- package/dist/capabilities/effect.js +394 -0
- package/dist/capabilities/external-event.js +149 -0
- package/dist/capabilities/fact-log.js +55 -0
- package/dist/capabilities/flyai-config.js +337 -0
- package/dist/capabilities/flyai.js +947 -110
- package/dist/capabilities/fx-contract.js +293 -0
- package/dist/capabilities/geo-atlas.js +246 -0
- package/dist/capabilities/ground-transfer.js +509 -0
- package/dist/capabilities/hbcli.js +260 -123
- package/dist/capabilities/hotelbyte-transaction.js +194 -0
- package/dist/capabilities/incident-log.js +131 -61
- package/dist/capabilities/itinerary-artifact.js +190 -0
- package/dist/capabilities/itinerary-deck-artifact.js +194 -0
- package/dist/capabilities/itinerary-deck-export.js +339 -0
- package/dist/capabilities/lavish-local.js +924 -0
- package/dist/capabilities/model-override.js +38 -0
- package/dist/capabilities/opensky.js +54 -43
- package/dist/capabilities/resilience.js +191 -0
- package/dist/capabilities/session/action-cache.js +50 -44
- package/dist/capabilities/session/adapters/ctrip-flight.js +120 -62
- package/dist/capabilities/session/adapters/ctrip-hotel.js +222 -0
- package/dist/capabilities/session/adapters/dida-portal.js +274 -0
- package/dist/capabilities/session/adapters/meituan-local.js +35 -54
- package/dist/capabilities/session/adapters/rail-12306.js +342 -0
- package/dist/capabilities/session/benchmark.js +189 -0
- package/dist/capabilities/session/bridge-ledger.js +224 -0
- package/dist/capabilities/session/extension-bridge.js +636 -0
- package/dist/capabilities/session/extension-channel.js +109 -0
- package/dist/capabilities/session/extension-distribution.js +230 -0
- package/dist/capabilities/session/extract.js +31 -21
- package/dist/capabilities/session/golden-score.js +114 -0
- package/dist/capabilities/session/health-watch.js +215 -0
- package/dist/capabilities/session/read-guard.js +75 -24
- package/dist/capabilities/session/static-flight-golden.js +150 -0
- package/dist/capabilities/session/transport.js +97 -83
- package/dist/capabilities/session/wizard.js +117 -0
- package/dist/capabilities/session-consent.js +178 -0
- package/dist/capabilities/session-login.js +170 -0
- package/dist/capabilities/session-search.js +635 -44
- package/dist/capabilities/spawn-bounded.js +203 -0
- package/dist/capabilities/sponsor-plugin.js +341 -0
- package/dist/capabilities/visa-policy.js +140 -0
- package/dist/capabilities/weather.js +243 -138
- package/dist/data/session-golden-20.json +25 -0
- package/dist/data/sf-golden-manifest.json +103 -0
- package/dist/data/sf-static-routes.json +91 -0
- package/dist/scripts/action-cache-tests.js +24 -59
- package/dist/scripts/agent-planning-turn-deadline-e2e.js +242 -0
- package/dist/scripts/agent-planning-turn-deadline-tests.js +185 -0
- package/dist/scripts/agent-reach-deep-tests.js +30 -40
- package/dist/scripts/agent-reach-error-tests.js +354 -0
- package/dist/scripts/agent-reach-tests.js +26 -18
- package/dist/scripts/agent-reach-wrapper-tests.js +50 -50
- package/dist/scripts/anything-tests.js +116 -60
- package/dist/scripts/artifact-client-contract-tests.js +220 -0
- package/dist/scripts/artifacts-capability-tests.js +727 -0
- package/dist/scripts/async-collect.js +50 -11
- package/dist/scripts/benchmark-environment-bridge-e2e.js +741 -0
- package/dist/scripts/benchmark-environment-bridge-tests.js +1452 -0
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +126 -0
- package/dist/scripts/booking-copilot-availability-policy-tests.js +265 -0
- package/dist/scripts/booking-copilot-bin-proof-tests.js +66 -0
- package/dist/scripts/booking-copilot-cleanup-proof-helper.js +76 -0
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +144 -0
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +424 -0
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +2839 -0
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +309 -0
- package/dist/scripts/booking-copilot-dsh-readiness-proof.js +238 -0
- package/dist/scripts/booking-copilot-dsh-warmer-proof.js +123 -0
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +156 -0
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +70 -0
- package/dist/scripts/booking-copilot-intent-proof-tests.js +422 -0
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +184 -0
- package/dist/scripts/booking-copilot-planner-live-probe.js +95 -0
- package/dist/scripts/booking-copilot-proof-runtime.js +42 -0
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +210 -0
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +2349 -0
- package/dist/scripts/booking-copilot-server-proof-tests.js +784 -0
- package/dist/scripts/booking-copilot-startup-proof-tests.js +94 -0
- package/dist/scripts/booking-executor-tests.js +80 -0
- package/dist/scripts/booking-recovery-chain-tests.js +182 -0
- package/dist/scripts/booking-saga-tests.js +125 -0
- package/dist/scripts/booking-surface-contract-proof-tests.js +212 -0
- package/dist/scripts/bootstrap-tests.js +1479 -0
- package/dist/scripts/bridge-events-tests.js +239 -0
- package/dist/scripts/bridge-ledger-tests.js +428 -0
- package/dist/scripts/build-changelog.js +238 -0
- package/dist/scripts/build-metrics-report.js +331 -0
- package/dist/scripts/changelog-tests.js +118 -0
- package/dist/scripts/channel-probe-tests.js +103 -0
- package/dist/scripts/channel-probe.js +169 -0
- package/dist/scripts/channel-registry-tests.js +208 -0
- package/dist/scripts/companion-tests.js +25 -70
- package/dist/scripts/dida-cookie-probe.js +87 -0
- package/dist/scripts/dida-runner-stop-tests.js +311 -0
- package/dist/scripts/dida-sw-probe.js +114 -0
- package/dist/scripts/diff-test.js +17 -12
- package/dist/scripts/doctor-tests.js +564 -0
- package/dist/scripts/dsh-artifact-e2e.js +279 -0
- package/dist/scripts/dsh-artifact-web-e2e.js +675 -0
- package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
- package/dist/scripts/dsh-runtime-closure-tests.js +182 -0
- package/dist/scripts/dsh-runtime-closure.js +162 -0
- package/dist/scripts/dsh-session-v3-migration-proof.js +109 -0
- package/dist/scripts/dsh-subprocess-local-proof.js +150 -0
- package/dist/scripts/dsh-target-closure-proof.js +178 -0
- package/dist/scripts/effect-tests.js +539 -0
- package/dist/scripts/engine-run.js +1 -3
- package/dist/scripts/engine-tests.js +16 -10
- package/dist/scripts/evaluation-cadence-tests.js +174 -0
- package/dist/scripts/evaluation-contract-tests.js +268 -0
- package/dist/scripts/extension-distribution-cli.js +39 -0
- package/dist/scripts/extension-distribution-tests.js +443 -0
- package/dist/scripts/extension-tests.js +988 -0
- package/dist/scripts/external-event-tests.js +350 -0
- package/dist/scripts/fact-gate-tests.js +869 -0
- package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
- package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
- package/dist/scripts/flight-malformed-tests.js +159 -0
- package/dist/scripts/flyai-setup-tests.js +228 -0
- package/dist/scripts/flyai-setup-tool-tests.js +54 -0
- package/dist/scripts/flyai-tests.js +692 -0
- package/dist/scripts/flyai-tool-registration-contract-tests.js +91 -0
- package/dist/scripts/fx-contract-tests.js +201 -0
- package/dist/scripts/g5-guard-tests.js +131 -0
- package/dist/scripts/g5-guard.js +189 -0
- package/dist/scripts/gen-lunar-anchors.js +106 -0
- package/dist/scripts/geo-atlas-tests.js +283 -0
- package/dist/scripts/gotry-backend-kernel-tests.js +206 -0
- package/dist/scripts/gotry-try-demo-tests.js +79 -0
- package/dist/scripts/gotry-try-demo.js +49 -0
- package/dist/scripts/ground-transfer-tests.js +956 -0
- package/dist/scripts/hbcli-e2e-tests.js +159 -0
- package/dist/scripts/hbcli-live-optin-tests.js +96 -0
- package/dist/scripts/hbcli-release-contract-tests.js +122 -0
- package/dist/scripts/hbcli-tests.js +121 -69
- package/dist/scripts/health-watch-cli.js +73 -0
- package/dist/scripts/hotel-date-gate-tests.js +496 -0
- package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
- package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
- package/dist/scripts/i18n-tests.js +41 -0
- package/dist/scripts/incident-tests.js +281 -87
- package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
- package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
- package/dist/scripts/issue-271-liveness-tests.js +569 -0
- package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
- package/dist/scripts/issue-289-model-retry-tests.js +441 -0
- package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
- package/dist/scripts/issue-338-home-city-e2e.js +577 -0
- package/dist/scripts/issue343-real-entry-e2e.js +160 -0
- package/dist/scripts/issue436-persisted-routing-e2e.js +247 -0
- package/dist/scripts/itinerary-artifact-tests.js +368 -0
- package/dist/scripts/itinerary-deck-artifact-tests.js +416 -0
- package/dist/scripts/itinerary-deck-export-tests.js +552 -0
- package/dist/scripts/itinerary-deck-tests.js +435 -0
- package/dist/scripts/itinerary-html-tests.js +426 -0
- package/dist/scripts/journey-tests.js +22 -27
- package/dist/scripts/kernel-manifest-evidence.js +109 -0
- package/dist/scripts/kernel-manifest-gate.js +126 -0
- package/dist/scripts/kernel-manifest-tests.js +212 -0
- package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
- package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
- package/dist/scripts/kernel-manifest.js +369 -0
- package/dist/scripts/lavish-local-tests.js +1226 -0
- package/dist/scripts/lavish-product-tools-tests.js +537 -0
- package/dist/scripts/ledger-repair-apply-tests.js +316 -0
- package/dist/scripts/ledger-repair-plan-tests.js +375 -0
- package/dist/scripts/ledger-tests.js +382 -246
- package/dist/scripts/ledger-workflow-crash.js +10 -20
- package/dist/scripts/managed-dsh-cleanup-diagnostic-proof.js +184 -0
- package/dist/scripts/managed-dsh-close-failure-proof.js +172 -0
- package/dist/scripts/managed-dsh-run-port-proof.js +153 -0
- package/dist/scripts/managed-dsh-terminal-proof.js +252 -0
- package/dist/scripts/map-tools-vendor-package-proof.js +365 -0
- package/dist/scripts/memory-capture-tests.js +242 -61
- package/dist/scripts/memory-decay-tests.js +18 -52
- package/dist/scripts/memory-lifecycle-tests.js +657 -0
- package/dist/scripts/memory-lifecycle.js +196 -0
- package/dist/scripts/memory-metrics.js +24 -12
- package/dist/scripts/memory-value-report.js +631 -0
- package/dist/scripts/memory-value-tests.js +328 -0
- package/dist/scripts/metrics-report-tests.js +151 -0
- package/dist/scripts/model-override-e2e.js +137 -0
- package/dist/scripts/nightly-evidence-tests.js +161 -0
- package/dist/scripts/nightly-evidence.js +286 -0
- package/dist/scripts/nudge-digest.js +39 -28
- package/dist/scripts/observability-tests.js +381 -0
- package/dist/scripts/onboarding-tests.js +161 -0
- package/dist/scripts/opensky-check.js +18 -13
- package/dist/scripts/opensky-tests.js +28 -28
- package/dist/scripts/persona-surface-guard-tests.js +49 -0
- package/dist/scripts/pnpm-dsh-closure-proof.js +17 -0
- package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
- package/dist/scripts/price-drift-tests.js +340 -0
- package/dist/scripts/price-drift-watch.js +447 -0
- package/dist/scripts/probe-poi-tests.js +53 -45
- package/dist/scripts/product-metrics.js +483 -0
- package/dist/scripts/publish-preverify.js +110 -0
- package/dist/scripts/realtime-pricing-tests.js +76 -0
- package/dist/scripts/recall-tests.js +513 -0
- package/dist/scripts/replay-async.js +20 -18
- package/dist/scripts/replay-real.js +18 -18
- package/dist/scripts/replay.js +52 -82
- package/dist/scripts/session-attach-diagnose.js +20 -20
- package/dist/scripts/session-attach-poc.js +34 -37
- package/dist/scripts/session-benchmark.js +180 -0
- package/dist/scripts/session-dida-live-e2e.js +199 -0
- package/dist/scripts/session-extract-tests.js +14 -35
- package/dist/scripts/session-link-tests.js +478 -0
- package/dist/scripts/session-login.js +27 -46
- package/dist/scripts/session-tests.js +796 -96
- package/dist/scripts/session-zones-tests.js +358 -0
- package/dist/scripts/sf-live-benchmark.js +367 -0
- package/dist/scripts/sf-live-challenge-stop-tests.js +453 -0
- package/dist/scripts/sf-live-cli-tests.js +92 -0
- package/dist/scripts/sf-live-options.js +46 -0
- package/dist/scripts/sf-manifest-tests.js +30 -0
- package/dist/scripts/sf-manifest.js +33 -0
- package/dist/scripts/sf-soft-score-tests.js +67 -0
- package/dist/scripts/sf-summary-tests.js +324 -0
- package/dist/scripts/sf-summary.js +525 -0
- package/dist/scripts/share-tests.js +432 -0
- package/dist/scripts/skeleton-check.js +16 -14
- package/dist/scripts/skeleton-integration-test.js +28 -11
- package/dist/scripts/skills-contract-tests.js +51 -46
- package/dist/scripts/smoke-session-gate-tests.js +21 -0
- package/dist/scripts/smoke.js +690 -246
- package/dist/scripts/spawn-bounded-abort-tests.js +241 -0
- package/dist/scripts/sponsor-reuse-tests.js +262 -0
- package/dist/scripts/state-cli-tests.js +226 -101
- package/dist/scripts/state-cli.js +550 -199
- package/dist/scripts/state-repair-cli.js +66 -0
- package/dist/scripts/state-repair-tests.js +140 -0
- package/dist/scripts/static-golden-tests.js +165 -0
- package/dist/scripts/time-eval-tests.js +210 -192
- package/dist/scripts/travel-timeline-tests.js +29 -93
- package/dist/scripts/turn-handoff-collect-tests.js +169 -0
- package/dist/scripts/turn-handoff-collect.js +161 -0
- package/dist/scripts/turn-policy-tests.js +55 -0
- package/dist/scripts/typed-contract-canary.js +176 -0
- package/dist/scripts/tz-resolver-sanity.js +50 -0
- package/dist/scripts/tz-resolver-tests.js +584 -0
- package/dist/scripts/unified-tests.js +23 -19
- package/dist/scripts/visa-policy-tests.js +62 -0
- package/dist/scripts/weather-tests.js +276 -41
- package/dist/scripts/wish-channel-gate-tests.js +54 -0
- package/dist/scripts/write-gate-tests.js +522 -0
- package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
- package/dist/scripts/z3-lifecycle-tests.js +170 -0
- package/dist/scripts/z3-race-repeat-tests.js +39 -0
- package/dist/scripts/z3-race-tests.js +41 -0
- package/dist/src/artifact-gate.js +829 -0
- package/dist/src/backend/kernel.js +91 -0
- package/dist/src/backend/modules/booking-copilot.js +92 -0
- package/dist/src/backend/modules/booking-executor.js +124 -0
- package/dist/src/backend/modules/session-search.js +551 -0
- package/dist/src/benchmark-agent-conformance.js +578 -0
- package/dist/src/benchmark-environment-bridge.js +515 -0
- package/dist/src/benchmark-headless-child-diagnostics.js +166 -0
- package/dist/src/benchmark-tool-isolation.js +151 -0
- package/dist/src/bookable-facts.js +533 -0
- package/dist/src/booking-saga.js +97 -0
- package/dist/src/booking-surface/availability-policy.js +697 -0
- package/dist/src/booking-surface/booking-intent.js +43 -0
- package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
- package/dist/src/booking-surface/canonical-schema.js +116 -0
- package/dist/src/booking-surface/contracts.js +75 -0
- package/dist/src/booking-surface/dsh-planner.js +1854 -0
- package/dist/src/booking-surface/dsh-plugin.js +378 -0
- package/dist/src/booking-surface/dsh-worker.js +98 -0
- package/dist/src/booking-surface/error-codes.js +231 -0
- package/dist/src/booking-surface/index.js +9 -0
- package/dist/src/booking-surface/managed-dsh-cleanup-diagnostic.js +97 -0
- package/dist/src/booking-surface/managed-dsh-run-port.js +244 -0
- package/dist/src/booking-surface/profile.js +24 -0
- package/dist/src/booking-surface/recovery-chain.js +367 -0
- package/dist/src/booking-surface/runtime.js +1968 -0
- package/dist/src/booking-surface/server.js +674 -0
- package/dist/src/booking-surface/startup.js +168 -0
- package/dist/src/booking-surface/validation.js +261 -0
- package/dist/src/bridge.js +20 -13
- package/dist/src/companions.js +48 -74
- package/dist/src/contracts.js +21 -43
- package/dist/src/dsh-llm.js +97 -111
- package/dist/src/engine.js +276 -193
- package/dist/src/evaluation-cadence.js +97 -0
- package/dist/src/evaluation-contracts.js +283 -0
- package/dist/src/flight-pack-adapter.js +54 -0
- package/dist/src/flight-pack-contract.js +2 -0
- package/dist/src/flyai-setup-tool.js +70 -0
- package/dist/src/gotry-backend.js +125 -0
- package/dist/src/hotel-date-gate.js +118 -0
- package/dist/src/i18n.js +88 -0
- package/dist/src/index.js +2227 -882
- package/dist/src/itinerary-deck.js +139 -0
- package/dist/src/itinerary-doc-shared.js +661 -0
- package/dist/src/itinerary-html.js +131 -0
- package/dist/src/journey.js +155 -108
- package/dist/src/lavish-tools.js +400 -0
- package/dist/src/ledger-repair-apply.js +538 -0
- package/dist/src/ledger-repair-plan.js +463 -0
- package/dist/src/loop.js +319 -160
- package/dist/src/memory-capture.js +180 -32
- package/dist/src/memory-decay.js +30 -14
- package/dist/src/memory-lifecycle.js +1310 -0
- package/dist/src/memory-utility.js +27 -40
- package/dist/src/mock-llm.js +36 -61
- package/dist/src/model.js +81 -36
- package/dist/src/realtime-pricing.js +103 -0
- package/dist/src/recall/card.js +74 -0
- package/dist/src/recall/evaluator.js +140 -0
- package/dist/src/recall/integration.js +17 -0
- package/dist/src/recall/tick.js +166 -0
- package/dist/src/session-link/plan-it-action.js +48 -0
- package/dist/src/session-link/session-link.js +248 -0
- package/dist/src/session-zones.js +507 -0
- package/dist/src/share/adapters.js +51 -0
- package/dist/src/share/share-consent.js +76 -0
- package/dist/src/share/share-deck.js +58 -0
- package/dist/src/share/share-token.js +146 -0
- package/dist/src/slot-spec.js +84 -106
- package/dist/src/state-ledger.js +435 -516
- package/dist/src/state-repair.js +299 -0
- package/dist/src/subagent-job-id-guard.js +58 -0
- package/dist/src/time-anchor.js +231 -33
- package/dist/src/tool-packet.js +20 -8
- package/dist/src/travel-slots.js +58 -35
- package/dist/src/travel-timeline.js +44 -63
- package/dist/src/turn-deadline.js +287 -0
- package/dist/src/turn-policy.js +118 -0
- package/dist/src/tz-resolver.js +143 -0
- package/dist/src/unified.js +535 -359
- package/dist/src/wish-pool.js +42 -12
- package/dist/src/write-gate.js +495 -0
- package/dist/src/z3-shared.js +284 -0
- package/extension/README.md +21 -0
- package/extension/README.zh-CN.md +21 -0
- package/extension/background.js +463 -0
- package/extension/content-bridge.js +270 -0
- package/extension/content-main.js +111 -0
- package/extension/manifest.json +60 -0
- package/package.json +407 -14
- package/schemas/booking.intent.schema.json +193 -0
- package/schemas/booking.surface.schema.json +2622 -0
- package/ts/capabilities/agent-reach-deep.ts +114 -0
- package/ts/capabilities/agent-reach.ts +198 -0
- package/ts/capabilities/anything.ts +255 -0
- package/ts/capabilities/artifacts.ts +428 -0
- package/ts/capabilities/fact-log.ts +54 -0
- package/ts/capabilities/flyai-config.ts +388 -0
- package/ts/capabilities/flyai.ts +1274 -0
- package/ts/capabilities/hbcli.ts +253 -71
- package/ts/capabilities/incident-log.ts +88 -59
- package/ts/capabilities/itinerary-artifact.ts +214 -0
- package/ts/capabilities/itinerary-deck-artifact.ts +218 -0
- package/ts/capabilities/itinerary-deck-export.ts +378 -0
- package/ts/capabilities/opensky.ts +121 -0
- package/ts/capabilities/session/action-cache.ts +148 -0
- package/ts/capabilities/session/adapters/ctrip-flight.ts +181 -0
- package/ts/capabilities/session/adapters/ctrip-hotel.ts +238 -0
- package/ts/capabilities/session/adapters/dida-portal.ts +338 -0
- package/ts/capabilities/session/adapters/meituan-local.ts +70 -0
- package/ts/capabilities/session/adapters/rail-12306.ts +392 -0
- package/ts/capabilities/session/benchmark.ts +276 -0
- package/ts/capabilities/session/bridge-ledger.ts +333 -0
- package/ts/capabilities/session/extension-bridge.ts +794 -0
- package/ts/capabilities/session/extension-channel.ts +144 -0
- package/ts/capabilities/session/extension-distribution.ts +266 -0
- package/ts/capabilities/session/extract.ts +54 -0
- package/ts/capabilities/session/golden-score.ts +139 -0
- package/ts/capabilities/session/health-watch.ts +264 -0
- package/ts/capabilities/session/read-guard.ts +141 -0
- package/ts/capabilities/session/static-flight-golden.ts +209 -0
- package/ts/capabilities/session/transport.ts +146 -0
- package/ts/capabilities/session/wizard.ts +140 -0
- package/ts/capabilities/session-consent.ts +211 -0
- package/ts/capabilities/session-login.ts +208 -0
- package/ts/capabilities/session-search.ts +893 -0
- package/ts/capabilities/weather.ts +330 -0
- package/ts/dsh-runtime/vendor/README.md +68 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/LICENSE +21 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +405 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +404 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/client/client.js +1584 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/cordis.patch.yml +5 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/icon.svg +5 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/amap.js +595 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/nominatim.js +63 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/osrm.js +58 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/photon.js +55 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-file.js +87 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-route.js +131 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config.js +16 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/geo.js +232 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/index.js +120 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +40 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/staticmap-route.js +115 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/geocode.js +127 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/poi.js +84 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/routes.js +267 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/amap.d.ts +90 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/nominatim.d.ts +17 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/osrm.d.ts +15 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/photon.d.ts +22 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-file.d.ts +33 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-route.d.ts +23 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config.d.ts +23 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/geo.d.ts +61 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/index.d.ts +18 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +14 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/staticmap-route.d.ts +31 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/geocode.d.ts +11 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/poi.d.ts +9 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/routes.d.ts +75 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/types.d.ts +68 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types.js +16 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +97 -0
- package/ts/package.json +293 -3
- package/ts/scripts/async-collect.ts +91 -0
- package/ts/scripts/map-tools-vendor-package-proof.ts +404 -0
- package/ts/scripts/skeleton-check.ts +1 -1
- package/ts/scripts/skeleton-integration-test.ts +23 -1
- package/ts/scripts/state-cli.ts +568 -0
- package/ts/scripts/turn-handoff-collect.ts +178 -0
- package/ts/src/artifact-gate.ts +991 -0
- package/ts/src/benchmark-agent-conformance.ts +619 -0
- package/ts/src/benchmark-environment-bridge.ts +538 -0
- package/ts/src/benchmark-headless-child-diagnostics.ts +184 -0
- package/ts/src/benchmark-tool-isolation.ts +166 -0
- package/ts/src/bookable-facts.ts +837 -0
- package/ts/src/booking-saga.ts +133 -0
- package/ts/src/booking-surface/availability-policy.ts +522 -0
- package/ts/src/booking-surface/booking-intent.ts +86 -0
- package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
- package/ts/src/booking-surface/canonical-schema.js +116 -0
- package/ts/src/booking-surface/contracts.ts +475 -0
- package/ts/src/booking-surface/dsh-planner.ts +1949 -0
- package/ts/src/booking-surface/dsh-plugin.js +378 -0
- package/ts/src/booking-surface/dsh-worker.ts +110 -0
- package/ts/src/booking-surface/error-codes.ts +241 -0
- package/ts/src/booking-surface/index.ts +9 -0
- package/ts/src/booking-surface/managed-dsh-cleanup-diagnostic.ts +151 -0
- package/ts/src/booking-surface/managed-dsh-run-port.ts +273 -0
- package/ts/src/booking-surface/profile.ts +42 -0
- package/ts/src/booking-surface/recovery-chain.ts +409 -0
- package/ts/src/booking-surface/runtime.ts +1793 -0
- package/ts/src/booking-surface/server.ts +733 -0
- package/ts/src/booking-surface/startup.ts +201 -0
- package/ts/src/booking-surface/validation.ts +217 -0
- package/ts/src/companions.ts +113 -0
- package/ts/src/contracts.ts +13 -3
- package/ts/src/dsh-llm.ts +44 -11
- package/ts/src/flyai-setup-tool.ts +77 -0
- package/ts/src/index.ts +1948 -198
- package/ts/src/itinerary-deck.ts +189 -0
- package/ts/src/itinerary-doc-shared.ts +741 -0
- package/ts/src/itinerary-html.ts +171 -0
- package/ts/src/loop.ts +223 -25
- package/ts/src/memory-capture.ts +283 -0
- package/ts/src/memory-decay.ts +55 -0
- package/ts/src/memory-utility.ts +70 -0
- package/ts/src/mock-llm.ts +4 -8
- package/ts/src/model.ts +88 -2
- package/ts/src/recall/card.ts +106 -0
- package/ts/src/recall/evaluator.ts +170 -0
- package/ts/src/recall/integration.ts +35 -0
- package/ts/src/recall/tick.ts +213 -0
- package/ts/src/session-link/plan-it-action.ts +78 -0
- package/ts/src/session-link/session-link.ts +275 -0
- package/ts/src/share/adapters.ts +93 -0
- package/ts/src/share/share-consent.ts +105 -0
- package/ts/src/share/share-deck.ts +85 -0
- package/ts/src/share/share-token.ts +166 -0
- package/ts/src/slot-spec.ts +172 -0
- package/ts/src/state-ledger.ts +912 -0
- package/ts/src/subagent-job-id-guard.ts +72 -0
- package/ts/src/time-anchor.ts +338 -0
- package/ts/src/tool-packet.ts +32 -0
- package/ts/src/travel-slots.ts +219 -0
- package/ts/src/travel-timeline.ts +83 -0
- package/ts/src/turn-deadline.ts +366 -0
- package/ts/src/turn-policy.ts +154 -0
- package/ts/src/unified.ts +354 -88
- package/ts/src/wish-pool.ts +79 -0
- package/dist/scripts/session-attach-wait.js +0 -41
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recall condition evaluator(issue #577,Phase D;研究决策 §3 Phase D;
|
|
3
|
+
* 自检 review 修复见 PR #578 评论——两轮:广播升级/容器与时钟守卫/单表派生;
|
|
4
|
+
* #577 收口:条件资格门 + 命中证据,见 docs/design/recall-trigger.md §2)。
|
|
5
|
+
*
|
|
6
|
+
* 纯函数:给定 wish-pool 候选条目 + 当前上下文(时间 + 召回窗口 match_context +
|
|
7
|
+
* 各类简化信号),产出一组 RecallTrigger——每个含「为什么是现在」的具体原因
|
|
8
|
+
* (条件名 + 当前值 + 阈值)与实际命中的愿望条件(match_hits)与 source tag
|
|
9
|
+
* (**proactive with provenance**——研究文档红线)。
|
|
10
|
+
*
|
|
11
|
+
* 纪律:
|
|
12
|
+
* - 原因闭集 5 类,**单表派生**(RECALL_REASON_LABEL 为唯一事实源;union / 数组 /
|
|
13
|
+
* label 全部从它推导——新增原因 = 改一张表,编译器强制 card.ts 的 hint 表补全);
|
|
14
|
+
* - **条件资格门**:候选必须先过 scoreWishMatch(wish-pool.ts 原样复用,不复制
|
|
15
|
+
* 判定逻辑)——任一 days/budget/month 命中即有召回资格(既有语义:是召回资格,
|
|
16
|
+
* 不是「全部出行条件已满足」的证明);命名 down 通道否证。信号(含定向)不能
|
|
17
|
+
* 绕过资格门;match_context 缺省/畸形 → fail-closed 空返回,绝不降级为广播;
|
|
18
|
+
* - **职责边界**:信号是 producer 给的人话信号(current_value/threshold 是展示
|
|
19
|
+
* 字符串,不是类型化价格/日期事实),评估器不解析它们;资格事实只来自注入的
|
|
20
|
+
* match_context;
|
|
21
|
+
* - 畸形输入(信号/容器/时钟/条目)**跳过或空返回,不崩**——评估器是消费链一环,
|
|
22
|
+
* 一个坏 tick 不能杀掉调用方的 drain loop;
|
|
23
|
+
* - wish_ids 非数组(如字符串)= 畸形信号,**跳过**(绝不降级为广播匹配——
|
|
24
|
+
* 广播是最激进模式,降级等于扩权);
|
|
25
|
+
* - muted 过滤用 `muted === true` 严格判定(JSON 往返的 'false' 字符串不再误杀)。
|
|
26
|
+
*/
|
|
27
|
+
import { scoreWishMatch } from '../wish-pool.js';
|
|
28
|
+
/** 唯一事实源:5 类原因 → 中文标签。新增原因 = 在此表加一行(union/数组自动派生;
|
|
29
|
+
* card.ts 的 ACTION_HINT 表由 Record<RecallReason,...> 编译强制补全)。 */
|
|
30
|
+
export const RECALL_REASON_LABEL = {
|
|
31
|
+
holiday_proximity: '假期临近',
|
|
32
|
+
price_drop: '价格降至阈值',
|
|
33
|
+
weather_window: '天气窗口打开',
|
|
34
|
+
route_new: '新航线/班次开通',
|
|
35
|
+
availability_recovered: '通道恢复可用',
|
|
36
|
+
};
|
|
37
|
+
/** 闭集数组从 label 表派生(单一事实源;不再手抄第二份) */
|
|
38
|
+
export const RECALL_REASONS = Object.keys(RECALL_REASON_LABEL);
|
|
39
|
+
/**
|
|
40
|
+
* 评估:候选 × 信号 → 触发列表。
|
|
41
|
+
* 两道门,先资格后匹配:
|
|
42
|
+
* 1. **资格门**:候选非 muted + 有稳定非空 wish_id + scoreWishMatch(entry,
|
|
43
|
+
* match_context) 非空(任一条件命中,且无命名 down 通道否证)——信号匹配只在
|
|
44
|
+
* 有资格的候选上进行;
|
|
45
|
+
* 2. **匹配门**:signal.wish_ids 是非空字符串数组时按 id 精确匹配(定向);
|
|
46
|
+
* 为空/缺省时匹配全部**有资格**候选(广播)。**wish_ids 为其它真值类型
|
|
47
|
+
* (字符串/对象等)视为畸形信号,整条跳过**——绝不降级为广播。
|
|
48
|
+
* 容器/时钟/上下文守卫:signals/pool 非数组、ctx.now 非法、match_context 缺省或
|
|
49
|
+
* 畸形 → 返回 [](不抛,不崩 drain loop,不广播扩权)。
|
|
50
|
+
*/
|
|
51
|
+
export function evaluateRecallTriggers(pool, ctx) {
|
|
52
|
+
// 容器与时钟守卫:一个坏 tick 不能杀掉调用方(fail-closed 空返回)
|
|
53
|
+
if (!Array.isArray(pool) || !Array.isArray(ctx?.signals))
|
|
54
|
+
return [];
|
|
55
|
+
const nowMs = ctx.now instanceof Date ? ctx.now.getTime() : Number.NaN;
|
|
56
|
+
if (!Number.isFinite(nowMs))
|
|
57
|
+
return [];
|
|
58
|
+
// 条件上下文守卫:缺省/畸形 → fail-closed(绝不降级为「全部广播」——
|
|
59
|
+
// 没有窗口证据就广播「现在可以去」正是 #577 要堵住的扩权)
|
|
60
|
+
if (!isWishMatchContext(ctx.match_context))
|
|
61
|
+
return [];
|
|
62
|
+
const evaluatedAt = new Date(nowMs).toISOString();
|
|
63
|
+
// 资格门:scoreWishMatch 非空(≥1 项 days/budget/month 命中,且无命名通道 down 否证)
|
|
64
|
+
// 才进入候选;保持 pool 原序(广播/定向的输出顺序与既有行为一致)
|
|
65
|
+
const eligible = [];
|
|
66
|
+
for (const e of pool) {
|
|
67
|
+
if (e?.muted === true || typeof e?.wish_id !== 'string' || e.wish_id.length === 0)
|
|
68
|
+
continue;
|
|
69
|
+
const match = scoreCandidate(e, ctx.match_context);
|
|
70
|
+
if (match !== null)
|
|
71
|
+
eligible.push({ entry: e, match });
|
|
72
|
+
}
|
|
73
|
+
const triggers = [];
|
|
74
|
+
for (const signal of ctx.signals) {
|
|
75
|
+
if (!isRecallSignal(signal))
|
|
76
|
+
continue;
|
|
77
|
+
const targets = Array.isArray(signal.wish_ids) && signal.wish_ids.length > 0
|
|
78
|
+
? eligible.filter(({ entry }) => signal.wish_ids.includes(entry.wish_id))
|
|
79
|
+
: eligible;
|
|
80
|
+
for (const { entry, match } of targets) {
|
|
81
|
+
triggers.push({
|
|
82
|
+
wish_id: String(entry.wish_id),
|
|
83
|
+
wish_name: typeof entry.name === 'string' && entry.name.length > 0 ? entry.name : String(entry.wish_id),
|
|
84
|
+
signal,
|
|
85
|
+
evaluated_at: evaluatedAt,
|
|
86
|
+
match_hits: [...match.hits],
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return triggers;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* 资格判定:scoreWishMatch 原样复用(唯一判定层,不复制判定/阈值逻辑)。
|
|
94
|
+
* 意外抛错(运行时畸形形态逃过静态与结构检查)→ 该条目无资格,不崩——
|
|
95
|
+
* 单个坏条目不能杀掉整轮评估。
|
|
96
|
+
*/
|
|
97
|
+
function scoreCandidate(entry, ctx) {
|
|
98
|
+
try {
|
|
99
|
+
return scoreWishMatch(entry, ctx);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** match_context 运行时校验(JSON 往返边界强制):非数组对象 + 数值字段(若给出)
|
|
106
|
+
* 必须是有限数字 + channelDown(若给出非 null)必须具备 has 方法(Set 形)。
|
|
107
|
+
* 与 scoreWishMatch 的缺省语义对齐:channelDown 缺省/null = 不启用通道否证。 */
|
|
108
|
+
function isWishMatchContext(value) {
|
|
109
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
110
|
+
return false;
|
|
111
|
+
const c = value;
|
|
112
|
+
for (const k of ['days', 'budgetCny', 'month']) {
|
|
113
|
+
const v = c[k];
|
|
114
|
+
if (v !== undefined && (typeof v !== 'number' || !Number.isFinite(v)))
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
const down = c.channelDown;
|
|
118
|
+
if (down !== undefined && down !== null && typeof down.has !== 'function')
|
|
119
|
+
return false;
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
/** 信号校验(边界强制,不从输入信任):reason 在闭集 + source 非空字符串 +
|
|
123
|
+
* current_value/threshold 为字符串 + **wish_ids 一旦给出必须是字符串数组**
|
|
124
|
+
* (字符串等真值非数组 = 畸形,拒绝——绝不降级广播)。 */
|
|
125
|
+
function isRecallSignal(value) {
|
|
126
|
+
if (typeof value !== 'object' || value === null)
|
|
127
|
+
return false;
|
|
128
|
+
const s = value;
|
|
129
|
+
if (!RECALL_REASONS.includes(s.reason))
|
|
130
|
+
return false;
|
|
131
|
+
if (typeof s.source !== 'string' || s.source.length === 0)
|
|
132
|
+
return false;
|
|
133
|
+
if (typeof s.current_value !== 'string' || typeof s.threshold !== 'string')
|
|
134
|
+
return false;
|
|
135
|
+
if (s.wish_ids !== undefined && s.wish_ids !== null) {
|
|
136
|
+
if (!Array.isArray(s.wish_ids) || !s.wish_ids.every(id => typeof id === 'string'))
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recall wish-pool integration(issue #577,Phase D)。
|
|
3
|
+
*
|
|
4
|
+
* 把 wish-pool 候选条目接到 evaluator:从 tick + signals 组装上下文 → 跑评估 →
|
|
5
|
+
* 产触发列表 + why-now 卡。**只读 wish-pool**(不调用 mutation;撤回/通知留 M4)。
|
|
6
|
+
*
|
|
7
|
+
* 本模块不接 IO——数据进出全靠参数(测试全离线;M4 由真实 tick source + sensor 喂数据)。
|
|
8
|
+
*/
|
|
9
|
+
import { evaluateRecallTriggers } from './evaluator.js';
|
|
10
|
+
import { buildWhyNowCard } from './card.js';
|
|
11
|
+
/** 从 wish-pool + tick + signals + 召回窗口跑一轮评估并产卡(纯函数编排) */
|
|
12
|
+
export function evaluatePoolRecall(input) {
|
|
13
|
+
const ctx = { now: input.tick.at, signals: input.signals, match_context: input.match_context };
|
|
14
|
+
const triggers = evaluateRecallTriggers(input.pool, ctx);
|
|
15
|
+
const cards = triggers.map(buildWhyNowCard);
|
|
16
|
+
return { triggers, cards };
|
|
17
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recall tick scheduler(issue #577,Phase D;研究决策见
|
|
3
|
+
* docs/research/karpo-deck-web-research.md §3 Phase D;
|
|
4
|
+
* seam 约束见 docs/design/external-event-seam.md §3.3/§4——**pull model,M5 前无 push**;
|
|
5
|
+
* 自检 review 两轮修复见 PR #578 评论)。
|
|
6
|
+
*
|
|
7
|
+
* 设计契约:
|
|
8
|
+
* - TickSource:`next()` **运行中阻塞到下一个 tick 可用**;**未运行(未启动或已
|
|
9
|
+
* stop)返回 null = 流终结**——drain-loop 调用方安全退出,永不悬挂;
|
|
10
|
+
* - PeriodicTickSource:**默认 enabled=false**(显式 opt-in);start() 三态
|
|
11
|
+
* ('started'|'disabled'|'already-running');timer.unref()(不拽住事件循环);
|
|
12
|
+
* pending 有界(maxPending 默认 100,溢出丢最旧);**stop() 清空 pending 并把
|
|
13
|
+
* 全部等待者以 null 唤醒**(终结核平——消费协程不泄漏,重启不吐陈旧 tick);
|
|
14
|
+
* - RecallTickScheduler:**永不抛错**——evaluate/toCard/deliver 全部容错:
|
|
15
|
+
* evaluate 抛错 → {delivered:0, failed:0, error};单卡 toCard/deliver 抛错 →
|
|
16
|
+
* 只弃该卡(failed++ 并记 errors),余下继续;返回 {delivered, failed, errors?};
|
|
17
|
+
* - **不推送**:sink 是注入的下游,不是 notification/push 通道(seam §4)。
|
|
18
|
+
*/
|
|
19
|
+
/** 测试用 InMemory tick source:预排好的一组 tick,按序返回,耗尽返回 null。
|
|
20
|
+
* 构造与 next() 均防御式深拷贝(调用方改返回的 tick.at 不会污染队列快照)。 */
|
|
21
|
+
export class InMemoryTickSource {
|
|
22
|
+
queue;
|
|
23
|
+
constructor(ticks) {
|
|
24
|
+
this.queue = ticks.map(t => ({ at: new Date(t.at.getTime()), source: t.source }));
|
|
25
|
+
}
|
|
26
|
+
async next() {
|
|
27
|
+
const t = this.queue.shift();
|
|
28
|
+
if (t === undefined)
|
|
29
|
+
return null;
|
|
30
|
+
return { at: new Date(t.at.getTime()), source: t.source }; // 返回副本,调用方可安全改
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 周期 tick source(默认关闭):封装 Node setInterval。
|
|
35
|
+
* - enabled=false 构造默认,显式 opt-in;start() 三态返回(不混同);
|
|
36
|
+
* - next() 语义:**运行中**阻塞到下个 tick;**未运行**(disabled/未 start/已 stop)
|
|
37
|
+
* 立即返回 null(流终结——drain-loop 安全退出,永不悬挂);
|
|
38
|
+
* - stop() 幂等:**清空 pending(重启不吐陈旧 tick)+ 把全部等待者以 null 唤醒**
|
|
39
|
+
* (消费协程不泄漏);
|
|
40
|
+
* - timer.unref():忘了 stop() 也不拽住进程事件循环;
|
|
41
|
+
* - pending 有界(maxPending,溢出丢最旧)。
|
|
42
|
+
*/
|
|
43
|
+
export class PeriodicTickSource {
|
|
44
|
+
enabled;
|
|
45
|
+
intervalMs;
|
|
46
|
+
sourceLabel;
|
|
47
|
+
maxPending;
|
|
48
|
+
timer = null;
|
|
49
|
+
pending = [];
|
|
50
|
+
waiters = [];
|
|
51
|
+
constructor(opts) {
|
|
52
|
+
this.enabled = opts.enabled === true;
|
|
53
|
+
this.intervalMs = opts.intervalMs;
|
|
54
|
+
this.sourceLabel = opts.source ?? 'periodic';
|
|
55
|
+
this.maxPending = opts.maxPending ?? 100;
|
|
56
|
+
if (!Number.isFinite(this.intervalMs) || this.intervalMs <= 0) {
|
|
57
|
+
throw new Error(`PeriodicTickSource intervalMs 必须为正数(实测 ${this.intervalMs})`);
|
|
58
|
+
}
|
|
59
|
+
if (!Number.isInteger(this.maxPending) || this.maxPending < 1) {
|
|
60
|
+
throw new Error(`PeriodicTickSource maxPending 必须为 ≥1 整数(实测 ${this.maxPending})`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** 启动内部 setInterval;三态返回(见类注释)。已启动时幂等。 */
|
|
64
|
+
start() {
|
|
65
|
+
if (!this.enabled)
|
|
66
|
+
return 'disabled';
|
|
67
|
+
if (this.timer !== null)
|
|
68
|
+
return 'already-running';
|
|
69
|
+
this.timer = setInterval(() => {
|
|
70
|
+
const tick = { at: new Date(), source: this.sourceLabel };
|
|
71
|
+
// 优先唤醒等待者;无等待者入有界队列(溢出丢最旧)
|
|
72
|
+
const waiter = this.waiters.shift();
|
|
73
|
+
if (waiter !== undefined) {
|
|
74
|
+
waiter(tick);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (this.pending.length >= this.maxPending)
|
|
78
|
+
this.pending.shift();
|
|
79
|
+
this.pending.push(tick);
|
|
80
|
+
}, this.intervalMs);
|
|
81
|
+
this.timer.unref?.();
|
|
82
|
+
return 'started';
|
|
83
|
+
}
|
|
84
|
+
/** 停止(幂等):清 timer + **清空 pending(重启不吐陈旧 tick)+ 等待者全部以 null 唤醒**(终结核平) */
|
|
85
|
+
stop() {
|
|
86
|
+
if (this.timer !== null) {
|
|
87
|
+
clearInterval(this.timer);
|
|
88
|
+
this.timer = null;
|
|
89
|
+
}
|
|
90
|
+
this.pending = [];
|
|
91
|
+
const waiters = this.waiters;
|
|
92
|
+
this.waiters = [];
|
|
93
|
+
for (const w of waiters)
|
|
94
|
+
w(null);
|
|
95
|
+
}
|
|
96
|
+
/** 当前 pending 队列长度(只读;测试与诊断用) */
|
|
97
|
+
get pendingLength() {
|
|
98
|
+
return this.pending.length;
|
|
99
|
+
}
|
|
100
|
+
/** **运行中**阻塞到下一个 tick;**未运行**(disabled/未 start/已 stop)返回 null(流终结) */
|
|
101
|
+
async next() {
|
|
102
|
+
if (this.timer === null)
|
|
103
|
+
return null;
|
|
104
|
+
const queued = this.pending.shift();
|
|
105
|
+
if (queued !== undefined)
|
|
106
|
+
return queued;
|
|
107
|
+
return new Promise(resolve => {
|
|
108
|
+
this.waiters.push(resolve);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** 数组收集 sink(测试用) */
|
|
113
|
+
export class ArrayRecallSink {
|
|
114
|
+
items = [];
|
|
115
|
+
deliver(card) {
|
|
116
|
+
this.items.push(card);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* 编排器:拉 tick → 评估 → 转卡 → 送 sink。
|
|
121
|
+
* 每次 run 处理一个 tick(调用方决定循环;scheduler 自己不循环——不悄悄长驻)。
|
|
122
|
+
* **永不抛错**:evaluate 抛错 → {delivered:0, failed:0, error 记入 errors};
|
|
123
|
+
* 单卡 toCard/deliver 抛错 → 只弃该卡(failed++/errors 记一条),余下继续。
|
|
124
|
+
*/
|
|
125
|
+
export class RecallTickScheduler {
|
|
126
|
+
deps;
|
|
127
|
+
constructor(deps) {
|
|
128
|
+
this.deps = deps;
|
|
129
|
+
}
|
|
130
|
+
async run(tick) {
|
|
131
|
+
const errors = [];
|
|
132
|
+
let evaluations;
|
|
133
|
+
try {
|
|
134
|
+
evaluations = this.deps.evaluate(tick);
|
|
135
|
+
if (!Array.isArray(evaluations))
|
|
136
|
+
evaluations = [];
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
return { delivered: 0, failed: 0, errors: [`evaluate: ${err?.message ?? String(err)}`] };
|
|
140
|
+
}
|
|
141
|
+
let delivered = 0;
|
|
142
|
+
let failed = 0;
|
|
143
|
+
for (const evaluation of evaluations) {
|
|
144
|
+
let card;
|
|
145
|
+
try {
|
|
146
|
+
card = this.deps.toCard(evaluation, tick);
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
failed++;
|
|
150
|
+
if (errors.length < 4)
|
|
151
|
+
errors.push(`toCard: ${err?.message ?? String(err)}`);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
await this.deps.sink.deliver(card);
|
|
156
|
+
delivered++;
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
failed++;
|
|
160
|
+
if (errors.length < 4)
|
|
161
|
+
errors.push(`deliver: ${err?.message ?? String(err)}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return errors.length > 0 ? { delivered, failed, errors } : { delivered, failed };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan-it 行动卡(issue #580,Phase E)——why-now 卡(Phase D)的一键行动
|
|
3
|
+
* 面:「好,规划它」深链回本地 session 继续规划该 wish。**只打开,不写**:
|
|
4
|
+
* 行动词位只有 plan_it(Continue planning),booking/payment 不在链接词位
|
|
5
|
+
* (session-link.ts 的 action 闭集),WriteGate 保持 sealed。
|
|
6
|
+
*
|
|
7
|
+
* 纪律:
|
|
8
|
+
* - 行动卡是数据(不是渲染 HTML);渲染面(deck / 对话 / 通知)各自消费;
|
|
9
|
+
* - `wish_id` 与 token payload **同源构造**(同一个值进两处),不可能漂移——
|
|
10
|
+
* 渲染面显示的 wish 与链接打开的 wish 是构造级同一个;
|
|
11
|
+
* - label 是封闭词汇(「好,规划它」改措辞 = 契约变更,先 review);
|
|
12
|
+
* - why-now 卡的「not_now」是渲染面本地消解(无链接、无状态写),
|
|
13
|
+
* 本契约不覆盖(文档声明)。
|
|
14
|
+
*/
|
|
15
|
+
import { formatSessionLink, signSessionLink } from './session-link.js';
|
|
16
|
+
/** plan-it 行动文案(封闭词汇) */
|
|
17
|
+
export const PLAN_IT_LABEL = '好,规划它';
|
|
18
|
+
/**
|
|
19
|
+
* why-now 卡 → plan-it 行动卡。wish_id 取卡上的结构化字段(Phase E 起
|
|
20
|
+
* WhyNowCard 必含 wish_id——从 title 字符串反解不可接受)。
|
|
21
|
+
* 非法输入(wish_id 缺失/空、session_ref 路径化)抛错:行动卡构造层
|
|
22
|
+
* 即 fail-closed,不产「点了不知道开什么」的链接。
|
|
23
|
+
*/
|
|
24
|
+
export function buildPlanItAction(card, deps) {
|
|
25
|
+
if (typeof card?.wish_id !== 'string' || card.wish_id.length === 0) {
|
|
26
|
+
throw new Error('buildPlanItAction:card.wish_id 必须是非空字符串(WhyNowCard 结构化自指其 wish 是 plan-it 行动的前提)');
|
|
27
|
+
}
|
|
28
|
+
if (typeof deps?.session_ref !== 'string' || deps.session_ref.length === 0) {
|
|
29
|
+
throw new Error(`buildPlanItAction:session_ref 必须是非空字符串(实测 ${String(deps?.session_ref)})`);
|
|
30
|
+
}
|
|
31
|
+
const now = deps.now ?? (() => new Date());
|
|
32
|
+
const payload = {
|
|
33
|
+
link_id: deps.link_id ?? `plan-${now().getTime().toString(36)}-${Math.random().toString(36).slice(2, 10)}`,
|
|
34
|
+
session_ref: deps.session_ref,
|
|
35
|
+
action: 'plan_it',
|
|
36
|
+
wish_id: card.wish_id,
|
|
37
|
+
created_at: now().toISOString(),
|
|
38
|
+
ttl_seconds: deps.ttl_seconds ?? 86_400,
|
|
39
|
+
};
|
|
40
|
+
// wish_id 同源:payload.wish_id 就是上面 card.wish_id 这一个值
|
|
41
|
+
const token = signSessionLink(payload, deps.secret);
|
|
42
|
+
return {
|
|
43
|
+
kind: 'plan_it',
|
|
44
|
+
label: PLAN_IT_LABEL,
|
|
45
|
+
deep_link: formatSessionLink(token, deps.base),
|
|
46
|
+
wish_id: card.wish_id,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session link token 签发/校验(issue #580,Phase E;研究决策见
|
|
3
|
+
* docs/research/karpo-deck-web-research.md §3 Phase E——one-tap 深链回本地
|
|
4
|
+
* session,不碰写路径,WriteGate 保持 sealed;消费端注册是 M4 激活)。
|
|
5
|
+
*
|
|
6
|
+
* HMAC-SHA256 over base64url(payload-json),签名为 base64url。
|
|
7
|
+
* Token 形如 `${body}.${sig}`(body = base64url(JSON.stringify(payload)))。
|
|
8
|
+
*
|
|
9
|
+
* 与 share-token(Phase C)的差异(刻意,非偶然):
|
|
10
|
+
* - **schema 闭集到未知键拒绝**:verify 对 payload 里出现 schema 之外的键
|
|
11
|
+
* 直接 link_invalid——session link 是「de-identified by construction」面,
|
|
12
|
+
* 链接只携带引用(link_id/session_ref/wish_id)不携带事实/记忆,闭集
|
|
13
|
+
* schema 让这句承诺变成可测试字面真(share token 允许附加键,因为 share
|
|
14
|
+
* manifest 本身就需要开放扩展;两者威胁模型不同,不共享校验器);
|
|
15
|
+
* - **action 词位闭集 = WriteGate 链接层结构性封死**:`plan_it` | `open`,
|
|
16
|
+
* 词位里不存在 book/pay/confirm——任何写动词连构造都构造不出来
|
|
17
|
+
* (sign 层抛错,不只是 verify 层拒绝);
|
|
18
|
+
* - **plan_it ⇔ wish_id 成对**:`plan_it` 必须带非空 wish_id(防「规划
|
|
19
|
+
* 空 wish」退化),`open` 必须不带(防通用打开链接夹带定向意图);
|
|
20
|
+
* - **session_ref 路径护栏**:不透明引用,拒绝含 `/` `\` `..` NUL——
|
|
21
|
+
* 「不写共享状态」的链接层版:链接永远无法命名 state 路径;
|
|
22
|
+
* - 失败闭集只有 2 类(`link_invalid` | `link_expired`)——session link
|
|
23
|
+
* 无 channel/adapter/consent 维度,不需要 share 的 8 类;
|
|
24
|
+
* - token 不进 URL:scheme/宿主是消费端渲染期选择(`formatSessionLink`),
|
|
25
|
+
* 链接产物跨消费端可移植,签名载荷不含宿主细节。
|
|
26
|
+
*
|
|
27
|
+
* 硬化清单与 share-token 同源:恰一个点、timingSafeEqual(先长度)、
|
|
28
|
+
* ttl 非负整数上限 2^31-1(防 expiresMs 溢出 Infinity 永不过期)、
|
|
29
|
+
* 时间注入(测试不依赖 wall clock)、secret 缺省且 NODE_ENV=production
|
|
30
|
+
* 时 fail-closed(绝不静默用仓库里公开的 dev 常量签生产链接)——刻意抛错
|
|
31
|
+
* 保留在签发面(resolve/sign);verify 面把同样的配置错误收敛为 link_invalid,
|
|
32
|
+
* 不让异常越出「永不抛错」的校验边界。注入时钟本身失效(getTime 非有限值
|
|
33
|
+
* 或回调抛错)→ fail-closed link_invalid:NaN 与 expiresMs 比较恒 false,
|
|
34
|
+
* 曾把过期链接静默判成有效——无效时间上不产生任何接受。verify 期 secret
|
|
35
|
+
* 为非字符串运行期键(数字/对象/Symbol 等 JS 畸形输入)同样收敛 link_invalid,
|
|
36
|
+
* HMAC 计算本身也在守卫内——任何调用方/配置输入都无法越出「永不抛错」边界。
|
|
37
|
+
*/
|
|
38
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
39
|
+
/** 行动闭集:`plan_it`(回 session 继续规划某条 wish)| `open`(通用打开)。
|
|
40
|
+
* **词位即安全边界**——新增行动先 review;book/pay/confirm 等写动词
|
|
41
|
+
* 永不入集(WriteGate sealed 在链接层的结构性表达)。 */
|
|
42
|
+
export const SESSION_LINK_ACTIONS = ['plan_it', 'open'];
|
|
43
|
+
/** 显式 dev 默认 secret——生产环境必须由 `SESSION_LINK_HMAC_SECRET` env 覆盖 */
|
|
44
|
+
export const DEFAULT_DEV_SESSION_LINK_SECRET = 'gotry-session-link-dev-secret-DO-NOT-USE-IN-PROD';
|
|
45
|
+
/** ttl 上界(秒):2^31-1,防 created_at + ttl*1000 溢出 Number 上限 */
|
|
46
|
+
export const SESSION_LINK_TTL_MAX_SECONDS = 2_147_483_647;
|
|
47
|
+
/** payload schema 的键闭集(verify 用:未知键 → link_invalid) */
|
|
48
|
+
const PAYLOAD_KEYS = ['link_id', 'session_ref', 'action', 'wish_id', 'created_at', 'ttl_seconds'];
|
|
49
|
+
/** 获取运行期 secret:env 优先;production 缺 env 时 fail-closed */
|
|
50
|
+
export function resolveSessionLinkSecret(env = process.env) {
|
|
51
|
+
const s = env.SESSION_LINK_HMAC_SECRET;
|
|
52
|
+
if (typeof s === 'string' && s.length > 0)
|
|
53
|
+
return s;
|
|
54
|
+
if (env.NODE_ENV === 'production') {
|
|
55
|
+
throw new Error('SESSION_LINK_HMAC_SECRET 未设置且 NODE_ENV=production:拒绝回落公开的 dev 默认 secret(fail-closed;任何读到仓库的人都能伪造 session link)');
|
|
56
|
+
}
|
|
57
|
+
return DEFAULT_DEV_SESSION_LINK_SECRET;
|
|
58
|
+
}
|
|
59
|
+
/** session_ref 路径护栏:不透明引用——含路径分隔/上跳/NUL 即拒(链接永远无法命名 state 路径) */
|
|
60
|
+
function isSafeSessionRef(value) {
|
|
61
|
+
return typeof value === 'string' && value.length > 0 && value.length <= 128
|
|
62
|
+
&& !value.includes('/') && !value.includes('\\') && !value.includes('..')
|
|
63
|
+
&& !value.includes('\0');
|
|
64
|
+
}
|
|
65
|
+
/** 签发:payload 校验(闭集 action / plan_it⇔wish_id 成对 / session_ref 护栏 / ttl 上界)
|
|
66
|
+
* 不过期检查由 verify 做(签名时刻的时钟是消费端的)。非法 payload 抛错——
|
|
67
|
+
* 构造层把坏链接挡在签名之前,不是等到校验才发现。 */
|
|
68
|
+
export function signSessionLink(payload, secret = resolveSessionLinkSecret()) {
|
|
69
|
+
if (typeof payload !== 'object' || payload === null) {
|
|
70
|
+
throw new Error(`signSessionLink:payload 必须是对象(实测 ${String(payload)})`);
|
|
71
|
+
}
|
|
72
|
+
if (typeof payload.link_id !== 'string' || payload.link_id.length === 0) {
|
|
73
|
+
throw new Error('signSessionLink:link_id 必须是非空字符串');
|
|
74
|
+
}
|
|
75
|
+
if (!isSafeSessionRef(payload.session_ref)) {
|
|
76
|
+
throw new Error(`signSessionLink:session_ref 必须是不含路径分隔符的不透明引用(实测 ${String(payload.session_ref)})`);
|
|
77
|
+
}
|
|
78
|
+
if (!SESSION_LINK_ACTIONS.includes(payload.action)) {
|
|
79
|
+
throw new Error(`signSessionLink:action 不在闭集(实测 ${String(payload.action)};允许:${SESSION_LINK_ACTIONS.join(' / ')})。写动词(book/pay/confirm)永不入集——WriteGate 在链接层结构性封死`);
|
|
80
|
+
}
|
|
81
|
+
// plan_it ⇔ wish_id 成对(双向:plan_it 必须有目标;open 必须不夹带定向意图)
|
|
82
|
+
if (payload.action === 'plan_it' && (typeof payload.wish_id !== 'string' || payload.wish_id.length === 0)) {
|
|
83
|
+
throw new Error('signSessionLink:action=plan_it 必须携带非空 wish_id(否则「规划」无对象,是退化链接)');
|
|
84
|
+
}
|
|
85
|
+
if (payload.action === 'open' && payload.wish_id !== undefined) {
|
|
86
|
+
throw new Error('signSessionLink:action=open 必须不携带 wish_id(通用打开链接夹带定向意图 = 混淆行动语义)');
|
|
87
|
+
}
|
|
88
|
+
if (typeof payload.created_at !== 'string' || payload.created_at.length === 0 || !Number.isFinite(Date.parse(payload.created_at))) {
|
|
89
|
+
throw new Error('signSessionLink:created_at 必须是可解析的 ISO 时间字符串');
|
|
90
|
+
}
|
|
91
|
+
if (typeof payload.ttl_seconds !== 'number' || !Number.isInteger(payload.ttl_seconds)
|
|
92
|
+
|| payload.ttl_seconds < 0 || payload.ttl_seconds > SESSION_LINK_TTL_MAX_SECONDS) {
|
|
93
|
+
throw new Error(`signSessionLink:ttl_seconds 必须为 0..${SESSION_LINK_TTL_MAX_SECONDS} 的整数(实测 ${String(payload.ttl_seconds)})`);
|
|
94
|
+
}
|
|
95
|
+
// 序列化前剥掉 undefined 键(wish_id 缺省时),保证 body 可复现
|
|
96
|
+
const clean = {};
|
|
97
|
+
const raw = payload;
|
|
98
|
+
for (const k of PAYLOAD_KEYS) {
|
|
99
|
+
const v = raw[k];
|
|
100
|
+
if (v !== undefined)
|
|
101
|
+
clean[k] = v;
|
|
102
|
+
}
|
|
103
|
+
const body = Buffer.from(JSON.stringify(clean), 'utf-8').toString('base64url');
|
|
104
|
+
const sig = createHmac('sha256', secret).update(body).digest('base64url');
|
|
105
|
+
return `${body}.${sig}`;
|
|
106
|
+
}
|
|
107
|
+
/** 校验:secret 解析(守卫体)→ 格式(恰一个点)→ HMAC 等值(timingSafeEqual)
|
|
108
|
+
* → schema 闭集(未知键拒绝)→ 字段形态 → plan_it⇔wish_id 成对 → session_ref
|
|
109
|
+
* 护栏 → 过期(注入时钟失效 = link_invalid)。**永不抛错**:production 缺 env
|
|
110
|
+
* secret 与失效时钟都是操作面错误,收敛进 link_invalid(消费端拿到 link_invalid
|
|
111
|
+
* 只能 fail-closed 不打开任何 session),刻意的配置/签发异常只保留在 resolve/sign 侧。 */
|
|
112
|
+
export function verifySessionLink(token, secret, now = () => new Date()) {
|
|
113
|
+
// 运行期 secret 解析放进守卫体:production 缺 env 的配置错在验证面 fail-closed
|
|
114
|
+
// 为 link_invalid,不抛错(若静默回落公开 dev 常量,伪造链接将被放行);
|
|
115
|
+
// 非字符串运行期键(数字/对象/Symbol 等 JS 畸形调用方输入)同样收敛
|
|
116
|
+
// link_invalid——createHmac 对它们会抛 TypeError,绝不让异常越出本函数
|
|
117
|
+
let hmacSecret;
|
|
118
|
+
try {
|
|
119
|
+
const resolved = secret === undefined ? resolveSessionLinkSecret() : secret;
|
|
120
|
+
if (typeof resolved !== 'string')
|
|
121
|
+
return { ok: false, reason: 'link_invalid' };
|
|
122
|
+
hmacSecret = resolved;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return { ok: false, reason: 'link_invalid' };
|
|
126
|
+
}
|
|
127
|
+
if (typeof token !== 'string' || token.length === 0)
|
|
128
|
+
return { ok: false, reason: 'link_invalid' };
|
|
129
|
+
const dot = token.indexOf('.');
|
|
130
|
+
// 恰一个点:多点(base64url 解码器会静默吞非法字符)与无点都拒绝
|
|
131
|
+
if (dot <= 0 || dot === token.length - 1)
|
|
132
|
+
return { ok: false, reason: 'link_invalid' };
|
|
133
|
+
if (token.indexOf('.', dot + 1) !== -1)
|
|
134
|
+
return { ok: false, reason: 'link_invalid' };
|
|
135
|
+
const body = token.slice(0, dot);
|
|
136
|
+
const sig = token.slice(dot + 1);
|
|
137
|
+
// HMAC 等值比较(先长度,再 timingSafeEqual;Buffer-vs-Buffer);加密计算
|
|
138
|
+
// 本身也在守卫内——任何运行期键异常收敛 link_invalid,绝不外泄异常
|
|
139
|
+
let expected;
|
|
140
|
+
try {
|
|
141
|
+
expected = createHmac('sha256', hmacSecret).update(body).digest();
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return { ok: false, reason: 'link_invalid' };
|
|
145
|
+
}
|
|
146
|
+
let actual;
|
|
147
|
+
try {
|
|
148
|
+
actual = Buffer.from(sig, 'base64url');
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return { ok: false, reason: 'link_invalid' };
|
|
152
|
+
}
|
|
153
|
+
if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) {
|
|
154
|
+
return { ok: false, reason: 'link_invalid' };
|
|
155
|
+
}
|
|
156
|
+
// 解析 payload
|
|
157
|
+
let payload;
|
|
158
|
+
try {
|
|
159
|
+
const json = Buffer.from(body, 'base64url').toString('utf-8');
|
|
160
|
+
payload = JSON.parse(json);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return { ok: false, reason: 'link_invalid' };
|
|
164
|
+
}
|
|
165
|
+
if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) {
|
|
166
|
+
return { ok: false, reason: 'link_invalid' };
|
|
167
|
+
}
|
|
168
|
+
// schema 闭集:未知键拒绝(de-identified by construction 的可测试形式)
|
|
169
|
+
for (const key of Object.keys(payload)) {
|
|
170
|
+
if (!PAYLOAD_KEYS.includes(key))
|
|
171
|
+
return { ok: false, reason: 'link_invalid' };
|
|
172
|
+
}
|
|
173
|
+
// 字段形态
|
|
174
|
+
if (typeof payload.link_id !== 'string' || payload.link_id.length === 0
|
|
175
|
+
|| !isSafeSessionRef(payload.session_ref)
|
|
176
|
+
|| typeof payload.action !== 'string' || !SESSION_LINK_ACTIONS.includes(payload.action)
|
|
177
|
+
|| typeof payload.created_at !== 'string' || payload.created_at.length === 0) {
|
|
178
|
+
return { ok: false, reason: 'link_invalid' };
|
|
179
|
+
}
|
|
180
|
+
// plan_it ⇔ wish_id 成对(与 sign 同规则,双向)
|
|
181
|
+
if (payload.action === 'plan_it' && (typeof payload.wish_id !== 'string' || payload.wish_id.length === 0)) {
|
|
182
|
+
return { ok: false, reason: 'link_invalid' };
|
|
183
|
+
}
|
|
184
|
+
if (payload.action === 'open' && payload.wish_id !== undefined) {
|
|
185
|
+
return { ok: false, reason: 'link_invalid' };
|
|
186
|
+
}
|
|
187
|
+
// ttl:非负整数且不溢出(防 created_at + ttl*1000 = Infinity 永不过期)
|
|
188
|
+
const ttl = payload.ttl_seconds;
|
|
189
|
+
if (typeof ttl !== 'number' || !Number.isInteger(ttl) || ttl < 0 || ttl > SESSION_LINK_TTL_MAX_SECONDS) {
|
|
190
|
+
return { ok: false, reason: 'link_invalid' };
|
|
191
|
+
}
|
|
192
|
+
const createdMs = Date.parse(payload.created_at);
|
|
193
|
+
if (!Number.isFinite(createdMs))
|
|
194
|
+
return { ok: false, reason: 'link_invalid' };
|
|
195
|
+
const expiresMs = createdMs + ttl * 1000;
|
|
196
|
+
if (!Number.isFinite(expiresMs))
|
|
197
|
+
return { ok: false, reason: 'link_invalid' };
|
|
198
|
+
// 注入时钟失效(抛错/非有限值)fail-closed 为 link_invalid——
|
|
199
|
+
// NaN 与 expiresMs 比较恒 false,失效时钟不得让任何链接通过
|
|
200
|
+
let nowMs;
|
|
201
|
+
try {
|
|
202
|
+
nowMs = now().getTime();
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return { ok: false, reason: 'link_invalid' };
|
|
206
|
+
}
|
|
207
|
+
if (!Number.isFinite(nowMs))
|
|
208
|
+
return { ok: false, reason: 'link_invalid' };
|
|
209
|
+
if (nowMs > expiresMs)
|
|
210
|
+
return { ok: false, reason: 'link_expired' };
|
|
211
|
+
return { ok: true, payload: payload };
|
|
212
|
+
}
|
|
213
|
+
/** 默认深链 base(scheme handler 注册是 M4 激活;消费端可传自己的 base) */
|
|
214
|
+
export const DEFAULT_SESSION_LINK_BASE = 'gotry://session';
|
|
215
|
+
/** 渲染期拼 URL:`${base}/${token}`——token 不进 URL 的签名,宿主是消费端选择 */
|
|
216
|
+
export function formatSessionLink(token, base = DEFAULT_SESSION_LINK_BASE) {
|
|
217
|
+
if (typeof token !== 'string' || token.length === 0) {
|
|
218
|
+
throw new Error('formatSessionLink:token 必须是非空字符串');
|
|
219
|
+
}
|
|
220
|
+
if (typeof base !== 'string' || base.length === 0 || base.endsWith('/')) {
|
|
221
|
+
throw new Error(`formatSessionLink:base 必须是非空字符串且不以 '/' 结尾(实测 ${String(base)})`);
|
|
222
|
+
}
|
|
223
|
+
if (base.includes('?') || base.includes('#')) {
|
|
224
|
+
throw new Error('formatSessionLink:base 不得含 query/hash(产出即 parseSessionLink 拒绝的形态,锋利边角)');
|
|
225
|
+
}
|
|
226
|
+
return `${base}/${token}`;
|
|
227
|
+
}
|
|
228
|
+
/** 从 URL 严格反解 token:路径最后一段,且段本身须呈 `body.sig` 形态(恰
|
|
229
|
+
* 一个非空 body、非空 sig;段内多点由 verify 终裁——本函数只保证不把一个
|
|
230
|
+
* 裸 base(`gotry://session`)的最后一段误当 token)。**拒绝 query/hash**——
|
|
231
|
+
* 拼接歧义(如 `?x=../../etc`)一律 fail-closed;形态不符返回 null(消费端
|
|
232
|
+
* 不打开)。 */
|
|
233
|
+
export function parseSessionLink(url) {
|
|
234
|
+
if (typeof url !== 'string' || url.length === 0)
|
|
235
|
+
return null;
|
|
236
|
+
const q = url.indexOf('?');
|
|
237
|
+
const h = url.indexOf('#');
|
|
238
|
+
if (q !== -1 || h !== -1)
|
|
239
|
+
return null;
|
|
240
|
+
const segments = url.split('/').filter(s => s.length > 0);
|
|
241
|
+
const token = segments[segments.length - 1];
|
|
242
|
+
if (segments.length < 2 || token === undefined || token.length === 0)
|
|
243
|
+
return null;
|
|
244
|
+
const dot = token.indexOf('.');
|
|
245
|
+
if (dot <= 0 || dot === token.length - 1)
|
|
246
|
+
return null;
|
|
247
|
+
return token;
|
|
248
|
+
}
|