@danceiny/gotry 0.0.1-rc.8 → 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 -217
- 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 -49
- 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 +968 -0
- 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 +126 -0
- package/dist/capabilities/session/adapters/ctrip-flight.js +141 -0
- 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 +54 -0
- 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 +50 -0
- 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 +107 -0
- package/dist/capabilities/session/static-flight-golden.js +150 -0
- package/dist/capabilities/session/transport.js +109 -0
- 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 +686 -0
- 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 +72 -0
- 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 +68 -10
- 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 +67 -0
- 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 +480 -0
- package/dist/scripts/ledger-workflow-crash.js +32 -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 +49 -0
- package/dist/scripts/memory-lifecycle-tests.js +657 -0
- package/dist/scripts/memory-lifecycle.js +196 -0
- package/dist/scripts/memory-metrics.js +29 -31
- 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 +43 -42
- 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 +31 -0
- package/dist/scripts/session-attach-poc.js +62 -0
- 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 +60 -0
- package/dist/scripts/session-link-tests.js +478 -0
- package/dist/scripts/session-login.js +29 -0
- package/dist/scripts/session-tests.js +862 -0
- 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 +703 -219
- 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 +261 -0
- package/dist/scripts/state-cli.js +585 -0
- 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 +59 -0
- 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 +86 -0
- 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 +2305 -731
- 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 +358 -162
- package/dist/src/memory-capture.js +180 -32
- package/dist/src/memory-decay.js +47 -0
- 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 +767 -0
- 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 +59 -0
- 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 +538 -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 +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 +296 -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 +2107 -224
- 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 +278 -40
- 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 +360 -89
- package/ts/src/wish-pool.ts +79 -0
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 桥作业账本测试(§69;批次 A「桥作业账本化」,2026-10-01)。
|
|
3
|
+
*
|
|
4
|
+
* 全离线确定性:隔离 stateRoot(mkdtemp)+ 临时端口回环桥 + 进程内假扩展客户端
|
|
5
|
+
* (直 fetch 桥端点),零网络出站、零真实供应商、零真实凭据、不写共享状态。
|
|
6
|
+
*
|
|
7
|
+
* 覆盖(规格批次 A §7):
|
|
8
|
+
* ① 建表:三表落位 + 独立 db 文件(<stateRoot>/gotry-state/bridge.db,与
|
|
9
|
+
* state-ledger 的 gotry-state.db 物理分库)+ status CHECK 约束;
|
|
10
|
+
* ② write-before-submit:submit() 同步段内 queued 行已在账本(先写后派发),
|
|
11
|
+
* 账本写失败 = 提交失败不入队(fail-closed);
|
|
12
|
+
* ③ 领取/回包/超时结算:claim 落 claimed+attempts;回包落 resolved+result;
|
|
13
|
+
* claimed 超时→unresolved+bridge_pacing 记冷却;从未领取超时→void;
|
|
14
|
+
* ④ 未知回包:404 {ok:false,error:'unknown-job'} + 孤儿内容落
|
|
15
|
+
* bridge_jobs(status='unresolved',result_json 保留);resolved 迟到回包不覆盖;
|
|
16
|
+
* ⑤ 恢复重排:queued 未过期→重排可领取 / queued 已过期→void /
|
|
17
|
+
* claimed 已超时→unresolved+pacing / claimed 未超时→复活(迟到回包直达 resolved);
|
|
18
|
+
* ⑥ bridge_clients upsert(轮询即刷新 origin/capabilities/last_seen)+
|
|
19
|
+
* cookie-names 命中记 login_sites(名字级,零凭据)+ 非导航作业不记节律;
|
|
20
|
+
* ⑦ 挂载路径接线:stateRoot → ledger-backed 队列 + 启动即 recoverOnBoot +
|
|
21
|
+
* 检索前账本节律读判定(冷却内 429,冷却过期放行)。
|
|
22
|
+
*/
|
|
23
|
+
import assert from 'node:assert/strict';
|
|
24
|
+
import { EventEmitter } from 'node:events';
|
|
25
|
+
import { createServer } from 'node:http';
|
|
26
|
+
import { existsSync, mkdtempSync, rmSync } from 'node:fs';
|
|
27
|
+
import { tmpdir } from 'node:os';
|
|
28
|
+
import { join } from 'node:path';
|
|
29
|
+
import { randomUUID } from 'node:crypto';
|
|
30
|
+
import Database from 'better-sqlite3';
|
|
31
|
+
import { EXTENSION_ORIGIN, createBridgeJobQueue, } from '../capabilities/session/extension-bridge.js';
|
|
32
|
+
import { BRIDGE_SITE_COOLDOWN_MS, BridgeLedgerStore, bridgeLedgerDbPath, openBridgeLedgerStore, } from '../capabilities/session/bridge-ledger.js';
|
|
33
|
+
import { startSessionSearchModule } from '../src/backend/modules/session-search.js';
|
|
34
|
+
let passed = 0;
|
|
35
|
+
async function check(label, assertion) {
|
|
36
|
+
await assertion();
|
|
37
|
+
passed += 1;
|
|
38
|
+
console.log(` ok - ${label}`);
|
|
39
|
+
}
|
|
40
|
+
/** 回环桥测试面:临时端口 + close 时清连接(防 parked 长轮询钉住测试进程) */
|
|
41
|
+
async function serveQueue(queue) {
|
|
42
|
+
const server = createServer((req, res) => queue.handleRequest(req, res));
|
|
43
|
+
server.on('connection', (socket) => socket.unref());
|
|
44
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
45
|
+
server.unref();
|
|
46
|
+
const port = server.address().port;
|
|
47
|
+
return {
|
|
48
|
+
port,
|
|
49
|
+
close: () => new Promise((resolve) => {
|
|
50
|
+
server.closeAllConnections?.();
|
|
51
|
+
server.close(() => resolve());
|
|
52
|
+
}),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** 假扩展轮询:POST /jobs(可信 Origin;带 clientId/capabilities 时同时驱动 bridge_clients upsert) */
|
|
56
|
+
async function pollJobs(port, opts = {}) {
|
|
57
|
+
const r = await fetch(`http://127.0.0.1:${port}/jobs`, {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
headers: { 'content-type': 'application/json', origin: opts.origin ?? EXTENSION_ORIGIN },
|
|
60
|
+
body: JSON.stringify({
|
|
61
|
+
extensionVersion: 'bridge-ledger-tests',
|
|
62
|
+
...(opts.capabilities ? { capabilities: opts.capabilities } : {}),
|
|
63
|
+
...(opts.clientId ? { clientId: opts.clientId } : {}),
|
|
64
|
+
}),
|
|
65
|
+
signal: AbortSignal.timeout(3_000),
|
|
66
|
+
});
|
|
67
|
+
assert.equal(r.status, 200, `/jobs 轮询不得非 200(Origin=${opts.origin ?? EXTENSION_ORIGIN})`);
|
|
68
|
+
return (await r.json());
|
|
69
|
+
}
|
|
70
|
+
async function postResult(port, jobId, result, opts = {}) {
|
|
71
|
+
return fetch(`http://127.0.0.1:${port}/results/${encodeURIComponent(jobId)}`, {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers: {
|
|
74
|
+
'content-type': 'application/json',
|
|
75
|
+
origin: opts.origin ?? EXTENSION_ORIGIN,
|
|
76
|
+
...(opts.clientId ? { 'x-gotry-client-id': opts.clientId } : {}),
|
|
77
|
+
},
|
|
78
|
+
body: JSON.stringify(result),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/** 等 /status 报 parked=N(先 park 再 submit,派发即时无竞态) */
|
|
82
|
+
async function waitForParked(port, expected, timeoutMs = 2_000) {
|
|
83
|
+
const deadline = Date.now() + timeoutMs;
|
|
84
|
+
while (Date.now() < deadline) {
|
|
85
|
+
const r = await fetch(`http://127.0.0.1:${port}/status`);
|
|
86
|
+
if ((await r.json()).parked === expected)
|
|
87
|
+
return;
|
|
88
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
89
|
+
}
|
|
90
|
+
throw new Error(`/status 未在 ${timeoutMs}ms 内报告 parked=${expected}`);
|
|
91
|
+
}
|
|
92
|
+
async function main() {
|
|
93
|
+
console.log('§69 桥作业账本(bridge ledger,全离线)');
|
|
94
|
+
/* ---------- ① 建表与分库独立性 ---------- */
|
|
95
|
+
const rootA = mkdtempSync(join(tmpdir(), 'gotry-bridge-ledger-a-'));
|
|
96
|
+
const storeA = openBridgeLedgerStore(rootA);
|
|
97
|
+
await check('建表:bridge.db 落位 <stateRoot>/gotry-state/,三表齐备,与 state-ledger 的 gotry-state.db 物理分库', () => {
|
|
98
|
+
assert.ok(existsSync(bridgeLedgerDbPath(rootA)), `db 文件应存在:${bridgeLedgerDbPath(rootA)}`);
|
|
99
|
+
assert.equal(existsSync(join(rootA, 'gotry-state', 'gotry-state.db')), false, '不得触碰 state-ledger 的 gotry-state.db');
|
|
100
|
+
const raw = new Database(bridgeLedgerDbPath(rootA), { readonly: true });
|
|
101
|
+
try {
|
|
102
|
+
const tables = raw.prepare(`SELECT name FROM sqlite_master WHERE type='table'`).all().map((t) => t.name);
|
|
103
|
+
for (const t of ['bridge_jobs', 'bridge_clients', 'bridge_pacing'])
|
|
104
|
+
assert.ok(tables.includes(t), `缺表 ${t}`);
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
raw.close();
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
await check('建表:bridge_jobs.status CHECK 约束(闭集 queued|claimed|resolved|unresolved|void)', () => {
|
|
111
|
+
const raw = new Database(bridgeLedgerDbPath(rootA));
|
|
112
|
+
try {
|
|
113
|
+
assert.throws(() => raw.prepare(`INSERT INTO bridge_jobs (id, kind, site, payload_json, status, created_at, updated_at) VALUES ('x', 'search', 's', '{}', 'bogus', 0, 0)`).run(), /CHECK/, '非法 status 必须被物理约束拒绝');
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
raw.close();
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
/* ---------- ② write-before-submit 顺序 ---------- */
|
|
120
|
+
const queueB = createBridgeJobQueue({ store: storeA });
|
|
121
|
+
await check('write-before-submit:submit() 调用的同步段内 queued 行已落账(先写后派发)', async () => {
|
|
122
|
+
const server = await serveQueue(queueB);
|
|
123
|
+
try {
|
|
124
|
+
// 不 await:同一同步段查账,证明 INSERT 先于任何派发/入队发生
|
|
125
|
+
const pending = queueB.submit({ kind: 'search', site: 'dida-portal', url: 'https://portal.dida.com/find' }, { timeoutMs: 400, extensionWaitMs: 0 });
|
|
126
|
+
const raw = new Database(bridgeLedgerDbPath(rootA), { readonly: true });
|
|
127
|
+
let queued = [];
|
|
128
|
+
try {
|
|
129
|
+
queued = raw.prepare(`SELECT id, payload_json FROM bridge_jobs WHERE status='queued'`).all();
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
raw.close();
|
|
133
|
+
}
|
|
134
|
+
assert.equal(queued.length, 1, '入队前必须已有恰好一行 queued');
|
|
135
|
+
assert.ok(queued[0].payload_json.includes('portal.dida.com'), 'payload_json 保留作业原文供恢复重放');
|
|
136
|
+
const outcome = await pending;
|
|
137
|
+
assert.equal(outcome.ok, false);
|
|
138
|
+
assert.equal(outcome.reason, 'extension-not-connected', '宽限 0ms 且扩展未连接 → no-spend 失败');
|
|
139
|
+
assert.equal(storeA.getJob(queued[0].id)?.status, 'void', '从未被领取的失败终态 = void(零站点花费)');
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
await server.close();
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
await check('write-before-submit:账本写失败 = 提交失败不入队(fail-closed)', async () => {
|
|
146
|
+
const broken = new BridgeLedgerStore(join(rootA, 'gotry-state', 'broken.db'));
|
|
147
|
+
broken.close();
|
|
148
|
+
const q = createBridgeJobQueue({ store: broken });
|
|
149
|
+
try {
|
|
150
|
+
const outcome = await q.submit({ kind: 'search', site: 'dida-portal', url: 'https://portal.dida.com/find' }, { timeoutMs: 300, extensionWaitMs: 0 });
|
|
151
|
+
assert.equal(outcome.ok, false);
|
|
152
|
+
assert.equal(outcome.reason, 'bridge-unavailable');
|
|
153
|
+
assert.match(outcome.summary, /账本写入失败/, '失败摘要必须指向账本写入');
|
|
154
|
+
assert.equal(q.stats().queued, 0, '未入队');
|
|
155
|
+
assert.equal(q.stats().inFlight, 0, '未派发');
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
await q.close();
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
await queueB.close();
|
|
162
|
+
storeA.close();
|
|
163
|
+
rmSync(rootA, { recursive: true, force: true });
|
|
164
|
+
/* ---------- ③ 领取/回包/超时结算 ---------- */
|
|
165
|
+
const rootC = mkdtempSync(join(tmpdir(), 'gotry-bridge-ledger-c-'));
|
|
166
|
+
const storeC = openBridgeLedgerStore(rootC);
|
|
167
|
+
const queueC = createBridgeJobQueue({ store: storeC });
|
|
168
|
+
const CLAIM_CLIENT = randomUUID();
|
|
169
|
+
await check('领取与善果回包:claim 落 claimed+领取者+attempts+导航记节律;回包落 resolved+result;迟到回包不翻案', async () => {
|
|
170
|
+
const server = await serveQueue(queueC);
|
|
171
|
+
try {
|
|
172
|
+
const pollPromise = pollJobs(server.port, { clientId: CLAIM_CLIENT, capabilities: ['dida-portal'] });
|
|
173
|
+
await waitForParked(server.port, 1);
|
|
174
|
+
const pending = queueC.submit({ kind: 'search', site: 'dida-portal', url: 'https://portal.dida.com/find' }, { timeoutMs: 3_000 });
|
|
175
|
+
const polled = await pollPromise;
|
|
176
|
+
assert.ok(polled.job, 'parked 取活者应即时领走新作业');
|
|
177
|
+
const row = storeC.getJob(polled.job.jobId);
|
|
178
|
+
assert.equal(row?.status, 'claimed');
|
|
179
|
+
assert.equal(row?.claimed_origin, EXTENSION_ORIGIN);
|
|
180
|
+
assert.equal(row?.claimed_by_client, CLAIM_CLIENT);
|
|
181
|
+
assert.equal(row?.attempts, 1);
|
|
182
|
+
assert.ok(row?.claimed_at !== null, 'claimed_at 落账');
|
|
183
|
+
const pacing = storeC.pacingOf('dida-portal');
|
|
184
|
+
assert.ok(pacing && pacing.cooldownUntil > Date.now(), '导航类作业领取即记节律冷却');
|
|
185
|
+
// 正常善果回包 → resolved + result 保留
|
|
186
|
+
const r = await postResult(server.port, polled.job.jobId, { ok: true, kind: 'search', body: '{"rates":[]}', title: 'ok' }, { clientId: CLAIM_CLIENT });
|
|
187
|
+
assert.equal(r.status, 200);
|
|
188
|
+
const outcome = await pending;
|
|
189
|
+
assert.equal(outcome.ok, true);
|
|
190
|
+
const settled = storeC.getJob(polled.job.jobId);
|
|
191
|
+
assert.equal(settled?.status, 'resolved');
|
|
192
|
+
assert.ok(settled?.result_json?.includes('rates'), 'result_json 保留回包内容');
|
|
193
|
+
// resolved 之后迟到重复回包:未知化 404,且不覆盖已决 resolved
|
|
194
|
+
const late = await postResult(server.port, polled.job.jobId, { ok: true, kind: 'search', body: 'LATE' }, { clientId: CLAIM_CLIENT });
|
|
195
|
+
assert.equal(late.status, 404);
|
|
196
|
+
assert.equal(storeC.getJob(polled.job.jobId)?.status, 'resolved', '迟到回包不得翻案已决结果');
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
await server.close();
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
await check('超时结算:曾被领取 → unresolved + bridge_pacing 记冷却(该站点可能已被打)', async () => {
|
|
203
|
+
const server = await serveQueue(queueC);
|
|
204
|
+
try {
|
|
205
|
+
const pollPromise = pollJobs(server.port);
|
|
206
|
+
await waitForParked(server.port, 1);
|
|
207
|
+
const pending = queueC.submit({ kind: 'search', site: 'train-12306', url: 'https://kyfw.12306.cn/otn/leftTicket/query' }, { timeoutMs: 400 });
|
|
208
|
+
const polled = await pollPromise;
|
|
209
|
+
assert.ok(polled.job, '领取成功');
|
|
210
|
+
const outcome = await pending;
|
|
211
|
+
assert.equal(outcome.ok, false);
|
|
212
|
+
assert.equal(outcome.reason, 'timeout');
|
|
213
|
+
const row = storeC.getJob(polled.job.jobId);
|
|
214
|
+
assert.equal(row?.status, 'unresolved', '曾被领取的超时终态 = unresolved');
|
|
215
|
+
assert.ok(row?.result_json?.includes('timeout'), '超时结算留 timeout 备注');
|
|
216
|
+
const pacing = storeC.pacingOf('train-12306');
|
|
217
|
+
assert.ok(pacing && pacing.cooldownUntil >= Date.now() + BRIDGE_SITE_COOLDOWN_MS - 2_000, '超时结算把站点冷却推满');
|
|
218
|
+
}
|
|
219
|
+
finally {
|
|
220
|
+
await server.close();
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
await check('超时结算:从未被领取 → void,超时结算不追加冷却(提交时刻的节律按内存闸同语义保留)', async () => {
|
|
224
|
+
const server = await serveQueue(queueC);
|
|
225
|
+
try {
|
|
226
|
+
const pending = queueC.submit({ kind: 'search', site: 'ctrip-hotel', url: 'https://hotels.ctrip.com/hotels/list' }, { timeoutMs: 300, extensionWaitMs: 30_000 });
|
|
227
|
+
const pacingAtSubmit = storeC.pacingOf('ctrip-hotel');
|
|
228
|
+
assert.ok(pacingAtSubmit && pacingAtSubmit.cooldownUntil > Date.now(), '导航类作业提交即记节律(与 sessionDidaSearch 内存闸同语义:提交即承诺一次导航)');
|
|
229
|
+
const outcome = await pending;
|
|
230
|
+
assert.equal(outcome.ok, false);
|
|
231
|
+
assert.equal(outcome.reason, 'timeout', '宽限放足,确保走到超时而非未连接');
|
|
232
|
+
const raw = new Database(bridgeLedgerDbPath(rootC), { readonly: true });
|
|
233
|
+
let voided = [];
|
|
234
|
+
try {
|
|
235
|
+
voided = raw.prepare(`SELECT id FROM bridge_jobs WHERE status='void' AND site='ctrip-hotel'`).all();
|
|
236
|
+
}
|
|
237
|
+
finally {
|
|
238
|
+
raw.close();
|
|
239
|
+
}
|
|
240
|
+
assert.equal(voided.length, 1, '未被领取的超时作业 = void');
|
|
241
|
+
const pacingAfter = storeC.pacingOf('ctrip-hotel');
|
|
242
|
+
assert.equal(pacingAfter?.cooldownUntil, pacingAtSubmit?.cooldownUntil, '从未被领取的超时结算不追加冷却(站点未被打)');
|
|
243
|
+
}
|
|
244
|
+
finally {
|
|
245
|
+
await server.close();
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
/* ---------- ④ 未知回包 404 + 孤儿落库 ---------- */
|
|
249
|
+
await check('未知回包:404 {ok:false,error:"unknown-job"} + 孤儿内容落 bridge_jobs(unresolved+result 保留)', async () => {
|
|
250
|
+
const server = await serveQueue(queueC);
|
|
251
|
+
try {
|
|
252
|
+
const r = await postResult(server.port, 'orphan-job-404', { ok: true, kind: 'search', body: 'ORPHAN-BODY-EVIDENCE', title: 'orphan' });
|
|
253
|
+
assert.equal(r.status, 404, '未知 jobId 不再 200 静默丢弃');
|
|
254
|
+
const body = (await r.json());
|
|
255
|
+
assert.equal(body.ok, false);
|
|
256
|
+
assert.equal(body.error, 'unknown-job');
|
|
257
|
+
const row = storeC.getJob('orphan-job-404');
|
|
258
|
+
assert.equal(row?.status, 'unresolved', '孤儿回包以 unresolved 落库供对账');
|
|
259
|
+
assert.equal(row?.kind, 'search', 'kind 取回包自报');
|
|
260
|
+
assert.ok(row?.result_json?.includes('ORPHAN-BODY-EVIDENCE'), '回包内容 result_json 原样保留');
|
|
261
|
+
}
|
|
262
|
+
finally {
|
|
263
|
+
await server.close();
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
/* ---------- ⑤ 恢复重排(recoverOnBoot) ---------- */
|
|
267
|
+
await check('恢复重排:queued 未过期重排可领取 / queued 已过期 void / claimed 已超时 unresolved+pacing / claimed 未超时复活', async () => {
|
|
268
|
+
const rootE = mkdtempSync(join(tmpdir(), 'gotry-bridge-ledger-e-'));
|
|
269
|
+
const storeE = openBridgeLedgerStore(rootE);
|
|
270
|
+
const t0 = Date.now();
|
|
271
|
+
const LIVE_CLIENT = randomUUID();
|
|
272
|
+
let q;
|
|
273
|
+
let server;
|
|
274
|
+
try {
|
|
275
|
+
// 直接落账模拟上一进程崩溃后的持久状态(不经过队列 close——close 会把未领取作业落 void)
|
|
276
|
+
storeE.insertQueuedJob({ jobId: 'rec-queued-fresh', kind: 'search', site: 'dida-portal', url: 'https://portal.dida.com/find' }, t0 + 60_000);
|
|
277
|
+
storeE.insertQueuedJob({ jobId: 'rec-queued-stale', kind: 'search', site: 'dida-portal', url: 'https://portal.dida.com/find' }, t0 - 1_000);
|
|
278
|
+
storeE.insertQueuedJob({ jobId: 'rec-claimed-live', kind: 'search', site: 'ctrip-flight', url: 'https://flights.ctrip.com/online/list' }, t0 + 60_000);
|
|
279
|
+
storeE.markClaimed('rec-claimed-live', { origin: EXTENSION_ORIGIN, clientId: LIVE_CLIENT });
|
|
280
|
+
storeE.insertQueuedJob({ jobId: 'rec-claimed-dead', kind: 'search', site: 'ctrip-hotel', url: 'https://hotels.ctrip.com/hotels/list' }, t0 - 1_000);
|
|
281
|
+
storeE.markClaimed('rec-claimed-dead', { origin: EXTENSION_ORIGIN });
|
|
282
|
+
q = createBridgeJobQueue({ store: storeE });
|
|
283
|
+
server = await serveQueue(q);
|
|
284
|
+
// 恢复先于任何轮询执行(等价挂载路径:模块启动即 recoverOnBoot)
|
|
285
|
+
const summary = q.recoverOnBoot();
|
|
286
|
+
assert.equal(summary.requeued, 1);
|
|
287
|
+
assert.equal(summary.voidedStale, 1);
|
|
288
|
+
assert.equal(summary.rehydratedInFlight, 1);
|
|
289
|
+
assert.equal(summary.settledUnresolved, 1);
|
|
290
|
+
assert.equal(storeE.getJob('rec-queued-stale')?.status, 'void', '未被领取即过期 → void,不再派发(零花费)');
|
|
291
|
+
assert.equal(storeE.getJob('rec-claimed-dead')?.status, 'unresolved', '领取后超时 → unresolved');
|
|
292
|
+
assert.ok(storeE.pacingOf('ctrip-hotel') && storeE.pacingOf('ctrip-hotel').cooldownUntil > Date.now(), '超时结算记节律冷却');
|
|
293
|
+
// 重排作业可被新进程的扩展领取并善果落账
|
|
294
|
+
const polled = await pollJobs(server.port, { clientId: LIVE_CLIENT, capabilities: ['dida-portal'] });
|
|
295
|
+
assert.equal(polled.job?.jobId, 'rec-queued-fresh', '恢复的 queued 作业重排进队列');
|
|
296
|
+
await postResult(server.port, 'rec-queued-fresh', { ok: true, kind: 'search', body: 'RECOVERED' }, { clientId: LIVE_CLIENT });
|
|
297
|
+
assert.equal(storeE.getJob('rec-queued-fresh')?.status, 'resolved', '重排作业回包 → resolved(迟到善果不丢)');
|
|
298
|
+
// 复活的 claimed 作业:扩展迟到回包直达 resolved(重启丢在飞作业缺陷的修复面)
|
|
299
|
+
const late = await postResult(server.port, 'rec-claimed-live', { ok: true, kind: 'search', body: 'LATE-AFTER-RESTART' }, { clientId: LIVE_CLIENT });
|
|
300
|
+
assert.equal(late.status, 200, '复活在飞作业对领取客户端放行');
|
|
301
|
+
assert.equal(storeE.getJob('rec-claimed-live')?.status, 'resolved', '重启前领取的作业,重启后回包仍闭环');
|
|
302
|
+
}
|
|
303
|
+
finally {
|
|
304
|
+
if (server)
|
|
305
|
+
await server.close();
|
|
306
|
+
if (q)
|
|
307
|
+
await q.close();
|
|
308
|
+
storeE.close();
|
|
309
|
+
rmSync(rootE, { recursive: true, force: true });
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
/* ---------- ⑥ bridge_clients upsert 与登录站点(名字级) ---------- */
|
|
313
|
+
await check('bridge_clients upsert:轮询即刷新 origin/capabilities/last_seen;cookie-names 命中记 login_sites;非导航作业不记节律', async () => {
|
|
314
|
+
const server = await serveQueue(queueC);
|
|
315
|
+
try {
|
|
316
|
+
const CLIENT = randomUUID();
|
|
317
|
+
const pacingBefore = storeC.pacingOf('dida-portal');
|
|
318
|
+
const first = queueC.submit({ kind: 'cookie-names', site: 'dida-portal', timeoutMs: 3_000 }, { timeoutMs: 3_000 });
|
|
319
|
+
const polled = await pollJobs(server.port, { clientId: CLIENT, capabilities: ['dida-portal'] });
|
|
320
|
+
assert.ok(polled.job, 'cookie-names 作业应被领取');
|
|
321
|
+
await postResult(server.port, polled.job.jobId, { ok: true, kind: 'cookie-names', names: ['dida_session_sig'] }, { clientId: CLIENT });
|
|
322
|
+
assert.equal((await first).ok, true);
|
|
323
|
+
const row1 = storeC.getClient(CLIENT);
|
|
324
|
+
assert.ok(row1, '轮询即注册客户端');
|
|
325
|
+
assert.equal(row1?.origin, EXTENSION_ORIGIN);
|
|
326
|
+
assert.deepEqual(JSON.parse(row1?.capabilities_json ?? '[]'), ['dida-portal']);
|
|
327
|
+
assert.deepEqual(JSON.parse(row1?.login_sites_json ?? '[]'), ['dida-portal'], 'cookie-names 命中票据名 → login_sites(名字级)');
|
|
328
|
+
const seen1 = row1?.last_seen_at ?? 0;
|
|
329
|
+
// upsert 二次:能力清单变化 + last_seen 刷新,login_sites 不丢
|
|
330
|
+
const second = queueC.submit({ kind: 'cookie-names', site: 'dida-portal', timeoutMs: 3_000 }, { timeoutMs: 3_000 });
|
|
331
|
+
const polled2 = await pollJobs(server.port, { clientId: CLIENT, capabilities: ['dida-portal', 'train-12306'] });
|
|
332
|
+
assert.ok(polled2.job, '第二次轮询仍可领取');
|
|
333
|
+
await postResult(server.port, polled2.job.jobId, { ok: true, kind: 'cookie-names', names: [] }, { clientId: CLIENT });
|
|
334
|
+
assert.equal((await second).ok, true);
|
|
335
|
+
const row2 = storeC.getClient(CLIENT);
|
|
336
|
+
assert.deepEqual(JSON.parse(row2?.capabilities_json ?? '[]'), ['dida-portal', 'train-12306'], 'upsert 覆盖能力清单');
|
|
337
|
+
assert.ok((row2?.last_seen_at ?? 0) >= seen1, 'last_seen_at 单调刷新');
|
|
338
|
+
assert.deepEqual(JSON.parse(row2?.login_sites_json ?? '[]'), ['dida-portal'], 'upsert 不回退 login_sites');
|
|
339
|
+
const pacingAfter = storeC.pacingOf('dida-portal');
|
|
340
|
+
assert.deepEqual(pacingAfter, pacingBefore, 'cookie-names 非导航作业,不推动节律');
|
|
341
|
+
}
|
|
342
|
+
finally {
|
|
343
|
+
await server.close();
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
await queueC.close();
|
|
347
|
+
storeC.close();
|
|
348
|
+
rmSync(rootC, { recursive: true, force: true });
|
|
349
|
+
/* ---------- ⑦ 挂载路径模块接线(stateRoot → ledger-backed + recoverOnBoot + 节律读判定) ---------- */
|
|
350
|
+
await check('挂载路径:stateRoot → ledger-backed 队列 + 启动即 recoverOnBoot + 账本冷却内 429/冷却过期放行', async () => {
|
|
351
|
+
const rootG = mkdtempSync(join(tmpdir(), 'gotry-bridge-ledger-g-'));
|
|
352
|
+
try {
|
|
353
|
+
// 预置一条上一进程遗留的已超时 claimed 作业:模块启动即应结算 unresolved
|
|
354
|
+
const seed = openBridgeLedgerStore(rootG);
|
|
355
|
+
seed.insertQueuedJob({ jobId: 'mount-recover-dead', kind: 'search', site: 'dida-portal', url: 'https://portal.dida.com/find' }, Date.now() - 1_000);
|
|
356
|
+
seed.markClaimed('mount-recover-dead', { origin: EXTENSION_ORIGIN });
|
|
357
|
+
seed.close();
|
|
358
|
+
const mod = startSessionSearchModule({
|
|
359
|
+
apiKey: () => 'ledger-test-key',
|
|
360
|
+
stateRoot: rootG,
|
|
361
|
+
search: async () => ({
|
|
362
|
+
ok: true, via: 'session-dida-portal', evidence: '[会话:dida-portal@ledger-test]', latencyMs: 1, verdict: 'hit', rates: [],
|
|
363
|
+
}),
|
|
364
|
+
});
|
|
365
|
+
try {
|
|
366
|
+
assert.ok(existsSync(bridgeLedgerDbPath(rootG)), '挂载路径模块创建即落 bridge.db');
|
|
367
|
+
const probe = openBridgeLedgerStore(rootG);
|
|
368
|
+
try {
|
|
369
|
+
// recoverOnBoot 已在模块启动路径跑过:遗留 claimed 超时作业被结算
|
|
370
|
+
assert.equal(probe.getJob('mount-recover-dead')?.status, 'unresolved', '启动即恢复结算遗留超时作业');
|
|
371
|
+
// 账本节律读判定:冷却内 429(带 retry-after)
|
|
372
|
+
probe.applyCooldown('dida-portal');
|
|
373
|
+
const hot = await driveModuleSearch(mod, 'Bearer ledger-test-key');
|
|
374
|
+
assert.equal(hot.statusCode, 429, '账本冷却内检索被节律闸拒绝');
|
|
375
|
+
assert.ok(String(hot.headers['retry-after'] ?? '').length > 0, '429 携带 retry-after');
|
|
376
|
+
assert.ok(hot.body.includes('cooldown'), '错误面说明冷却来源');
|
|
377
|
+
// 冷却过期 → 放行(fake search 命中 200)
|
|
378
|
+
const raw = new Database(bridgeLedgerDbPath(rootG));
|
|
379
|
+
try {
|
|
380
|
+
raw.prepare(`UPDATE bridge_pacing SET cooldown_until=? WHERE site='dida-portal'`).run(Date.now() - 1_000);
|
|
381
|
+
}
|
|
382
|
+
finally {
|
|
383
|
+
raw.close();
|
|
384
|
+
}
|
|
385
|
+
const cool = await driveModuleSearch(mod, 'Bearer ledger-test-key');
|
|
386
|
+
assert.equal(cool.statusCode, 200, '冷却过期后放行');
|
|
387
|
+
assert.ok(cool.body.includes('"verdict":"hit"'), '注入的 fake 检索结果透传');
|
|
388
|
+
}
|
|
389
|
+
finally {
|
|
390
|
+
probe.close();
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
finally {
|
|
394
|
+
await mod.close?.();
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
finally {
|
|
398
|
+
rmSync(rootG, { recursive: true, force: true });
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
console.log(`\n§69 bridge ledger: ${passed} 段全绿`);
|
|
402
|
+
}
|
|
403
|
+
/** 模块路由直驱:假请求/响应(handle 同步注册 data/end 监听后再投喂 body) */
|
|
404
|
+
async function driveModuleSearch(mod, auth) {
|
|
405
|
+
const req = new EventEmitter();
|
|
406
|
+
Object.assign(req, { method: 'POST', url: '/v1/session/search', headers: { authorization: auth, 'content-type': 'application/json' } });
|
|
407
|
+
const headers = {};
|
|
408
|
+
let settle;
|
|
409
|
+
const done = new Promise((resolve) => { settle = resolve; });
|
|
410
|
+
let statusCode = 200;
|
|
411
|
+
const res = {
|
|
412
|
+
get statusCode() { return statusCode; },
|
|
413
|
+
set statusCode(v) { statusCode = v; },
|
|
414
|
+
setHeader: (k, v) => { headers[k.toLowerCase()] = v; },
|
|
415
|
+
end: (chunk) => { settle({ statusCode, body: typeof chunk === 'string' ? chunk : '', headers }); },
|
|
416
|
+
};
|
|
417
|
+
const route = mod.routes.find((r) => r.method === 'POST' && r.path === '/v1/session/search');
|
|
418
|
+
if (!route)
|
|
419
|
+
throw new Error('session-search 模块应注册 POST /v1/session/search');
|
|
420
|
+
route.handle(req, res);
|
|
421
|
+
req.emit('data', Buffer.from('{"supplier":"dida-portal"}'));
|
|
422
|
+
req.emit('end');
|
|
423
|
+
return done;
|
|
424
|
+
}
|
|
425
|
+
main().catch((e) => {
|
|
426
|
+
console.error('FAIL:', e);
|
|
427
|
+
process.exit(1);
|
|
428
|
+
});
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CHANGELOG.md 自动生成器(Keep a Changelog 1.1.0 + Conventional Commits 解析)
|
|
3
|
+
*
|
|
4
|
+
* 设计(2026-08-30 owner 拍板:AGENTS.md 「发布闸」+ 「doc-only 同步纪律」延伸):
|
|
5
|
+
* - 输入:`git log <prev-tag>..HEAD --no-merges`,从 package.json 锁定当前版本号;
|
|
6
|
+
* - 解析 Conventional Commits:feat/fix/perf/refactor/docs/test/build/ci/chore/revert
|
|
7
|
+
* + 可选 scope(`feat(scope):`)——scope 用于同一节内归类;
|
|
8
|
+
* - 输出:Keep a Changelog 1.1.0 节结构(Added/Changed/Fixed/Removed/Security),
|
|
9
|
+
* 在 `CHANGELOG.md` 顶部插入新版本段;**已有 CHANGELOG.md 的历史段原样保留**(不重写)。
|
|
10
|
+
* - PR 引用 `#NN` 写入条目尾(支持自动跨 commit 链接)。
|
|
11
|
+
*
|
|
12
|
+
* 运行(在 ts/ 下):
|
|
13
|
+
* npx tsx scripts/build-changelog.ts # 从上一 tag 到 HEAD 生成段,stdout 打印 diff 提示
|
|
14
|
+
* npx tsx scripts/build-changelog.ts --write # 写入 CHANGELOG.md(覆盖顶部新版本段)
|
|
15
|
+
* npx tsx scripts/build-changelog.ts --since v0.0.1-rc.15
|
|
16
|
+
* npx tsx scripts/build-changelog.ts --version 0.0.1-rc.16 --date 2026-08-30
|
|
17
|
+
*
|
|
18
|
+
* 不接 Conventional Commits 严格门:commit subject 不一定全符合(历史 commit 多为长中文),
|
|
19
|
+
* parser 容错——只把带 type: 前缀的归类,其余落入「Other(未分类)」,保持版本段完整可读。
|
|
20
|
+
*/
|
|
21
|
+
import { execFileSync } from 'node:child_process';
|
|
22
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
23
|
+
import { join } from 'node:path';
|
|
24
|
+
const TYPE_TO_KEEP_A_CHANGELOG = {
|
|
25
|
+
feat: 'Added',
|
|
26
|
+
fix: 'Fixed',
|
|
27
|
+
perf: 'Changed',
|
|
28
|
+
refactor: 'Changed',
|
|
29
|
+
docs: 'Documentation',
|
|
30
|
+
test: 'Tests',
|
|
31
|
+
build: 'Build',
|
|
32
|
+
ci: 'CI',
|
|
33
|
+
chore: 'Chore',
|
|
34
|
+
revert: 'Reverted',
|
|
35
|
+
};
|
|
36
|
+
const DISPLAY_ORDER = [
|
|
37
|
+
'feat', 'fix', 'perf', 'refactor', 'revert', 'docs', 'test', 'build', 'ci', 'chore', 'other',
|
|
38
|
+
];
|
|
39
|
+
const CONVENTIONAL_RE = /^(feat|fix|perf|refactor|docs|test|build|ci|chore|revert)(?:\(([^)]+)\))?(!)?:\s*(.+)$/;
|
|
40
|
+
export function parseCommit(raw) {
|
|
41
|
+
const lines = raw.split('\n');
|
|
42
|
+
const firstLine = lines[0] ?? '';
|
|
43
|
+
if (!firstLine)
|
|
44
|
+
return null;
|
|
45
|
+
// Conventional Commits 形如: "feat(scope)!: subject (#PR)" — 把尾 (#NN) 单独拆出来,
|
|
46
|
+
// 防 subject 内的 (xxx) 被 CONVENTIONAL_RE 的 scope-group (\\([^)]+\\)) 误吞。
|
|
47
|
+
const { head, prRef } = splitSubjectAndPr(firstLine);
|
|
48
|
+
const match = head.match(CONVENTIONAL_RE);
|
|
49
|
+
if (!match) {
|
|
50
|
+
return {
|
|
51
|
+
sha: '',
|
|
52
|
+
type: 'other',
|
|
53
|
+
scope: null,
|
|
54
|
+
subject: head,
|
|
55
|
+
prRef,
|
|
56
|
+
raw,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const [, type, scope] = match;
|
|
60
|
+
// subject:从 ": " 之后到行尾(已被 splitSubjectAndPr 切过尾)
|
|
61
|
+
const colonIdx = head.indexOf(': ');
|
|
62
|
+
const subject = colonIdx >= 0 ? head.slice(colonIdx + 2).trim() : head;
|
|
63
|
+
return {
|
|
64
|
+
sha: '',
|
|
65
|
+
type: type,
|
|
66
|
+
scope: scope ?? null,
|
|
67
|
+
subject,
|
|
68
|
+
prRef,
|
|
69
|
+
raw,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function splitSubjectAndPr(text) {
|
|
73
|
+
// 行尾的 " (#NN)" 拆出来;中文长 subject 中也可能有"(...)"形态,只切最后那个
|
|
74
|
+
const m = text.match(/\s+\(#(\d+)\)\s*$/);
|
|
75
|
+
if (m)
|
|
76
|
+
return { head: text.slice(0, m.index).trim(), prRef: m[1] };
|
|
77
|
+
return { head: text.trim(), prRef: null };
|
|
78
|
+
}
|
|
79
|
+
function extractPrRef(text) {
|
|
80
|
+
const m = text.match(/\(#(\d+)\)/);
|
|
81
|
+
return m ? m[1] : null;
|
|
82
|
+
}
|
|
83
|
+
export function listCommits(opts) {
|
|
84
|
+
const args = ['log', '--no-merges', '--pretty=format:%H%x00%s%x00%b%x00%x1e'];
|
|
85
|
+
if (opts.since)
|
|
86
|
+
args.push(`${opts.since}..HEAD`);
|
|
87
|
+
const out = execFileSync('git', args, { cwd: opts.cwd, encoding: 'utf8' });
|
|
88
|
+
if (!out)
|
|
89
|
+
return [];
|
|
90
|
+
return out.split('\u001e').filter(Boolean).map((raw) => {
|
|
91
|
+
const [sha, subject, body] = raw.split('\u0000').map((s) => (s ?? '').trim());
|
|
92
|
+
return { sha: sha ?? '', subject: subject ?? '', body: body ?? '' };
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/** 把 commit 列表分类成 sections;相同 type 合并,scope 作为 sub-group(同 type 内) */
|
|
96
|
+
export function groupCommits(commits) {
|
|
97
|
+
const sections = new Map();
|
|
98
|
+
for (const c of commits) {
|
|
99
|
+
if (!sections.has(c.type))
|
|
100
|
+
sections.set(c.type, { type: c.type, items: [] });
|
|
101
|
+
sections.get(c.type).items.push({ subject: c.subject, sha: c.sha, prRef: c.prRef });
|
|
102
|
+
}
|
|
103
|
+
// 按 DISPLAY_ORDER 输出
|
|
104
|
+
const out = [];
|
|
105
|
+
for (const t of DISPLAY_ORDER) {
|
|
106
|
+
const sec = sections.get(t);
|
|
107
|
+
if (sec && sec.items.length > 0)
|
|
108
|
+
out.push(sec);
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
export function renderMarkdownEntry(entry) {
|
|
113
|
+
const lines = [];
|
|
114
|
+
lines.push(`## [${entry.version}] - ${entry.date}`);
|
|
115
|
+
lines.push('');
|
|
116
|
+
for (const sec of entry.sections) {
|
|
117
|
+
const header = sec.type === 'other' ? 'Other' : (TYPE_TO_KEEP_A_CHANGELOG[sec.type] ?? sec.type);
|
|
118
|
+
lines.push(`### ${header}`);
|
|
119
|
+
lines.push('');
|
|
120
|
+
for (const item of sec.items) {
|
|
121
|
+
const subject = truncateSubject(item.subject, 120);
|
|
122
|
+
const suffix = item.prRef ? ` (#${item.prRef})` : (item.sha ? ` (${item.sha.slice(0, 7)})` : '');
|
|
123
|
+
lines.push(`- ${subject}${suffix}`);
|
|
124
|
+
}
|
|
125
|
+
lines.push('');
|
|
126
|
+
}
|
|
127
|
+
return lines.join('\n');
|
|
128
|
+
}
|
|
129
|
+
/** 长 subject 在第一个「。」/「——」/「;」处截断,防历史超长 commit message 把 CHANGELOG 撑爆;
|
|
130
|
+
* 失败兜底:超过 2×max 字符直接切 + …(短 breaker 优先;超长用纯字符切片) */
|
|
131
|
+
function truncateSubject(text, max) {
|
|
132
|
+
if (text.length <= max * 2) {
|
|
133
|
+
const breakers = ['。', '——', '; ', ' — ', ',', ','];
|
|
134
|
+
for (const b of breakers) {
|
|
135
|
+
const i = text.indexOf(b);
|
|
136
|
+
if (i > 30 && i <= max * 2)
|
|
137
|
+
return `${text.slice(0, i).trim()}…`;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return `${text.slice(0, max).trim()}…`;
|
|
141
|
+
}
|
|
142
|
+
export function buildEntry(opts) {
|
|
143
|
+
return { version: opts.version, date: opts.date, sections: groupCommits(opts.commits) };
|
|
144
|
+
}
|
|
145
|
+
const CHANGELOG_HEADER = `# Changelog
|
|
146
|
+
|
|
147
|
+
All notable changes to this project will be documented in this file.
|
|
148
|
+
|
|
149
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
150
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
151
|
+
|
|
152
|
+
`;
|
|
153
|
+
export function splitChangelog(raw) {
|
|
154
|
+
// 找第一个 "## [" 位置——之前的全是 preamble
|
|
155
|
+
const firstSection = raw.indexOf('\n## [');
|
|
156
|
+
if (firstSection < 0)
|
|
157
|
+
return { preamble: raw.endsWith('\n') ? raw : `${raw}\n`, history: [] };
|
|
158
|
+
const preamble = raw.slice(0, firstSection + 1); // 含末尾换行
|
|
159
|
+
const history = raw.slice(firstSection + 1);
|
|
160
|
+
return { preamble, history: [history] };
|
|
161
|
+
}
|
|
162
|
+
export function prependEntry(raw, entryMarkdown) {
|
|
163
|
+
if (!existsSync(join('..', 'CHANGELOG.md')) && !raw) {
|
|
164
|
+
return CHANGELOG_HEADER + entryMarkdown;
|
|
165
|
+
}
|
|
166
|
+
const split = splitChangelog(raw || (CHANGELOG_HEADER + ''));
|
|
167
|
+
// 头:preamble + 新版本段 + 历史段(history 中已含首个 "\n## [")
|
|
168
|
+
return split.preamble + entryMarkdown + '\n' + split.history.join('').replace(/^\n/, '');
|
|
169
|
+
}
|
|
170
|
+
function arg(name) {
|
|
171
|
+
const i = process.argv.indexOf(name);
|
|
172
|
+
return i >= 0 ? process.argv[i + 1] : undefined;
|
|
173
|
+
}
|
|
174
|
+
function readPackageVersion() {
|
|
175
|
+
const raw = JSON.parse(readFileSync(join('..', 'package.json'), 'utf8'));
|
|
176
|
+
if (!raw.version)
|
|
177
|
+
throw new Error('package.json 缺 version 字段');
|
|
178
|
+
return raw.version;
|
|
179
|
+
}
|
|
180
|
+
function gitHeadSha() {
|
|
181
|
+
return execFileSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf8' }).trim();
|
|
182
|
+
}
|
|
183
|
+
export function buildFromGit(opts) {
|
|
184
|
+
const rawCommits = listCommits({ cwd: opts.cwd, since: opts.since });
|
|
185
|
+
const parsed = [];
|
|
186
|
+
for (const r of rawCommits) {
|
|
187
|
+
const m = parseCommit(r.subject);
|
|
188
|
+
if (!m)
|
|
189
|
+
continue;
|
|
190
|
+
m.sha = r.sha;
|
|
191
|
+
m.raw = r.body;
|
|
192
|
+
parsed.push(m);
|
|
193
|
+
}
|
|
194
|
+
const entry = buildEntry({ version: opts.version, date: opts.date, commits: parsed });
|
|
195
|
+
const markdown = renderMarkdownEntry(entry);
|
|
196
|
+
return { entry, markdown };
|
|
197
|
+
}
|
|
198
|
+
async function main() {
|
|
199
|
+
const cwd = join('..');
|
|
200
|
+
const version = arg('--version') ?? readPackageVersion();
|
|
201
|
+
const date = arg('--date') ?? new Date().toISOString().slice(0, 10);
|
|
202
|
+
const since = arg('--since');
|
|
203
|
+
const write = process.argv.includes('--write');
|
|
204
|
+
let sinceTag = since;
|
|
205
|
+
if (!sinceTag) {
|
|
206
|
+
// 自动推断:取 tags 中 < version 的最高 tag
|
|
207
|
+
const out = execFileSync('git', ['tag', '-l', 'v*', '--sort=-v:refname'], { cwd, encoding: 'utf8' });
|
|
208
|
+
const tags = out.split('\n').map((t) => t.trim()).filter(Boolean);
|
|
209
|
+
sinceTag = tags.find((t) => t !== `v${version}` && tagsAreLessThan(t, `v${version}`)) ?? tags[tags.length - 1];
|
|
210
|
+
}
|
|
211
|
+
if (!sinceTag) {
|
|
212
|
+
console.error('build-changelog: 找不到起始 tag;传 --since <tag> 显式指定');
|
|
213
|
+
process.exit(2);
|
|
214
|
+
}
|
|
215
|
+
const { entry, markdown } = buildFromGit({ cwd, version, date, since: sinceTag });
|
|
216
|
+
const existing = existsSync(join(cwd, 'CHANGELOG.md')) ? readFileSync(join(cwd, 'CHANGELOG.md'), 'utf8') : '';
|
|
217
|
+
const next = write ? prependEntry(existing, markdown) : markdown;
|
|
218
|
+
console.log(`# build-changelog: ${entry.sections.length} sections / ${entry.sections.reduce((n, s) => n + s.items.length, 0)} commits since ${sinceTag}`);
|
|
219
|
+
for (const sec of entry.sections) {
|
|
220
|
+
console.log(`## ${sec.type} (${sec.items.length})`);
|
|
221
|
+
for (const it of sec.items)
|
|
222
|
+
console.log(` - ${it.subject}${it.prRef ? ` (#${it.prRef})` : ''}`);
|
|
223
|
+
}
|
|
224
|
+
if (write) {
|
|
225
|
+
writeFileSync(join(cwd, 'CHANGELOG.md'), next, 'utf8');
|
|
226
|
+
console.log(`\n>> wrote CHANGELOG.md (${next.length}B) @ ${gitHeadSha().slice(0, 7)}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function tagsAreLessThan(a, b) {
|
|
230
|
+
// 简化:字符串字典序比较(对 v0.0.1-rc.N 系列足够;复杂语义版本需 semver 库)
|
|
231
|
+
return a < b;
|
|
232
|
+
}
|
|
233
|
+
if (process.argv[1]?.endsWith('build-changelog.ts')) {
|
|
234
|
+
main().catch((e) => {
|
|
235
|
+
console.error(`build-changelog error: ${e.message}`);
|
|
236
|
+
process.exit(1);
|
|
237
|
+
});
|
|
238
|
+
}
|