@danceiny/gotry 0.0.1-rc.16 → 0.0.1-rc.18
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 +161 -116
- package/README.zh-CN.md +173 -144
- package/bin/gotry-booking-copilot.js +53 -0
- package/bin/gotry-bootstrap.js +113 -66
- package/bin/gotry-inner.js +436 -70
- package/bin/gotry-runtime-resolution.d.ts +27 -0
- package/bin/gotry-runtime-resolution.js +50 -0
- package/bin/gotry.js +1 -1
- package/cordis.gotry-patch.yml +5 -1
- package/dist/capabilities/agent-reach-deep.js +1 -1
- package/dist/capabilities/agent-reach.js +1 -1
- package/dist/capabilities/anything.js +1 -1
- package/dist/capabilities/artifacts.js +1 -1
- package/dist/capabilities/effect.js +1 -1
- package/dist/capabilities/fact-log.js +1 -1
- package/dist/capabilities/flyai.js +20 -6
- package/dist/capabilities/hbcli.js +1 -1
- package/dist/capabilities/incident-log.js +1 -1
- package/dist/capabilities/model-override.js +18 -0
- package/dist/capabilities/opensky.js +1 -1
- package/dist/capabilities/resilience.js +1 -1
- package/dist/capabilities/session/action-cache.js +1 -1
- package/dist/capabilities/session/adapters/ctrip-flight.js +1 -1
- package/dist/capabilities/session/adapters/meituan-local.js +1 -1
- package/dist/capabilities/session/benchmark.js +1 -1
- package/dist/capabilities/session/extension-bridge.js +19 -6
- package/dist/capabilities/session/extension-channel.js +3 -3
- package/dist/capabilities/session/extension-distribution.js +234 -0
- package/dist/capabilities/session/extract.js +1 -1
- package/dist/capabilities/session/golden-score.js +92 -0
- package/dist/capabilities/session/health-watch.js +1 -1
- package/dist/capabilities/session/read-guard.js +1 -1
- package/dist/capabilities/session/static-flight-golden.js +137 -0
- package/dist/capabilities/session/transport.js +1 -1
- package/dist/capabilities/session/wizard.js +21 -251
- package/dist/capabilities/session-consent.js +1 -1
- package/dist/capabilities/session-login.js +11 -5
- package/dist/capabilities/session-search.js +46 -4
- package/dist/capabilities/weather.js +168 -46
- package/dist/data/session-golden-20.json +25 -0
- package/dist/data/sf-golden-manifest.json +102 -0
- package/dist/data/sf-static-routes.json +91 -0
- package/dist/scripts/action-cache-tests.js +1 -1
- package/dist/scripts/agent-planning-budget-e2e.js +227 -0
- package/dist/scripts/agent-planning-budget-tests.js +173 -0
- package/dist/scripts/agent-reach-deep-tests.js +1 -1
- package/dist/scripts/agent-reach-tests.js +1 -1
- package/dist/scripts/agent-reach-wrapper-tests.js +1 -1
- package/dist/scripts/anything-tests.js +1 -1
- package/dist/scripts/async-collect.js +1 -1
- package/dist/scripts/benchmark-environment-bridge-e2e.js +981 -0
- package/dist/scripts/benchmark-environment-bridge-tests.js +2544 -0
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +335 -0
- package/dist/scripts/booking-copilot-availability-policy-v2-tests.js +1355 -0
- package/dist/scripts/booking-copilot-bin-proof-tests.js +89 -0
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +113 -0
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +356 -0
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +557 -0
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +140 -0
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +202 -0
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +102 -0
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +374 -0
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +447 -0
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +205 -0
- package/dist/scripts/booking-copilot-server-proof-tests.js +317 -0
- package/dist/scripts/booking-copilot-startup-proof-tests.js +283 -0
- package/dist/scripts/booking-copilot-v2-runtime-proof-tests.js +3935 -0
- package/dist/scripts/booking-saga-tests.js +1 -1
- package/dist/scripts/booking-surface-contract-proof-tests.js +393 -0
- package/dist/scripts/booking-surface-v2-contract-proof-tests.js +1174 -0
- package/dist/scripts/bootstrap-tests.js +45 -39
- package/dist/scripts/build-changelog.js +1 -1
- package/dist/scripts/changelog-tests.js +1 -1
- package/dist/scripts/companion-tests.js +1 -1
- package/dist/scripts/diff-test.js +1 -1
- package/dist/scripts/dsh-runtime-closure-tests.js +225 -0
- package/dist/scripts/dsh-runtime-closure.js +150 -0
- package/dist/scripts/effect-tests.js +1 -1
- package/dist/scripts/engine-run.js +1 -1
- package/dist/scripts/engine-tests.js +1 -1
- package/dist/scripts/evaluation-cadence-tests.js +347 -0
- package/dist/scripts/evaluation-contract-tests.js +574 -0
- package/dist/scripts/extension-distribution-cli.js +35 -0
- package/dist/scripts/extension-distribution-tests.js +341 -0
- package/dist/scripts/extension-tests.js +128 -23
- package/dist/scripts/fact-gate-tests.js +1 -1
- package/dist/scripts/flyai-tests.js +1 -1
- package/dist/scripts/hbcli-e2e-tests.js +1 -1
- package/dist/scripts/hbcli-tests.js +1 -1
- package/dist/scripts/health-watch-cli.js +1 -1
- package/dist/scripts/i18n-tests.js +1 -1
- package/dist/scripts/incident-tests.js +1 -1
- package/dist/scripts/journey-tests.js +1 -1
- package/dist/scripts/ledger-tests.js +1 -1
- package/dist/scripts/ledger-workflow-crash.js +1 -1
- package/dist/scripts/memory-capture-tests.js +1 -1
- package/dist/scripts/memory-decay-tests.js +1 -1
- package/dist/scripts/memory-metrics.js +1 -1
- package/dist/scripts/memory-value-report.js +1 -1
- package/dist/scripts/model-override-e2e.js +176 -0
- package/dist/scripts/nightly-evidence-tests.js +1 -1
- package/dist/scripts/nightly-evidence.js +1 -1
- package/dist/scripts/nudge-digest.js +1 -1
- package/dist/scripts/onboarding-tests.js +21 -53
- package/dist/scripts/opensky-check.js +1 -1
- package/dist/scripts/opensky-tests.js +1 -1
- package/dist/scripts/pnpm-dsh-closure-proof.js +20 -0
- package/dist/scripts/price-drift-tests.js +1 -1
- package/dist/scripts/price-drift-watch.js +1 -1
- package/dist/scripts/probe-poi-tests.js +1 -1
- package/dist/scripts/product-metrics.js +1 -1
- package/dist/scripts/publish-preverify.js +40 -4
- package/dist/scripts/realtime-pricing-tests.js +1 -1
- package/dist/scripts/replay-async.js +1 -1
- package/dist/scripts/replay-real.js +1 -1
- package/dist/scripts/replay.js +1 -1
- package/dist/scripts/session-attach-diagnose.js +1 -1
- package/dist/scripts/session-attach-poc.js +1 -1
- package/dist/scripts/session-benchmark.js +1 -1
- package/dist/scripts/session-extract-tests.js +1 -1
- package/dist/scripts/session-login.js +1 -1
- package/dist/scripts/session-tests.js +70 -20
- package/dist/scripts/sf-live-benchmark.js +338 -0
- package/dist/scripts/sf-live-cli-tests.js +21 -0
- package/dist/scripts/sf-soft-score-tests.js +108 -0
- package/dist/scripts/sf-summary.js +93 -0
- package/dist/scripts/skeleton-check.js +1 -1
- package/dist/scripts/skeleton-integration-test.js +1 -1
- package/dist/scripts/skills-contract-tests.js +1 -1
- package/dist/scripts/smoke-session-gate-tests.js +29 -0
- package/dist/scripts/smoke.js +79 -33
- package/dist/scripts/state-cli-tests.js +1 -1
- package/dist/scripts/state-cli.js +1 -1
- package/dist/scripts/static-golden-tests.js +299 -0
- package/dist/scripts/time-eval-tests.js +1 -1
- package/dist/scripts/travel-timeline-tests.js +1 -1
- package/dist/scripts/unified-tests.js +1 -1
- package/dist/scripts/weather-tests.js +694 -44
- package/dist/scripts/z3-race-tests.js +1 -1
- package/dist/src/artifact-gate.js +1 -1
- package/dist/src/benchmark-agent-conformance.js +370 -0
- package/dist/src/benchmark-environment-bridge.js +384 -0
- package/dist/src/benchmark-headless-child-diagnostics.js +173 -0
- package/dist/src/benchmark-tool-isolation.js +124 -0
- package/dist/src/bookable-facts.js +1 -1
- package/dist/src/booking-saga.js +1 -1
- package/dist/src/booking-surface/availability-policy-v2.js +830 -0
- package/dist/src/booking-surface/canonical-schema.js +113 -0
- package/dist/src/booking-surface/contracts-v2.js +89 -0
- package/dist/src/booking-surface/contracts.js +46 -0
- package/dist/src/booking-surface/dsh-planner.js +453 -0
- package/dist/src/booking-surface/dsh-plugin.js +93 -0
- package/dist/src/booking-surface/error-codes.js +94 -0
- package/dist/src/booking-surface/index.js +15 -0
- package/dist/src/booking-surface/profile.js +68 -0
- package/dist/src/booking-surface/runtime-v2.js +1771 -0
- package/dist/src/booking-surface/runtime.js +351 -0
- package/dist/src/booking-surface/server-v2.js +334 -0
- package/dist/src/booking-surface/server.js +302 -0
- package/dist/src/booking-surface/startup.js +159 -0
- package/dist/src/booking-surface/validation-v2.js +319 -0
- package/dist/src/booking-surface/validation.js +809 -0
- package/dist/src/bridge.js +1 -1
- package/dist/src/companions.js +1 -1
- package/dist/src/contracts.js +1 -1
- package/dist/src/dsh-llm.js +1 -1
- package/dist/src/engine.js +1 -1
- package/dist/src/evaluation-cadence.js +234 -0
- package/dist/src/evaluation-contracts.js +906 -0
- package/dist/src/i18n.js +1 -1
- package/dist/src/index.js +50 -19
- package/dist/src/journey.js +1 -1
- package/dist/src/loop.js +1 -1
- package/dist/src/memory-capture.js +1 -1
- package/dist/src/memory-decay.js +1 -1
- package/dist/src/memory-utility.js +1 -1
- package/dist/src/mock-llm.js +1 -1
- package/dist/src/model.js +1 -1
- package/dist/src/realtime-pricing.js +1 -1
- package/dist/src/slot-spec.js +1 -1
- package/dist/src/state-ledger.js +2 -1
- package/dist/src/time-anchor.js +1 -1
- package/dist/src/tool-budget.js +136 -0
- package/dist/src/tool-packet.js +1 -1
- package/dist/src/travel-slots.js +1 -1
- package/dist/src/travel-timeline.js +1 -1
- package/dist/src/unified.js +1 -1
- package/dist/src/wish-pool.js +1 -1
- package/dist/src/z3-shared.js +1 -1
- package/extension/README.md +31 -7
- package/package.json +286 -11
- package/schemas/booking.surface.v1.schema.json +927 -0
- package/schemas/booking.surface.v2.schema.json +61 -0
- package/ts/capabilities/flyai.ts +16 -3
- package/ts/capabilities/session/extension-bridge.ts +37 -14
- package/ts/capabilities/session/extension-channel.ts +6 -3
- package/ts/capabilities/session/extension-distribution.ts +264 -0
- package/ts/capabilities/session/golden-score.ts +139 -0
- package/ts/capabilities/session/health-watch.ts +1 -1
- package/ts/capabilities/session/static-flight-golden.ts +209 -0
- package/ts/capabilities/session/wizard.ts +34 -176
- package/ts/capabilities/session-login.ts +15 -5
- package/ts/capabilities/session-search.ts +40 -3
- package/ts/capabilities/weather.ts +141 -52
- package/ts/package.json +3 -3
- package/ts/src/benchmark-agent-conformance.ts +448 -0
- package/ts/src/benchmark-environment-bridge.ts +348 -0
- package/ts/src/benchmark-headless-child-diagnostics.ts +184 -0
- package/ts/src/benchmark-tool-isolation.ts +166 -0
- package/ts/src/booking-surface/availability-policy-v2.ts +523 -0
- package/ts/src/booking-surface/canonical-schema.js +113 -0
- package/ts/src/booking-surface/contracts-v2.ts +118 -0
- package/ts/src/booking-surface/contracts.ts +380 -0
- package/ts/src/booking-surface/dsh-planner.ts +452 -0
- package/ts/src/booking-surface/dsh-plugin.js +93 -0
- package/ts/src/booking-surface/error-codes.ts +101 -0
- package/ts/src/booking-surface/index.ts +12 -0
- package/ts/src/booking-surface/profile.ts +42 -0
- package/ts/src/booking-surface/runtime-v2.ts +1466 -0
- package/ts/src/booking-surface/runtime.ts +483 -0
- package/ts/src/booking-surface/server-v2.ts +247 -0
- package/ts/src/booking-surface/server.ts +324 -0
- package/ts/src/booking-surface/startup.ts +196 -0
- package/ts/src/booking-surface/validation-v2.ts +205 -0
- package/ts/src/booking-surface/validation.ts +453 -0
- package/ts/src/index.ts +64 -11
- package/ts/src/state-ledger.ts +1 -0
- package/ts/src/tool-budget.ts +165 -0
- package/dist/scripts/wizard-bootstrap.js +0 -32
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://gotry.dev/schemas/booking.surface.v2.schema.json",
|
|
4
|
+
"oneOf": [
|
|
5
|
+
{"$ref":"#/$defs/Workspace"},{"$ref":"#/$defs/Action"},{"$ref":"#/$defs/Receipt"},
|
|
6
|
+
{"$ref":"#/$defs/Ingress"},{"$ref":"#/$defs/UserTurn"},{"$ref":"#/$defs/Continuation"},{"$ref":"#/$defs/Event"}
|
|
7
|
+
],
|
|
8
|
+
"$defs": {
|
|
9
|
+
"Ref":{"$ref":"https://gotry.dev/schemas/booking.surface.v1.schema.json#/$defs/OpaqueRef"},
|
|
10
|
+
"Hex64":{"type":"string","pattern":"^[0-9a-f]{64}$"},
|
|
11
|
+
"Surface":{"enum":["tenant","customer_portal","storefront","payment_link"]},
|
|
12
|
+
"Kind":{"enum":["search.patch","search.run","results.view.patch","hotel.focus","hotel.select","offers.query","offers.view.patch","offers.compare","offer.select","offer.check","checkout.prepare","order.observe"]},
|
|
13
|
+
"Codes":{"type":"array","minItems":0,"maxItems":28,"uniqueItems":true,"items":{"enum":["byos_mapped_risk","check_avail_failed","check_avail_unverified","component_executor_unavailable","hotel_not_visible","hotel_rates_failed","offer_facts_changed","offer_not_loaded","offer_unavailable","order_not_found","order_outcome_not_observed","order_state_unknown","order_status_unavailable","requested_offers_not_loaded","search_failed","search_form_invalid","search_session_expired","search_terminal_timeout","stale_revision","surface_adapter_failed","undo_token_not_found","unhandled","unsupported","verified_offer_required","workspace_changed_during_action","criterion_must_not_met","no_hotels_matched","offer_target_not_reached"]}},
|
|
14
|
+
"LoadedOffer":{"type":"object","additionalProperties":false,"required":["offerRef","offerVersionRef","hotelRef","evidenceLevel","factRefs"],"properties":{"offerRef":{"$ref":"#/$defs/Ref"},"offerVersionRef":{"$ref":"#/$defs/Ref"},"hotelRef":{"$ref":"#/$defs/Ref"},"evidenceLevel":{"enum":["listed","rate_loaded","checked"]},"factRefs":{"type":"array","maxItems":64,"uniqueItems":true,"items":{"$ref":"#/$defs/Ref"}}}},
|
|
15
|
+
"VerifiedOffer":{"type":"object","additionalProperties":false,"required":["offerRef","offerVersionRef","verifiedOfferRef","expiresAt"],"properties":{"offerRef":{"$ref":"#/$defs/Ref"},"offerVersionRef":{"$ref":"#/$defs/Ref"},"verifiedOfferRef":{"$ref":"#/$defs/Ref"},"expiresAt":{"type":"string","pattern":"^\\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\\d|3[01])T(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d{1,9})?(?:Z|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$"}}},
|
|
16
|
+
"OfferVersionInput":{"type":"object","additionalProperties":false,"required":["offerRef","offerVersionRef"],"properties":{"offerRef":{"$ref":"#/$defs/Ref"},"offerVersionRef":{"$ref":"#/$defs/Ref"}}},
|
|
17
|
+
"CheckoutPrepareVersionInput":{"type":"object","additionalProperties":false,"required":["offerRef","offerVersionRef","verifiedOfferRef"],"properties":{"offerRef":{"$ref":"#/$defs/Ref"},"offerVersionRef":{"$ref":"#/$defs/Ref"},"verifiedOfferRef":{"$ref":"#/$defs/Ref"}}},
|
|
18
|
+
"Workspace":{"type":"object","additionalProperties":false,"required":["schemaVersion","contextRef","surface","revision","locale","currency","searchDraft","results","visibleHotels","loadedOffers","shortlistedOfferRefs","capabilities"],"properties":{"schemaVersion":{"const":"booking.surface.v2"},"contextRef":{"$ref":"#/$defs/Ref"},"surface":{"$ref":"#/$defs/Surface"},"revision":{"$ref":"https://gotry.dev/schemas/booking.surface.v1.schema.json#/$defs/Revision"},"locale":{"$ref":"https://gotry.dev/schemas/booking.surface.v1.schema.json#/$defs/NonEmptyString"},"currency":{"type":"string","pattern":"^[A-Z]{3}$"},"searchDraft":{"$ref":"https://gotry.dev/schemas/booking.surface.v1.schema.json#/$defs/SearchDraftSnapshotV1"},"results":{"$ref":"https://gotry.dev/schemas/booking.surface.v1.schema.json#/$defs/ResultViewSnapshotV1"},"visibleHotels":{"type":"array","maxItems":100,"items":{"$ref":"https://gotry.dev/schemas/booking.surface.v1.schema.json#/$defs/VisibleHotelFactV1"}},"loadedOffers":{"type":"array","maxItems":200,"items":{"$ref":"#/$defs/LoadedOffer"}},"focusedHotelRef":{"$ref":"#/$defs/Ref"},"shortlistedOfferRefs":{"type":"array","maxItems":100,"uniqueItems":true,"items":{"$ref":"#/$defs/Ref"}},"selectedOfferRef":{"$ref":"#/$defs/Ref"},"verifiedOffer":{"$ref":"#/$defs/VerifiedOffer"},"capabilities":{"type":"object","additionalProperties":false,"required":["surface","allowedActions"],"properties":{"surface":{"$ref":"#/$defs/Surface"},"allowedActions":{"type":"array","minItems":1,"maxItems":12,"uniqueItems":true,"items":{"$ref":"#/$defs/Kind"}}}}}},
|
|
19
|
+
"IngressWorkspace":{"type":"object","additionalProperties":false,"required":["schemaVersion","revision","locale","currency","searchDraft","results","visibleHotels","loadedOffers","shortlistedOfferRefs"],"properties":{"schemaVersion":{"const":"booking.surface.v2"},"revision":{"type":"integer","minimum":0},"locale":{"type":"string","minLength":1,"maxLength":64},"currency":{"type":"string","pattern":"^[A-Z]{3}$"},"searchDraft":{"$ref":"https://gotry.dev/schemas/booking.surface.v1.schema.json#/$defs/SearchDraftSnapshotV1"},"results":{"$ref":"https://gotry.dev/schemas/booking.surface.v1.schema.json#/$defs/ResultViewSnapshotV1"},"visibleHotels":{"type":"array","maxItems":100,"items":{"$ref":"https://gotry.dev/schemas/booking.surface.v1.schema.json#/$defs/VisibleHotelFactV1"}},"loadedOffers":{"type":"array","maxItems":200,"items":{"$ref":"#/$defs/LoadedOffer"}},"focusedHotelRef":{"$ref":"#/$defs/Ref"},"shortlistedOfferRefs":{"type":"array","maxItems":100,"uniqueItems":true,"items":{"$ref":"#/$defs/Ref"}},"selectedOfferRef":{"$ref":"#/$defs/Ref"}}},
|
|
20
|
+
"PresentationKey":{"type":"string","minLength":1,"maxLength":512},
|
|
21
|
+
"RequestKey":{"type":"string","minLength":8,"maxLength":128,"pattern":"^[A-Za-z0-9][A-Za-z0-9:._-]{7,127}$"},
|
|
22
|
+
"ApprovalRef":{"type":"object","additionalProperties":false,"required":["approvalId","blockerId","contextRef","sourceTurnId","presentationRequestKey","sourceActionId","targetActionId","sourceRevision","targetActionKind","to","expiresAt","nonce","sourceReceiptDigest","scope","code","criterionPath","valueDigest"],"properties":{"approvalId":{"$ref":"#/$defs/Ref"},"blockerId":{"$ref":"#/$defs/Ref"},"contextRef":{"$ref":"#/$defs/Ref"},"sourceTurnId":{"$ref":"#/$defs/Ref"},"presentationRequestKey":{"$ref":"#/$defs/PresentationKey"},"sourceActionId":{"$ref":"#/$defs/Ref"},"targetActionId":{"$ref":"#/$defs/Ref"},"sourceRevision":{"type":"integer","minimum":0},"targetActionKind":{"$ref":"#/$defs/Kind"},"to":{"enum":["prefer","drop"]},"expiresAt":{"type":"string","minLength":1},"nonce":{"$ref":"#/$defs/Ref"},"sourceReceiptDigest":{"$ref":"#/$defs/Hex64"},"scope":{"enum":["search","offer","availability","checkout"]},"code":{"enum":["no_hotels_matched","hotel_not_visible","hotel_rates_failed","criterion_must_not_met","offer_target_not_reached","offer_unavailable"]},"criterionPath":{"type":"string","pattern":"^(searchDraft|results|offers|checkout)(\\.[A-Za-z][A-Za-z0-9_]*){0,3}$","maxLength":160},"valueDigest":{"$ref":"#/$defs/Hex64"}}},
|
|
23
|
+
"ActionBase":{"type":"object","additionalProperties":false,"required":["schemaVersion","kind","actionId","contextRef","expectedRevision","reason","factRefs","input"],"properties":{"schemaVersion":{"const":"booking.surface.v2"},"kind":{"$ref":"#/$defs/Kind"},"actionId":{"$ref":"#/$defs/Ref"},"contextRef":{"$ref":"#/$defs/Ref"},"expectedRevision":{"type":"integer","minimum":0},"reason":{"type":"string","minLength":1,"maxLength":1000},"factRefs":{"type":"array","maxItems":64,"uniqueItems":true,"items":{"$ref":"#/$defs/Ref"}},"input":{"type":"object"},"relaxationApprovalRef":{"$ref":"#/$defs/ApprovalRef"}}},
|
|
24
|
+
"Action":{"oneOf":[
|
|
25
|
+
{"allOf":[{"$ref":"#/$defs/ActionBase"},{"type":"object","properties":{"kind":{"const":"search.patch"},"input":{"$ref":"https://gotry.dev/schemas/booking.surface.v1.schema.json#/$defs/SearchPatchInputV1"}}}]},
|
|
26
|
+
{"allOf":[{"$ref":"#/$defs/ActionBase"},{"type":"object","properties":{"kind":{"const":"search.run"},"input":{"$ref":"https://gotry.dev/schemas/booking.surface.v1.schema.json#/$defs/SearchRunInputV1"}}}]},
|
|
27
|
+
{"allOf":[{"$ref":"#/$defs/ActionBase"},{"type":"object","properties":{"kind":{"const":"results.view.patch"},"input":{"$ref":"https://gotry.dev/schemas/booking.surface.v1.schema.json#/$defs/ResultsViewPatchInputV1"}}}]},
|
|
28
|
+
{"allOf":[{"$ref":"#/$defs/ActionBase"},{"type":"object","properties":{"kind":{"const":"hotel.focus"},"input":{"$ref":"https://gotry.dev/schemas/booking.surface.v1.schema.json#/$defs/HotelFocusInputV1"}}}]},
|
|
29
|
+
{"allOf":[{"$ref":"#/$defs/ActionBase"},{"type":"object","properties":{"kind":{"const":"hotel.select"},"input":{"$ref":"https://gotry.dev/schemas/booking.surface.v1.schema.json#/$defs/HotelSelectInputV1"}}}]},
|
|
30
|
+
{"allOf":[{"$ref":"#/$defs/ActionBase"},{"type":"object","properties":{"kind":{"const":"offers.query"},"input":{"$ref":"https://gotry.dev/schemas/booking.surface.v1.schema.json#/$defs/OffersQueryInputV1"}}}]},
|
|
31
|
+
{"allOf":[{"$ref":"#/$defs/ActionBase"},{"type":"object","properties":{"kind":{"const":"offers.view.patch"},"input":{"$ref":"https://gotry.dev/schemas/booking.surface.v1.schema.json#/$defs/OffersViewPatchInputV1"}}}]},
|
|
32
|
+
{"allOf":[{"$ref":"#/$defs/ActionBase"},{"type":"object","properties":{"kind":{"const":"offers.compare"},"input":{"$ref":"https://gotry.dev/schemas/booking.surface.v1.schema.json#/$defs/OffersCompareInputV1"}}}]},
|
|
33
|
+
{"allOf":[{"$ref":"#/$defs/ActionBase"},{"type":"object","properties":{"kind":{"const":"offer.select"},"input":{"$ref":"#/$defs/OfferVersionInput"}}}]},
|
|
34
|
+
{"allOf":[{"$ref":"#/$defs/ActionBase"},{"type":"object","properties":{"kind":{"const":"offer.check"},"input":{"$ref":"#/$defs/OfferVersionInput"}}}]},
|
|
35
|
+
{"allOf":[{"$ref":"#/$defs/ActionBase"},{"type":"object","properties":{"kind":{"const":"checkout.prepare"},"input":{"$ref":"#/$defs/CheckoutPrepareVersionInput"}}}]},
|
|
36
|
+
{"allOf":[{"$ref":"#/$defs/ActionBase"},{"type":"object","properties":{"kind":{"const":"order.observe"},"input":{"$ref":"https://gotry.dev/schemas/booking.surface.v1.schema.json#/$defs/OrderObserveInputV1"}}}]}
|
|
37
|
+
]},
|
|
38
|
+
"Approval":{"type":"object","additionalProperties":false,"required":["taskId","contextRef","sourceTurnId","presentationRequestKey","optionDigest","approvalId","deliveryNonce","blockerId","sourceActionId","sourceReceiptDigest","scope","code","criterionPath","valueDigest","from","to","approved"],"properties":{"taskId":{"$ref":"#/$defs/Ref"},"contextRef":{"$ref":"#/$defs/Ref"},"sourceTurnId":{"$ref":"#/$defs/Ref"},"presentationRequestKey":{"$ref":"#/$defs/PresentationKey"},"optionDigest":{"$ref":"#/$defs/Hex64"},"approvalId":{"$ref":"#/$defs/Ref"},"deliveryNonce":{"$ref":"#/$defs/Ref"},"blockerId":{"$ref":"#/$defs/Ref"},"sourceActionId":{"$ref":"#/$defs/Ref"},"sourceReceiptDigest":{"$ref":"#/$defs/Hex64"},"scope":{"enum":["search","offer","availability","checkout"]},"code":{"enum":["no_hotels_matched","hotel_not_visible","hotel_rates_failed","criterion_must_not_met","offer_target_not_reached","offer_unavailable"]},"criterionPath":{"type":"string","pattern":"^(searchDraft|results|offers|checkout)(\\.[A-Za-z][A-Za-z0-9_]*){0,3}$","maxLength":160},"valueDigest":{"$ref":"#/$defs/Hex64"},"from":{"const":"must"},"to":{"enum":["prefer","drop"]},"approved":{"const":true}}},
|
|
39
|
+
"Blocker":{"type":"object","additionalProperties":false,"required":["blockerId","sourceActionId","sourceReceiptDigest","scope","code","criterionPath","strength","valueDigest","evidence"],"properties":{"blockerId":{"$ref":"#/$defs/Ref"},"sourceActionId":{"$ref":"#/$defs/Ref"},"sourceReceiptDigest":{"$ref":"#/$defs/Hex64"},"scope":{"enum":["search","offer","availability","checkout"]},"code":{"enum":["no_hotels_matched","hotel_not_visible","hotel_rates_failed","criterion_must_not_met","offer_target_not_reached","offer_unavailable"]},"criterionPath":{"type":"string","pattern":"^(searchDraft|results|offers|checkout)(\\.[A-Za-z][A-Za-z0-9_]*){0,3}$","maxLength":160},"strength":{"const":"must"},"valueDigest":{"$ref":"#/$defs/Hex64"},"valueLabel":{"type":"string","minLength":1,"maxLength":120},"evidence":{"type":"object","additionalProperties":false,"required":["factRefs","gapCodes"],"properties":{"factRefs":{"type":"array","minItems":1,"maxItems":32,"uniqueItems":true,"items":{"$ref":"#/$defs/Ref"}},"gapCodes":{"$ref":"#/$defs/Codes"},"requested":{"type":"integer","minimum":0},"actual":{"type":"integer","minimum":0}}}}},
|
|
40
|
+
"Result":{"type":"object","additionalProperties":false,"required":["outcome","hardCriteriaMet","factRefs","gapCodes","blockers","relaxationsApplied"],"properties":{"outcome":{"enum":["complete","partial","empty"]},"requestedCount":{"type":"integer","minimum":0},"actualCount":{"type":"integer","minimum":0},"hardCriteriaMet":{"type":"boolean"},"factRefs":{"type":"array","maxItems":64,"uniqueItems":true,"items":{"$ref":"#/$defs/Ref"}},"gapCodes":{"$ref":"#/$defs/Codes"},"blockers":{"type":"array","maxItems":8,"items":{"$ref":"#/$defs/Blocker"}},"relaxationsApplied":{"type":"array","maxItems":1,"items":{"$ref":"#/$defs/Approval"}}}},
|
|
41
|
+
"Observation":{"oneOf":[{"type":"object","additionalProperties":false,"required":["kind"],"properties":{"kind":{"const":"search.state"},"searchSessionRef":{"$ref":"#/$defs/Ref"},"resultCount":{"type":"integer","minimum":0},"gapCodes":{"$ref":"#/$defs/Codes"}}},{"type":"object","additionalProperties":false,"required":["kind","matchedHotelRefs","visibleCount"],"properties":{"kind":{"const":"results.state"},"matchedHotelRefs":{"type":"array","maxItems":100,"items":{"$ref":"#/$defs/Ref"}},"visibleCount":{"type":"integer","minimum":0},"gapCodes":{"$ref":"#/$defs/Codes"}}},{"type":"object","additionalProperties":false,"required":["kind","hotelRef"],"properties":{"kind":{"enum":["hotel.focus","hotel.selection"]},"hotelRef":{"$ref":"#/$defs/Ref"}}},{"type":"object","additionalProperties":false,"required":["kind","hotelRefs","offerRefs","loadedHotelCount"],"properties":{"kind":{"const":"offers.state"},"hotelRefs":{"type":"array","maxItems":100,"items":{"$ref":"#/$defs/Ref"}},"offerRefs":{"type":"array","maxItems":200,"items":{"$ref":"#/$defs/Ref"}},"loadedHotelCount":{"type":"integer","minimum":0},"gapCodes":{"$ref":"#/$defs/Codes"}}},{"type":"object","additionalProperties":false,"required":["kind","offerRef","checkedOfferVersionRef","available","changedFactRefs"],"properties":{"kind":{"const":"offer.availability"},"offerRef":{"$ref":"#/$defs/Ref"},"checkedOfferVersionRef":{"$ref":"#/$defs/Ref"},"currentOfferVersionRef":{"$ref":"#/$defs/Ref"},"verifiedOfferRef":{"$ref":"#/$defs/Ref"},"available":{"type":"boolean"},"changedFactRefs":{"type":"array","maxItems":64,"items":{"$ref":"#/$defs/Ref"}},"gapCodes":{"$ref":"#/$defs/Codes"}}},{"type":"object","additionalProperties":false,"required":["kind","code","factRefs"],"properties":{"kind":{"const":"gap"},"code":{"enum":["byos_mapped_risk","check_avail_failed","check_avail_unverified","component_executor_unavailable","hotel_not_visible","hotel_rates_failed","offer_facts_changed","offer_not_loaded","offer_unavailable","order_not_found","order_outcome_not_observed","order_state_unknown","order_status_unavailable","requested_offers_not_loaded","search_failed","search_form_invalid","search_session_expired","search_terminal_timeout","stale_revision","surface_adapter_failed","undo_token_not_found","unhandled","unsupported","verified_offer_required","workspace_changed_during_action","criterion_must_not_met","no_hotels_matched","offer_target_not_reached"]},"factRefs":{"type":"array","maxItems":64,"items":{"$ref":"#/$defs/Ref"}}}}]},
|
|
42
|
+
"OfferSelectionObservation":{"type":"object","additionalProperties":false,"required":["kind","offerRef","offerVersionRef"],"properties":{"kind":{"const":"offer.selection"},"offerRef":{"$ref":"#/$defs/Ref"},"offerVersionRef":{"$ref":"#/$defs/Ref"}}},
|
|
43
|
+
"HandoffObservation":{"type":"object","additionalProperties":false,"required":["kind","offerRef","offerVersionRef","verifiedOfferRef","handoffRef"],"properties":{"kind":{"const":"checkout.handoff"},"offerRef":{"$ref":"#/$defs/Ref"},"offerVersionRef":{"$ref":"#/$defs/Ref"},"verifiedOfferRef":{"$ref":"#/$defs/Ref"},"handoffRef":{"$ref":"#/$defs/Ref"}}},
|
|
44
|
+
"OrderObservation":{"type":"object","additionalProperties":false,"required":["kind","orderRef","state"],"properties":{"kind":{"const":"order.state"},"orderRef":{"$ref":"#/$defs/Ref"},"state":{"enum":["pending","verified","failed","unknown"]},"gapCodes":{"$ref":"#/$defs/Codes"}}},
|
|
45
|
+
"ObservationExpanded":{"oneOf":[{"$ref":"#/$defs/Observation"},{"$ref":"#/$defs/OfferSelectionObservation"},{"$ref":"#/$defs/HandoffObservation"},{"$ref":"#/$defs/OrderObservation"}]},
|
|
46
|
+
"Receipt":{"type":"object","additionalProperties":false,"required":["schemaVersion","kind","actionId","contextRef","status","revision","observation","resultContract"],"properties":{"schemaVersion":{"const":"booking.surface.v2"},"kind":{"const":"action.receipt"},"actionId":{"$ref":"#/$defs/Ref"},"contextRef":{"$ref":"#/$defs/Ref"},"status":{"enum":["applied","needs_input","partial","no_match","unavailable","changed","stale","unsupported","failed"]},"revision":{"type":"integer","minimum":0},"observation":{"$ref":"#/$defs/ObservationExpanded"},"resultContract":{"$ref":"#/$defs/Result"},"undoToken":{"$ref":"#/$defs/Ref"}}},
|
|
47
|
+
"Ingress":{"type":"object","additionalProperties":false,"required":["schemaVersion","kind","requestKey","surfaceHint","workspace","request"],"properties":{"schemaVersion":{"const":"booking.surface.v2"},"kind":{"const":"user.turn.ingress"},"requestKey":{"$ref":"#/$defs/RequestKey"},"taskHandle":{"$ref":"#/$defs/Ref"},"surfaceHint":{"$ref":"#/$defs/Surface"},"workspace":{"$ref":"#/$defs/IngressWorkspace"},"request":{"type":"object","additionalProperties":false,"required":["text"],"properties":{"text":{"type":"string","minLength":1,"maxLength":4000}}}}},
|
|
48
|
+
"UserTurn":{"type":"object","additionalProperties":false,"required":["schemaVersion","kind","taskId","turnId","workspace","request"],"properties":{"schemaVersion":{"const":"booking.surface.v2"},"kind":{"const":"user.turn"},"taskId":{"$ref":"#/$defs/Ref"},"turnId":{"$ref":"#/$defs/Ref"},"workspace":{"$ref":"#/$defs/Workspace"},"request":{"type":"object","additionalProperties":false,"required":["text"],"properties":{"text":{"type":"string","minLength":1,"maxLength":4000},"approval":{"$ref":"#/$defs/Approval"}}}}},
|
|
49
|
+
"Continuation":{"type":"object","additionalProperties":false,"required":["schemaVersion","kind","taskId","workspace","receipt"],"properties":{"schemaVersion":{"const":"booking.surface.v2"},"kind":{"const":"action.receipt.continuation"},"taskId":{"$ref":"#/$defs/Ref"},"workspace":{"$ref":"#/$defs/Workspace"},"receipt":{"$ref":"#/$defs/Receipt"}}},
|
|
50
|
+
"Event":{"oneOf":[
|
|
51
|
+
{"$ref":"#/$defs/StatusEvent"},{"$ref":"#/$defs/QuestionEvent"},{"$ref":"#/$defs/OperationEvent"},{"$ref":"#/$defs/ExplanationEvent"},{"$ref":"#/$defs/TerminalEvent"},{"$ref":"#/$defs/ErrorEvent"}
|
|
52
|
+
]},
|
|
53
|
+
"EventBase":{"type":"object","required":["schemaVersion","eventId","taskId","contextRef","sequence","emittedAt","kind"],"properties":{"schemaVersion":{"const":"booking.surface.v2"},"eventId":{"$ref":"#/$defs/Ref"},"taskId":{"$ref":"#/$defs/Ref"},"contextRef":{"$ref":"#/$defs/Ref"},"sequence":{"type":"integer","minimum":1},"emittedAt":{"type":"string","minLength":1},"kind":{"enum":["status","question","operation","explanation","terminal","error"]}}},
|
|
54
|
+
"StatusEvent":{"type":"object","allOf":[{"$ref":"#/$defs/EventBase"},{"type":"object","required":["kind","status"],"properties":{"kind":{"const":"status"},"status":{"enum":["submitted","working","waiting_receipt","input_required"]}}}],"unevaluatedProperties":false},
|
|
55
|
+
"QuestionEvent":{"type":"object","additionalProperties":false,"required":["schemaVersion","eventId","taskId","contextRef","sequence","emittedAt","kind","question"],"properties":{"schemaVersion":{"const":"booking.surface.v2"},"eventId":{"$ref":"#/$defs/Ref"},"taskId":{"$ref":"#/$defs/Ref"},"contextRef":{"$ref":"#/$defs/Ref"},"sequence":{"type":"integer","minimum":1},"emittedAt":{"type":"string","minLength":1},"kind":{"const":"question"},"question":{"type":"object","additionalProperties":false,"required":["questionId","prompt","missingFields","type","blocker","approvalOptions"],"properties":{"questionId":{"$ref":"#/$defs/Ref"},"prompt":{"type":"string","minLength":1},"missingFields":{"type":"array","maxItems":32,"items":{"type":"string"}},"type":{"const":"relaxation_approval_required"},"blocker":{"$ref":"#/$defs/Blocker"},"approvalOptions":{"type":"array","minItems":1,"maxItems":2,"items":{"type":"object","additionalProperties":false,"required":["approval"],"properties":{"approval":{"$ref":"#/$defs/Approval"}}}}}}}},
|
|
56
|
+
"OperationEvent":{"type":"object","allOf":[{"$ref":"#/$defs/EventBase"},{"type":"object","required":["kind","action"],"properties":{"kind":{"const":"operation"},"action":{"$ref":"#/$defs/Action"}}}],"unevaluatedProperties":false},
|
|
57
|
+
"ExplanationEvent":{"type":"object","allOf":[{"$ref":"#/$defs/EventBase"},{"type":"object","required":["kind","explanation"],"properties":{"kind":{"const":"explanation"},"explanation":{"type":"object","additionalProperties":false,"required":["text","factRefs"],"properties":{"text":{"type":"string","minLength":1},"factRefs":{"type":"array","maxItems":64,"items":{"$ref":"#/$defs/Ref"}}}}}}],"unevaluatedProperties":false},
|
|
58
|
+
"TerminalEvent":{"type":"object","allOf":[{"$ref":"#/$defs/EventBase"},{"type":"object","required":["kind","terminal"],"properties":{"kind":{"const":"terminal"},"terminal":{"type":"object","additionalProperties":false,"required":["status","summary","factRefs"],"properties":{"status":{"enum":["completed","stopped"]},"summary":{"type":"string","minLength":1},"factRefs":{"type":"array","maxItems":64,"items":{"$ref":"#/$defs/Ref"}}}}}}],"unevaluatedProperties":false},
|
|
59
|
+
"ErrorEvent":{"type":"object","allOf":[{"$ref":"#/$defs/EventBase"},{"type":"object","required":["kind","error"],"properties":{"kind":{"const":"error"},"error":{"type":"object","additionalProperties":false,"required":["code","message","retryable"],"properties":{"code":{"type":"string","pattern":"^[A-Z][A-Z0-9_.-]*$"},"message":{"type":"string","minLength":1},"retryable":{"type":"boolean"}}}}}],"unevaluatedProperties":false}
|
|
60
|
+
}
|
|
61
|
+
}
|
package/ts/capabilities/flyai.ts
CHANGED
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { spawn } from 'node:child_process'
|
|
18
|
+
import { closeSync, openSync, readFileSync, unlinkSync } from 'node:fs'
|
|
19
|
+
import { tmpdir } from 'node:os'
|
|
20
|
+
import { join } from 'node:path'
|
|
18
21
|
|
|
19
22
|
export type FlyaiKind = 'flight' | 'train' | 'hotel'
|
|
20
23
|
|
|
@@ -175,20 +178,30 @@ export function parseFlyaiItemList(stdout: string): unknown[] {
|
|
|
175
178
|
}
|
|
176
179
|
|
|
177
180
|
|
|
181
|
+
/**
|
|
182
|
+
* 子进程 stdout 走临时文件而非管道(issue #84):
|
|
183
|
+
* flyai CLI 异步写 stdout 后立即 process.exit();stdout 是管道时未 flush 的
|
|
184
|
+
* 尾部会被丢掉(实测截断在 ~7.6KB,exit=0 静默)。stdout 是文件时 Node 同步写,
|
|
185
|
+
* exit 不丢数据;且文件无 64KB 管道缓冲上限。close 后读文件,语义不变。
|
|
186
|
+
*/
|
|
178
187
|
function sh(cmd: string, args: string[], opts: { timeoutMs: number }) {
|
|
179
|
-
const
|
|
180
|
-
|
|
188
|
+
const outFile = join(tmpdir(), `gotry-flyai-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.out`)
|
|
189
|
+
const outFd = openSync(outFile, 'w')
|
|
190
|
+
const child = spawn(cmd, args, { env: process.env, cwd: process.cwd(), stdio: ['ignore', outFd, 'pipe'] })
|
|
181
191
|
let stderr = ''
|
|
182
192
|
let error: string | undefined
|
|
183
193
|
const timer = setTimeout(() => {
|
|
184
194
|
try { child.kill('SIGKILL') } catch { /* ignore */ }
|
|
185
195
|
}, opts.timeoutMs)
|
|
186
196
|
return new Promise<{ code: number; stdout: string; stderr: string; error?: string; timedOut?: boolean }>((resolve) => {
|
|
187
|
-
child.stdout?.on('data', (d: Buffer) => { stdout += d.toString() })
|
|
188
197
|
child.stderr?.on('data', (d: Buffer) => { stderr += d.toString() })
|
|
189
198
|
child.on('error', (e) => { error = (e as Error).message.slice(0, 200) })
|
|
190
199
|
child.on('close', (code) => {
|
|
191
200
|
clearTimeout(timer)
|
|
201
|
+
closeSync(outFd)
|
|
202
|
+
let stdout = ''
|
|
203
|
+
try { stdout = readFileSync(outFile, 'utf8') } catch { /* 子进程未启动时文件可能为空,按空 stdout 处理 */ }
|
|
204
|
+
try { unlinkSync(outFile) } catch { /* ignore */ }
|
|
192
205
|
resolve({ code: code ?? -1, stdout, stderr, error })
|
|
193
206
|
})
|
|
194
207
|
}).finally(() => clearTimeout(timer))
|
|
@@ -5,14 +5,16 @@
|
|
|
5
5
|
* host_permissions 一一对应,防漂移测试守住);**零新依赖**(node:http 手写,对齐已退役 shell 薄壳
|
|
6
6
|
* 的回环纪律;不引 ws——MV3 Service Worker 靠 ≤20s 的长轮询节奏维持 30s 存活窗口,HTTP 够用)。
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* 授权模型:扩展经「一次性安装」获得信任,每请求校验 Origin ∈ 双通道扩展源白名单
|
|
9
|
+
* (网页发起的跨域请求必带邪恶 Origin,直接 403;POST 端点强制;GET /health /status 是本机
|
|
10
|
+
* 诊断面放行)。不做 CORS——有 host_permissions 的扩展 fetch 天然免预检,网页过不来。
|
|
11
|
+
* 双通道 ID 实录(2026-09-02):unpacked 通道(bundled/GitHub Releases)由 manifest 固定 key
|
|
12
|
+
* 派生 EXTENSION_ID;Chrome Web Store 用商店自己的签名 key 重签(不认 manifest key),
|
|
13
|
+
* 商店版 ID = EXTENSION_ID_STORE。两个 ID 都是 founder 控制的同一扩展,白名单双收。
|
|
12
14
|
*
|
|
13
15
|
* 协议(长轮询):
|
|
14
16
|
* GET /health → lastSeen 刷新(扩展每 ≤30s 一次 + 每次断线重连)
|
|
15
|
-
* GET /status → 诊断面 {port, extensionConnected, lastSeenMsAgo}
|
|
17
|
+
* GET /status → 诊断面 {port, extensionConnected, lastSeenMsAgo, queued, inFlight, parked}
|
|
16
18
|
* POST /jobs → 长轮询取活(hold ≤20s 返 {job:null});扩展单客户端
|
|
17
19
|
* POST /results/:jobId → 善果/失败回传(≤8MB;batchSearch ~550KB)
|
|
18
20
|
* 语义:桥不存在即整个扩展车道不存在——fail-closed 不变量在调用方(session-search 的 needs-extension)。
|
|
@@ -25,9 +27,20 @@ import { createServer, type IncomingMessage, type Server, type ServerResponse }
|
|
|
25
27
|
/** 桥端口池(extension/manifest.json host_permissions 必须同面;§38 断言) */
|
|
26
28
|
export const BRIDGE_PORTS = [8791, 8792, 8793, 8794, 8795] as const
|
|
27
29
|
export const BRIDGE_PROTOCOL = 'session-bridge.v1'
|
|
28
|
-
/** 扩展固定 ID(manifest key
|
|
30
|
+
/** 扩展固定 ID(unpacked 通道:manifest key 派生;bundled/GitHub Releases 均此 ID。变更即失配,Node 侧拒绝) */
|
|
29
31
|
export const EXTENSION_ID = 'olpgkofjhhiiiahdkkbcninhjmegghfe'
|
|
30
32
|
export const EXTENSION_ORIGIN = `chrome-extension://${EXTENSION_ID}`
|
|
33
|
+
/**
|
|
34
|
+
* Chrome Web Store 版扩展 ID(2026-09-02 上架):商店用自己生成的签名 key 重签,
|
|
35
|
+
* **不认 manifest 里的固定 key**——商店版 ID 与 unpacked 版不同(上架材料文档的
|
|
36
|
+
* 「若商店不认 key」预案坐实)。item ID 即商店详情页 URL 末段。
|
|
37
|
+
*/
|
|
38
|
+
export const EXTENSION_ID_STORE = 'oeajpiccmonococjcegddlooeeohlbgd'
|
|
39
|
+
export const EXTENSION_ORIGIN_STORE = `chrome-extension://${EXTENSION_ID_STORE}`
|
|
40
|
+
/** 商店详情页(一键安装 + 自动更新通道;推荐安装方式) */
|
|
41
|
+
export const EXTENSION_STORE_URL = `https://chromewebstore.google.com/detail/gotry-session-bridge/${EXTENSION_ID_STORE}`
|
|
42
|
+
/** 桥信任的扩展 Origin 白名单:unpacked(固定 key)+ 商店版,双通道同一扩展同源信任 */
|
|
43
|
+
export const EXTENSION_ORIGINS: readonly string[] = [EXTENSION_ORIGIN, EXTENSION_ORIGIN_STORE]
|
|
31
44
|
/** 扩展在线判定:/health 心跳 ≤30s 一次 + 轮询重连,1.5 倍容差 */
|
|
32
45
|
export const EXTENSION_CONNECTED_WINDOW_MS = 45_000
|
|
33
46
|
/** /jobs 长轮询 hold 上限(必须 < MV3 SW 30s 存活窗口) */
|
|
@@ -76,7 +89,8 @@ export interface SessionJobHandle {
|
|
|
76
89
|
export interface SessionBridgeOptions {
|
|
77
90
|
/** 测试用 [0] 让 OS 分配;缺省 8791..8795 端口池 */
|
|
78
91
|
ports?: number[]
|
|
79
|
-
|
|
92
|
+
/** 测试覆盖信任的扩展 Origin 白名单;缺省 EXTENSION_ORIGINS(unpacked + 商店版双通道) */
|
|
93
|
+
extensionOrigins?: readonly string[]
|
|
80
94
|
now?: () => number
|
|
81
95
|
/**
|
|
82
96
|
* **保持桥进程钉住事件循环**(不 unref)。wizard 期间需要:扩展 SW 周期心跳
|
|
@@ -118,7 +132,7 @@ function readBody(req: IncomingMessage, cap: number): Promise<string | { err: st
|
|
|
118
132
|
/** 创 Bridge 服务(测试可直接调;生产走 getOrCreateSessionBridge 懒单例) */
|
|
119
133
|
export async function createSessionBridge(opts: SessionBridgeOptions = {}): Promise<{ ok: true; bridge: SessionJobHandle } | { ok: false; summary: string }> {
|
|
120
134
|
const ports = opts.ports ?? [...BRIDGE_PORTS]
|
|
121
|
-
const
|
|
135
|
+
const extensionOrigins = new Set(opts.extensionOrigins ?? EXTENSION_ORIGINS)
|
|
122
136
|
const now = opts.now ?? Date.now
|
|
123
137
|
const queue: QueuedJob[] = []
|
|
124
138
|
const inFlight = new Map<string, QueuedJob>()
|
|
@@ -140,17 +154,18 @@ export async function createSessionBridge(opts: SessionBridgeOptions = {}): Prom
|
|
|
140
154
|
const urlPath = (req.url ?? '').split('?')[0]
|
|
141
155
|
const origin = req.headers.origin
|
|
142
156
|
const isPost = req.method === 'POST'
|
|
143
|
-
|
|
144
|
-
|
|
157
|
+
const originTrusted = typeof origin === 'string' && extensionOrigins.has(origin)
|
|
158
|
+
// 网页侧请求必带 Origin;桥端点只信任白名单内的 chrome-extension:// 源(诊断 GET 面放行)
|
|
159
|
+
if (isPost && !originTrusted) {
|
|
145
160
|
res.statusCode = 403
|
|
146
161
|
res.setHeader('content-type', 'application/json')
|
|
147
162
|
res.end(JSON.stringify({ ok: false, error: 'origin 不在桥白名单' }))
|
|
148
163
|
return
|
|
149
164
|
}
|
|
150
165
|
// 心跳:扩展平时只走 /jobs 长轮询(健康只在启动探测时 ping 一次)——
|
|
151
|
-
//
|
|
166
|
+
// 一切携带白名单扩展 Origin 的请求都刷新 lastSeen,否则 45s 后误判扩展掉线;
|
|
152
167
|
// 无 Origin 的诊断 GET(/health /status)不记心跳,curl 探活不能伪造「扩展在线」
|
|
153
|
-
if (
|
|
168
|
+
if (originTrusted) {
|
|
154
169
|
lastSeenAt = now()
|
|
155
170
|
}
|
|
156
171
|
const finish = (code: number, body: unknown): void => {
|
|
@@ -169,6 +184,7 @@ export async function createSessionBridge(opts: SessionBridgeOptions = {}): Prom
|
|
|
169
184
|
lastSeenMsAgo: lastSeenAt > 0 ? now() - lastSeenAt : null,
|
|
170
185
|
queued: queue.length,
|
|
171
186
|
inFlight: inFlight.size,
|
|
187
|
+
parked: parked.length,
|
|
172
188
|
})
|
|
173
189
|
return
|
|
174
190
|
}
|
|
@@ -182,6 +198,9 @@ export async function createSessionBridge(opts: SessionBridgeOptions = {}): Prom
|
|
|
182
198
|
if (i >= 0) parked.splice(i, 1)
|
|
183
199
|
finish(200, { job: null })
|
|
184
200
|
}, JOBS_LONG_POLL_MS)
|
|
201
|
+
// 默认桥是惰性能力:外部扩展的 parked 轮询不得在主流程结束后钉住 CLI。
|
|
202
|
+
// keepBridge wizard 反过来要靠它守住进程,因此只对默认形态 unref。
|
|
203
|
+
if (!opts.keepBridge) parkTimer.unref()
|
|
185
204
|
parked.push({ res, timer: parkTimer })
|
|
186
205
|
res.on('close', () => {
|
|
187
206
|
const i = parked.findIndex((p) => p.res === res)
|
|
@@ -208,6 +227,10 @@ export async function createSessionBridge(opts: SessionBridgeOptions = {}): Prom
|
|
|
208
227
|
}
|
|
209
228
|
finish(404, { ok: false, error: 'unknown endpoint' })
|
|
210
229
|
})
|
|
230
|
+
server.on('connection', (socket) => {
|
|
231
|
+
// server.unref() 只解开监听器;已接受的扩展长轮询 socket 仍会引用事件循环。
|
|
232
|
+
if (!opts.keepBridge) socket.unref()
|
|
233
|
+
})
|
|
211
234
|
|
|
212
235
|
/** 扩展回包:queued 与 inFlight 两处都可寻址(job 执行失败 = result.ok:false,桥自身不吞) */
|
|
213
236
|
function resolveJob(jobId: string, parsed: ExtensionJobResult): void {
|
|
@@ -269,7 +292,7 @@ export async function createSessionBridge(opts: SessionBridgeOptions = {}): Prom
|
|
|
269
292
|
settle({
|
|
270
293
|
ok: false,
|
|
271
294
|
reason: 'extension-not-connected',
|
|
272
|
-
summary:
|
|
295
|
+
summary: `GoTry Session Bridge 扩展未连接(未安装或已停用)。一次性安装(装完零弹窗):推荐 Chrome 应用商店一键装(自动更新) ${EXTENSION_STORE_URL} ;或 npx gotry setup 落位后 chrome://extensions 开发者模式「加载已解压的扩展程序」指向 ~/.gotry/extension`,
|
|
273
296
|
})
|
|
274
297
|
}
|
|
275
298
|
}, 250)
|
|
@@ -351,4 +374,4 @@ export async function __resetSessionBridgeForTest(): Promise<void> {
|
|
|
351
374
|
/** 测试注入:以临时端口的桥替换单例(channel 层测试不碰真实端口池) */
|
|
352
375
|
export function __setSessionBridgeForTest(bridge: SessionJobHandle | null): void {
|
|
353
376
|
singleton = bridge ? { ok: true, bridge } : null
|
|
354
|
-
}
|
|
377
|
+
}
|
|
@@ -10,12 +10,15 @@
|
|
|
10
10
|
* 检索页无回包不是车道失败:与 CDP 车道同语义(到点无 hints 命中 ⇒ miss,页标题命挑战 ⇒ challenged)。
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { getOrCreateSessionBridge, type SessionJobHandle } from './extension-bridge.ts'
|
|
13
|
+
import { getOrCreateSessionBridge, EXTENSION_STORE_URL, type SessionJobHandle } from './extension-bridge.ts'
|
|
14
14
|
|
|
15
15
|
/** 一次性用户门文案(needs-extension 统一指引,与 bootstrap setupExtension 输出同口径) */
|
|
16
16
|
export const NEEDS_EXTENSION_HINT =
|
|
17
|
-
'需要一次性安装 GoTry Session Bridge 浏览器扩展(
|
|
18
|
-
+
|
|
17
|
+
'需要一次性安装 GoTry Session Bridge 浏览器扩展(装完零弹窗):'
|
|
18
|
+
+ `推荐 Chrome 应用商店一键安装(自动更新) ${EXTENSION_STORE_URL} ;`
|
|
19
|
+
+ '免审核、版本更新更快的 GitHub 通道:npx gotry setup --extension-from=github(自动下载落位 ~/.gotry/extension;'
|
|
20
|
+
+ '手动下载 github.com/Danceiny/gotry/releases 标签 ext-* 的 gotry-session-bridge.tar.gz),'
|
|
21
|
+
+ '再在 chrome://extensions 开发者模式「加载已解压的扩展程序」指向解压目录'
|
|
19
22
|
|
|
20
23
|
export type BridgeFailureKind = 'bridge-unavailable' | 'extension-not-connected' | 'timeout' | 'job-error'
|
|
21
24
|
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 扩展产物分发(issue #21 分发通道,ADR-21;founder 2026-08-30「产物下载和安装也得做成更好的
|
|
3
|
+
* 用户体验,可以用 github 作为分发渠道」)。
|
|
4
|
+
*
|
|
5
|
+
* 平台硬约束(诚实前置):Chrome 禁止普通用户从任意 URL 安装打包 CRX——GitHub 只能改善
|
|
6
|
+
* 「下载」(版本化/回滚/与 npm 发版节奏解耦),改善不了「安装」;「开发者模式加载已解压」
|
|
7
|
+
* 那 3 次点击只有上架 Chrome Web Store 才能消掉(商店版自动更新,见 extension-webstore-submission.md)。
|
|
8
|
+
* manifest 固定 key ⇒ 双通道是同一个扩展 ID(olpgkofjhhiiiahdkkbcninhjmegghfe),端口池/
|
|
9
|
+
* host_permissions 不随通道漂移——本模块用 key 钉住这一不变量。
|
|
10
|
+
*
|
|
11
|
+
* 单一职责:① 产物源解析(bundled 默认离线确定性 | github 显式 opt-in);
|
|
12
|
+
* ② GitHub Releases 下载链:dist-manifest → tar.gz → SHA256 校验 → tar 解压 → key 钉扎校验
|
|
13
|
+
* → 版本比较 → 原子交换 ~/.gotry/extension;
|
|
14
|
+
* ③ 失败语义:github 源任何失败(无网/404/校验不过/key 漂移)→ 结构化 fallback bundled,
|
|
15
|
+
* 绝不挡 setup(安装外部产物永远不挡 gotry 本体——bootstrap 契约)。
|
|
16
|
+
*
|
|
17
|
+
* 信任边界(诚实声明):TLS 传输 + 同源 dist-manifest 哈希完整性——防截断/损坏/误传资产,
|
|
18
|
+
* 不防仓库本身被攻陷(无独立 pinning;独立 pin 需把哈希烘进 npm 包,与「扩展更新与 npm
|
|
19
|
+
* 发版解耦」目标冲突,明确不做)。
|
|
20
|
+
*
|
|
21
|
+
* 零新 npm 依赖:fetch 走 Node ≥22 全局;解压走平台原生 `tar -xzf`(macOS/Linux/Windows10+
|
|
22
|
+
* 自带);SHA256 走 node:crypto。spawn/fetch 可注入(离线测试)。
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { spawn } from 'node:child_process'
|
|
26
|
+
import { createHash } from 'node:crypto'
|
|
27
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
28
|
+
import { join } from 'node:path'
|
|
29
|
+
|
|
30
|
+
/** Releases 稳定资产名(每版覆盖同名上传;latest/download 永久链只认稳定名)。
|
|
31
|
+
* 与 scripts/package-extension.mjs 的资产名必须逐字一致——extension-distribution-tests 防漂移。 */
|
|
32
|
+
export const DIST_ASSET_TARBALL = 'gotry-session-bridge.tar.gz'
|
|
33
|
+
export const DIST_ASSET_STORE_ZIP = 'gotry-session-bridge-store.zip'
|
|
34
|
+
export const DIST_ASSET_MANIFEST = 'extension-dist-manifest.json'
|
|
35
|
+
|
|
36
|
+
export const DEFAULT_RELEASE_BASE = 'https://github.com/Danceiny/gotry/releases'
|
|
37
|
+
|
|
38
|
+
export interface ExtensionDistManifest {
|
|
39
|
+
version: string
|
|
40
|
+
tarball: string
|
|
41
|
+
tarballSha256: string
|
|
42
|
+
zip: string
|
|
43
|
+
zipSha256: string
|
|
44
|
+
builtFromCommit?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface GithubSourceOptions {
|
|
48
|
+
/** Releases 基址(测试/镜像可指回环);默认 GitHub 官方 */
|
|
49
|
+
releaseBase?: string
|
|
50
|
+
/** 注入 fetch(离线测试);默认全局 fetch */
|
|
51
|
+
fetch?: typeof fetch
|
|
52
|
+
/** 注入 spawn(离线测试拦截 tar);默认 child_process.spawn */
|
|
53
|
+
spawnFn?: typeof spawn
|
|
54
|
+
/** 下载/解压超时(默认 60s,有界) */
|
|
55
|
+
timeoutMs?: number
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type ExtensionInstallAction = 'installed' | 'up-to-date' | 'fallback-bundled'
|
|
59
|
+
|
|
60
|
+
export interface ExtensionInstallResult {
|
|
61
|
+
ok: boolean
|
|
62
|
+
action: ExtensionInstallAction
|
|
63
|
+
/** 远端 dist-manifest 声明的版本(action=up-to-date 时也带回,供比较报告) */
|
|
64
|
+
version?: string
|
|
65
|
+
/** 交换前 dest 里已就位的版本(无则 undefined) */
|
|
66
|
+
previousVersion?: string
|
|
67
|
+
error?: string
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 纯函数:三段数字版本比较(a<b 负 / 相等 0 / a>b 正;非数字段按 0 处理,长度差补 0) */
|
|
71
|
+
export function compareVersions(a: string, b: string): number {
|
|
72
|
+
const pa = String(a).split('.').map((s) => Number.parseInt(s, 10) || 0)
|
|
73
|
+
const pb = String(b).split('.').map((s) => Number.parseInt(s, 10) || 0)
|
|
74
|
+
const n = Math.max(pa.length, pb.length)
|
|
75
|
+
for (let i = 0; i < n; i += 1) {
|
|
76
|
+
const d = (pa[i] ?? 0) - (pb[i] ?? 0)
|
|
77
|
+
if (d !== 0) return d
|
|
78
|
+
}
|
|
79
|
+
return 0
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 纯函数:Releases latest/download 永久链(稳定资产名) */
|
|
83
|
+
export function distAssetUrls(releaseBase: string = DEFAULT_RELEASE_BASE): { tarball: string; storeZip: string; manifest: string } {
|
|
84
|
+
const base = releaseBase.replace(/\/+$/, '')
|
|
85
|
+
return {
|
|
86
|
+
tarball: `${base}/latest/download/${DIST_ASSET_TARBALL}`,
|
|
87
|
+
storeZip: `${base}/latest/download/${DIST_ASSET_STORE_ZIP}`,
|
|
88
|
+
manifest: `${base}/latest/download/${DIST_ASSET_MANIFEST}`,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const SHA256_RE = /^[0-9a-f]{64}$/
|
|
93
|
+
|
|
94
|
+
/** 纯函数:dist-manifest 解析,fail-closed(坏 JSON/缺字段/资产名漂移/哈希非 64-hex 一律拒绝) */
|
|
95
|
+
export function parseDistManifest(raw: string): ExtensionDistManifest {
|
|
96
|
+
let obj: Record<string, unknown>
|
|
97
|
+
try {
|
|
98
|
+
obj = JSON.parse(raw) as Record<string, unknown>
|
|
99
|
+
} catch {
|
|
100
|
+
throw new Error('dist-manifest 不是合法 JSON')
|
|
101
|
+
}
|
|
102
|
+
const version = obj.version
|
|
103
|
+
const tarball = obj.tarball
|
|
104
|
+
const tarballSha256 = obj.tarballSha256
|
|
105
|
+
const zip = obj.zip
|
|
106
|
+
const zipSha256 = obj.zipSha256
|
|
107
|
+
if (typeof version !== 'string' || !/^\d+\.\d+\.\d+$/.test(version)) throw new Error('dist-manifest.version 缺失或非 x.y.z')
|
|
108
|
+
if (tarball !== DIST_ASSET_TARBALL) throw new Error(`dist-manifest.tarball 资产名漂移(期望 ${DIST_ASSET_TARBALL})`)
|
|
109
|
+
if (zip !== DIST_ASSET_STORE_ZIP) throw new Error(`dist-manifest.zip 资产名漂移(期望 ${DIST_ASSET_STORE_ZIP})`)
|
|
110
|
+
if (typeof tarballSha256 !== 'string' || !SHA256_RE.test(tarballSha256)) throw new Error('dist-manifest.tarballSha256 非 64-hex')
|
|
111
|
+
if (typeof zipSha256 !== 'string' || !SHA256_RE.test(zipSha256)) throw new Error('dist-manifest.zipSha256 非 64-hex')
|
|
112
|
+
return {
|
|
113
|
+
version,
|
|
114
|
+
tarball,
|
|
115
|
+
tarballSha256,
|
|
116
|
+
zip,
|
|
117
|
+
zipSha256,
|
|
118
|
+
builtFromCommit: typeof obj.builtFromCommit === 'string' ? obj.builtFromCommit : undefined,
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function sha256Hex(bytes: Uint8Array | string): string {
|
|
123
|
+
return createHash('sha256').update(bytes).digest('hex')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** 读取扩展 manifest 的 key(固定 key ⇒ 固定扩展 ID;分发通道 ID 不变量的锚点) */
|
|
127
|
+
export function readExtensionKey(extensionDir: string): string | undefined {
|
|
128
|
+
const p = join(extensionDir, 'manifest.json')
|
|
129
|
+
if (!existsSync(p)) return undefined
|
|
130
|
+
try {
|
|
131
|
+
const m = JSON.parse(readFileSync(p, 'utf8')) as { key?: unknown }
|
|
132
|
+
return typeof m.key === 'string' ? m.key : undefined
|
|
133
|
+
} catch {
|
|
134
|
+
return undefined
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function readExtensionVersion(extensionDir: string): string | undefined {
|
|
139
|
+
const p = join(extensionDir, 'manifest.json')
|
|
140
|
+
if (!existsSync(p)) return undefined
|
|
141
|
+
try {
|
|
142
|
+
const m = JSON.parse(readFileSync(p, 'utf8')) as { version?: unknown }
|
|
143
|
+
return typeof m.version === 'string' ? m.version : undefined
|
|
144
|
+
} catch {
|
|
145
|
+
return undefined
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function runTar(args: string[], spawnFn: typeof spawn, timeoutMs: number): Promise<{ ok: boolean; error?: string }> {
|
|
150
|
+
return new Promise((resolve) => {
|
|
151
|
+
const child = spawnFn('tar', args, { stdio: 'ignore' })
|
|
152
|
+
let done = false
|
|
153
|
+
const timer = setTimeout(() => {
|
|
154
|
+
if (!done) {
|
|
155
|
+
done = true
|
|
156
|
+
try {
|
|
157
|
+
child.kill('SIGKILL')
|
|
158
|
+
} catch {
|
|
159
|
+
/* ignore */
|
|
160
|
+
}
|
|
161
|
+
resolve({ ok: false, error: `tar 超时(>${timeoutMs}ms)` })
|
|
162
|
+
}
|
|
163
|
+
}, timeoutMs)
|
|
164
|
+
child.on('error', (e) => {
|
|
165
|
+
if (!done) {
|
|
166
|
+
done = true
|
|
167
|
+
clearTimeout(timer)
|
|
168
|
+
resolve({ ok: false, error: `tar 不可用:${e.message}` })
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
child.on('close', (code) => {
|
|
172
|
+
if (!done) {
|
|
173
|
+
done = true
|
|
174
|
+
clearTimeout(timer)
|
|
175
|
+
resolve(code === 0 ? { ok: true } : { ok: false, error: `tar exit ${code}` })
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function fetchBytes(url: string, fetchFn: typeof fetch, timeoutMs: number): Promise<{ ok: true; bytes: Uint8Array } | { ok: false; error: string }> {
|
|
182
|
+
try {
|
|
183
|
+
const res = await fetchFn(url, { signal: AbortSignal.timeout(timeoutMs) })
|
|
184
|
+
if (!res.ok) return { ok: false, error: `HTTP ${res.status} ${url}` }
|
|
185
|
+
const buf = new Uint8Array(await res.arrayBuffer())
|
|
186
|
+
return { ok: true, bytes: buf }
|
|
187
|
+
} catch (e) {
|
|
188
|
+
return { ok: false, error: `下载失败 ${(e as Error).message}` }
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface InstallFromGithubOptions extends GithubSourceOptions {
|
|
193
|
+
/** 落位目录(默认 ~/.gotry/extension 由调用方决定,本层不猜) */
|
|
194
|
+
destDir: string
|
|
195
|
+
/** 钉扎参照:bundled extension/ 目录(其 manifest key 是 ID 不变量的基准;缺省只做版本校验) */
|
|
196
|
+
pinnedSourceDir?: string
|
|
197
|
+
/** 只报告不落盘(up-to-date/将安装哪个版本;不下载 tarball 之外的任何东西) */
|
|
198
|
+
checkOnly?: boolean
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* GitHub Releases → ~/.gotry/extension 安装链。
|
|
203
|
+
* 任何失败都返回 { ok:false, action:'fallback-bundled' }——调用方显式降级 bundled,
|
|
204
|
+
* 本函数不抛出(bootstrap 契约:安装外部产物永远不挡 gotry 本体)。
|
|
205
|
+
*/
|
|
206
|
+
export async function installExtensionFromGithub(opts: InstallFromGithubOptions): Promise<ExtensionInstallResult> {
|
|
207
|
+
const timeoutMs = opts.timeoutMs ?? 60_000
|
|
208
|
+
const fetchFn = opts.fetch ?? fetch
|
|
209
|
+
const spawnFn = opts.spawnFn ?? spawn
|
|
210
|
+
const urls = distAssetUrls(opts.releaseBase)
|
|
211
|
+
const fail = (error: string): ExtensionInstallResult => ({ ok: false, action: 'fallback-bundled', error })
|
|
212
|
+
|
|
213
|
+
const mRes = await fetchBytes(urls.manifest, fetchFn, timeoutMs)
|
|
214
|
+
if (!mRes.ok) return fail(mRes.error)
|
|
215
|
+
let manifest: ExtensionDistManifest
|
|
216
|
+
try {
|
|
217
|
+
manifest = parseDistManifest(new TextDecoder().decode(mRes.bytes))
|
|
218
|
+
} catch (e) {
|
|
219
|
+
return fail(`dist-manifest 校验失败:${(e as Error).message}`)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const previousVersion = readExtensionVersion(opts.destDir)
|
|
223
|
+
if (previousVersion != null && compareVersions(manifest.version, previousVersion) <= 0) {
|
|
224
|
+
return { ok: true, action: 'up-to-date', version: manifest.version, previousVersion }
|
|
225
|
+
}
|
|
226
|
+
if (opts.checkOnly) {
|
|
227
|
+
return { ok: true, action: 'up-to-date', version: manifest.version, previousVersion } // check-only 语义:报告远端版本,不落盘
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const tRes = await fetchBytes(urls.tarball, fetchFn, timeoutMs)
|
|
231
|
+
if (!tRes.ok) return fail(tRes.error)
|
|
232
|
+
const actual = sha256Hex(tRes.bytes)
|
|
233
|
+
if (actual !== manifest.tarballSha256) return fail(`SHA256 不符(期望 ${manifest.tarballSha256.slice(0, 12)}… 实得 ${actual.slice(0, 12)}…)`)
|
|
234
|
+
|
|
235
|
+
// 落盘 → tar 解压 → key 钉扎 → 原子交换;incoming 目录同盘保证 rename 原子性
|
|
236
|
+
const incoming = `${opts.destDir}.incoming-${process.pid}-${Date.now()}`
|
|
237
|
+
const tarPath = `${incoming}.tar.gz`
|
|
238
|
+
try {
|
|
239
|
+
rmSync(incoming, { recursive: true, force: true })
|
|
240
|
+
mkdirSync(incoming, { recursive: true })
|
|
241
|
+
writeFileSync(tarPath, tRes.bytes)
|
|
242
|
+
// 包内含顶层目录 gotry-session-bridge/;--strip-components 落到 incoming 根
|
|
243
|
+
const ext = await runTar(['-xzf', tarPath, '-C', incoming, '--strip-components', '1'], spawnFn, timeoutMs)
|
|
244
|
+
if (!ext.ok) return fail(ext.error!)
|
|
245
|
+
|
|
246
|
+
const incomingKey = readExtensionKey(incoming)
|
|
247
|
+
if (incomingKey == null) return fail('解压产物缺 manifest.json/key(非法包)')
|
|
248
|
+
if (opts.pinnedSourceDir) {
|
|
249
|
+
const pinnedKey = readExtensionKey(opts.pinnedSourceDir)
|
|
250
|
+
if (pinnedKey != null && incomingKey !== pinnedKey) return fail('扩展 key 漂移(通道产物与 bundled 不同 ID——拒绝安装,防端口池/host 权限漂移)')
|
|
251
|
+
}
|
|
252
|
+
const incomingVersion = readExtensionVersion(incoming)
|
|
253
|
+
if (incomingVersion !== manifest.version) return fail(`解压产物版本(${incomingVersion})与 dist-manifest(${manifest.version})不符`)
|
|
254
|
+
|
|
255
|
+
rmSync(opts.destDir, { recursive: true, force: true })
|
|
256
|
+
cpSync(incoming, opts.destDir, { recursive: true })
|
|
257
|
+
return { ok: true, action: 'installed', version: manifest.version, previousVersion }
|
|
258
|
+
} catch (e) {
|
|
259
|
+
return fail(`落位失败 ${(e as Error).message}`)
|
|
260
|
+
} finally {
|
|
261
|
+
rmSync(incoming, { recursive: true, force: true })
|
|
262
|
+
rmSync(tarPath, { force: true })
|
|
263
|
+
}
|
|
264
|
+
}
|