@danceiny/gotry 0.0.1-rc.21 → 0.0.1-rc.24
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 +102 -194
- package/README.zh-CN.md +115 -210
- package/bin/gotry-backend.js +53 -0
- package/bin/gotry-bootstrap.js +466 -35
- package/bin/gotry-inner.js +255 -36
- package/bin/gotry-process-liveness.d.ts +27 -0
- package/bin/gotry-process-liveness.js +97 -0
- package/bin/gotry-runtime-resolution.d.ts +1 -1
- package/bin/gotry-runtime-resolution.js +2 -2
- package/client/client.js +174 -0
- package/cordis.gotry-patch.yml +70 -93
- package/dist/capabilities/agent-reach-deep.js +45 -122
- package/dist/capabilities/agent-reach.js +63 -148
- package/dist/capabilities/anything.js +89 -75
- package/dist/capabilities/artifacts.js +123 -104
- package/dist/capabilities/channel-health.js +65 -45
- package/dist/capabilities/channel-registry.js +108 -87
- package/dist/capabilities/doctor.js +357 -235
- package/dist/capabilities/effect.js +223 -309
- package/dist/capabilities/fact-log.js +28 -14
- package/dist/capabilities/flyai.js +245 -156
- package/dist/capabilities/ground-transfer.js +509 -0
- package/dist/capabilities/hbcli.js +144 -148
- package/dist/capabilities/hotelbyte-transaction.js +194 -0
- package/dist/capabilities/incident-log.js +131 -61
- package/dist/capabilities/model-override.js +30 -10
- package/dist/capabilities/opensky.js +54 -43
- package/dist/capabilities/resilience.js +50 -32
- package/dist/capabilities/session/action-cache.js +50 -44
- package/dist/capabilities/session/adapters/ctrip-flight.js +119 -62
- package/dist/capabilities/session/adapters/ctrip-hotel.js +125 -110
- package/dist/capabilities/session/adapters/dida-portal.js +206 -0
- package/dist/capabilities/session/adapters/meituan-local.js +35 -54
- package/dist/capabilities/session/adapters/rail-12306.js +169 -112
- package/dist/capabilities/session/benchmark.js +85 -149
- package/dist/capabilities/session/extension-bridge.js +320 -259
- package/dist/capabilities/session/extension-channel.js +57 -76
- package/dist/capabilities/session/extension-distribution.js +115 -119
- package/dist/capabilities/session/extract.js +31 -21
- package/dist/capabilities/session/golden-score.js +55 -33
- package/dist/capabilities/session/health-watch.js +146 -154
- package/dist/capabilities/session/read-guard.js +55 -47
- package/dist/capabilities/session/static-flight-golden.js +49 -36
- package/dist/capabilities/session/transport.js +54 -61
- package/dist/capabilities/session/wizard.js +63 -93
- package/dist/capabilities/session-consent.js +143 -47
- package/dist/capabilities/session-login.js +95 -126
- package/dist/capabilities/session-search.js +391 -253
- package/dist/capabilities/sponsor-plugin.js +341 -0
- package/dist/capabilities/visa-policy.js +87 -73
- package/dist/capabilities/weather.js +163 -235
- package/dist/scripts/action-cache-tests.js +24 -59
- package/dist/scripts/agent-planning-turn-deadline-e2e.js +91 -142
- package/dist/scripts/agent-planning-turn-deadline-tests.js +80 -176
- package/dist/scripts/agent-reach-deep-tests.js +30 -40
- package/dist/scripts/agent-reach-tests.js +26 -18
- package/dist/scripts/agent-reach-wrapper-tests.js +46 -50
- package/dist/scripts/anything-tests.js +58 -62
- package/dist/scripts/artifact-client-contract-tests.js +69 -0
- package/dist/scripts/artifacts-capability-tests.js +235 -0
- package/dist/scripts/async-collect.js +26 -15
- package/dist/scripts/benchmark-environment-bridge-e2e.js +287 -684
- package/dist/scripts/benchmark-environment-bridge-tests.js +818 -3123
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +66 -279
- package/dist/scripts/booking-copilot-availability-policy-tests.js +153 -1257
- package/dist/scripts/booking-copilot-bin-proof-tests.js +22 -45
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +64 -76
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +69 -199
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +393 -759
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +33 -75
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +60 -110
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +28 -66
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +72 -271
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +73 -275
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +784 -3398
- package/dist/scripts/booking-copilot-server-proof-tests.js +63 -168
- package/dist/scripts/booking-copilot-startup-proof-tests.js +38 -124
- 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 +61 -123
- package/dist/scripts/booking-surface-contract-proof-tests.js +152 -1167
- package/dist/scripts/bootstrap-tests.js +1383 -128
- package/dist/scripts/build-changelog.js +89 -130
- package/dist/scripts/build-metrics-report.js +135 -143
- package/dist/scripts/changelog-tests.js +35 -82
- package/dist/scripts/channel-probe-tests.js +44 -116
- package/dist/scripts/channel-probe.js +84 -167
- package/dist/scripts/channel-registry-tests.js +97 -185
- package/dist/scripts/companion-tests.js +25 -70
- package/dist/scripts/dida-cookie-probe.js +87 -0
- package/dist/scripts/dida-sw-probe.js +114 -0
- package/dist/scripts/diff-test.js +17 -12
- package/dist/scripts/doctor-tests.js +428 -120
- package/dist/scripts/dsh-artifact-e2e.js +150 -0
- package/dist/scripts/dsh-artifact-web-e2e.js +443 -0
- package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
- package/dist/scripts/dsh-runtime-closure-tests.js +110 -153
- package/dist/scripts/dsh-runtime-closure.js +75 -63
- package/dist/scripts/dsh-session-v3-migration-proof.js +106 -0
- package/dist/scripts/dsh-subprocess-local-proof.js +149 -0
- package/dist/scripts/dsh-target-closure-proof.js +178 -0
- package/dist/scripts/effect-tests.js +165 -552
- package/dist/scripts/engine-run.js +1 -3
- package/dist/scripts/engine-tests.js +16 -10
- package/dist/scripts/evaluation-cadence-tests.js +69 -242
- package/dist/scripts/evaluation-contract-tests.js +193 -499
- package/dist/scripts/extension-distribution-cli.js +19 -15
- package/dist/scripts/extension-distribution-tests.js +98 -211
- package/dist/scripts/extension-tests.js +366 -389
- package/dist/scripts/fact-gate-tests.js +725 -263
- 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-tests.js +325 -68
- 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/gotry-backend-kernel-tests.js +79 -0
- package/dist/scripts/ground-transfer-tests.js +956 -0
- package/dist/scripts/hbcli-e2e-tests.js +71 -90
- 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 +41 -115
- package/dist/scripts/health-watch-cli.js +42 -28
- 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 +16 -9
- 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 +547 -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/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/ledger-repair-apply-tests.js +316 -0
- package/dist/scripts/ledger-repair-plan-tests.js +375 -0
- package/dist/scripts/ledger-tests.js +325 -340
- package/dist/scripts/ledger-workflow-crash.js +10 -20
- package/dist/scripts/map-tools-vendor-package-proof.js +206 -134
- package/dist/scripts/memory-capture-tests.js +242 -61
- package/dist/scripts/memory-decay-tests.js +18 -52
- package/dist/scripts/memory-lifecycle-tests.js +657 -0
- package/dist/scripts/memory-lifecycle.js +196 -0
- package/dist/scripts/memory-metrics.js +24 -12
- package/dist/scripts/memory-value-report.js +474 -222
- package/dist/scripts/memory-value-tests.js +328 -0
- package/dist/scripts/metrics-report-tests.js +62 -199
- package/dist/scripts/model-override-e2e.js +66 -105
- package/dist/scripts/nightly-evidence-tests.js +64 -89
- package/dist/scripts/nightly-evidence.js +108 -111
- package/dist/scripts/nudge-digest.js +39 -28
- package/dist/scripts/onboarding-tests.js +63 -88
- package/dist/scripts/opensky-check.js +18 -13
- package/dist/scripts/opensky-tests.js +28 -28
- package/dist/scripts/persona-surface-guard-tests.js +37 -12
- package/dist/scripts/pnpm-dsh-closure-proof.js +2 -5
- package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
- package/dist/scripts/price-drift-tests.js +126 -227
- package/dist/scripts/price-drift-watch.js +209 -253
- package/dist/scripts/probe-poi-tests.js +52 -45
- package/dist/scripts/product-metrics.js +187 -273
- package/dist/scripts/publish-preverify.js +47 -41
- package/dist/scripts/realtime-pricing-tests.js +37 -76
- package/dist/scripts/replay-async.js +20 -18
- package/dist/scripts/replay-real.js +16 -18
- package/dist/scripts/replay.js +52 -82
- package/dist/scripts/session-attach-diagnose.js +16 -12
- package/dist/scripts/session-attach-poc.js +34 -37
- package/dist/scripts/session-benchmark.js +66 -147
- package/dist/scripts/session-dida-live-e2e.js +151 -0
- package/dist/scripts/session-extract-tests.js +14 -35
- package/dist/scripts/session-login.js +14 -13
- package/dist/scripts/session-tests.js +545 -583
- package/dist/scripts/sf-live-benchmark.js +92 -110
- package/dist/scripts/sf-live-cli-tests.js +2 -9
- package/dist/scripts/sf-soft-score-tests.js +19 -60
- package/dist/scripts/sf-summary-tests.js +149 -0
- package/dist/scripts/sf-summary.js +466 -84
- package/dist/scripts/skeleton-check.js +16 -14
- package/dist/scripts/skeleton-integration-test.js +11 -10
- package/dist/scripts/skills-contract-tests.js +51 -46
- package/dist/scripts/smoke-session-gate-tests.js +12 -20
- package/dist/scripts/smoke.js +541 -422
- package/dist/scripts/sponsor-reuse-tests.js +262 -0
- package/dist/scripts/state-cli-tests.js +226 -101
- package/dist/scripts/state-cli.js +549 -212
- package/dist/scripts/static-golden-tests.js +46 -180
- package/dist/scripts/time-eval-tests.js +210 -192
- package/dist/scripts/travel-timeline-tests.js +29 -93
- package/dist/scripts/turn-handoff-collect-tests.js +46 -93
- package/dist/scripts/turn-handoff-collect.js +71 -99
- package/dist/scripts/turn-policy-tests.js +28 -29
- package/dist/scripts/typed-contract-canary.js +77 -162
- 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 +31 -44
- package/dist/scripts/weather-tests.js +121 -543
- package/dist/scripts/wish-channel-gate-tests.js +29 -86
- 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 +18 -12
- package/dist/src/artifact-gate.js +652 -221
- package/dist/src/backend/kernel.js +91 -0
- package/dist/src/backend/modules/booking-copilot.js +55 -0
- package/dist/src/backend/modules/booking-executor.js +124 -0
- package/dist/src/backend/modules/session-search.js +194 -0
- package/dist/src/benchmark-agent-conformance.js +296 -262
- package/dist/src/benchmark-environment-bridge.js +298 -413
- package/dist/src/benchmark-headless-child-diagnostics.js +93 -100
- package/dist/src/benchmark-tool-isolation.js +94 -67
- package/dist/src/bookable-facts.js +299 -174
- package/dist/src/booking-saga.js +64 -120
- package/dist/src/booking-surface/availability-policy.js +443 -576
- package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
- package/dist/src/booking-surface/contracts.js +24 -80
- package/dist/src/booking-surface/dsh-planner.js +510 -281
- package/dist/src/booking-surface/dsh-plugin.js +17 -1
- package/dist/src/booking-surface/error-codes.js +82 -10
- package/dist/src/booking-surface/index.js +0 -3
- package/dist/src/booking-surface/profile.js +10 -54
- package/dist/src/booking-surface/recovery-chain.js +367 -0
- package/dist/src/booking-surface/runtime.js +765 -891
- package/dist/src/booking-surface/server.js +200 -251
- package/dist/src/booking-surface/startup.js +58 -54
- package/dist/src/booking-surface/validation.js +150 -218
- package/dist/src/bridge.js +20 -13
- package/dist/src/companions.js +48 -74
- package/dist/src/contracts.js +21 -43
- package/dist/src/dsh-llm.js +82 -120
- package/dist/src/engine.js +253 -239
- package/dist/src/evaluation-cadence.js +72 -209
- package/dist/src/evaluation-contracts.js +220 -843
- package/dist/src/flight-pack-adapter.js +54 -0
- package/dist/src/flight-pack-contract.js +2 -0
- package/dist/src/gotry-backend.js +70 -0
- package/dist/src/hotel-date-gate.js +118 -0
- package/dist/src/i18n.js +18 -7
- package/dist/src/index.js +1425 -1629
- package/dist/src/journey.js +132 -81
- package/dist/src/ledger-repair-apply.js +538 -0
- package/dist/src/ledger-repair-plan.js +463 -0
- package/dist/src/loop.js +257 -166
- package/dist/src/memory-capture.js +180 -32
- package/dist/src/memory-decay.js +30 -14
- package/dist/src/memory-lifecycle.js +1310 -0
- package/dist/src/memory-utility.js +27 -40
- package/dist/src/mock-llm.js +36 -61
- package/dist/src/model.js +81 -36
- package/dist/src/realtime-pricing.js +63 -72
- package/dist/src/slot-spec.js +84 -106
- package/dist/src/state-ledger.js +310 -471
- package/dist/src/subagent-job-id-guard.js +58 -0
- package/dist/src/time-anchor.js +231 -33
- package/dist/src/tool-packet.js +20 -8
- package/dist/src/travel-slots.js +58 -35
- package/dist/src/travel-timeline.js +44 -63
- package/dist/src/turn-deadline.js +121 -127
- package/dist/src/turn-policy.js +61 -89
- package/dist/src/tz-resolver.js +143 -0
- package/dist/src/unified.js +504 -381
- package/dist/src/wish-pool.js +39 -14
- package/dist/src/write-gate.js +495 -0
- package/dist/src/z3-shared.js +273 -7
- package/extension/README.md +4 -3
- package/extension/background.js +125 -14
- package/extension/content-bridge.js +23 -1
- package/extension/content-main.js +15 -4
- package/extension/manifest.json +16 -8
- package/package.json +251 -220
- package/ts/capabilities/agent-reach.ts +4 -4
- package/ts/capabilities/anything.ts +1 -1
- package/ts/capabilities/artifacts.ts +46 -9
- package/ts/capabilities/flyai.ts +156 -37
- package/ts/capabilities/hbcli.ts +1 -1
- package/ts/capabilities/incident-log.ts +88 -59
- package/ts/capabilities/session/adapters/ctrip-flight.ts +99 -36
- package/ts/capabilities/session/adapters/dida-portal.ts +250 -0
- package/ts/capabilities/session/adapters/rail-12306.ts +123 -24
- package/ts/capabilities/session/extension-bridge.ts +226 -101
- package/ts/capabilities/session/extension-channel.ts +22 -9
- package/ts/capabilities/session/wizard.ts +2 -2
- package/ts/capabilities/session-consent.ts +98 -14
- package/ts/capabilities/session-login.ts +1 -0
- package/ts/capabilities/session-search.ts +315 -31
- package/ts/dsh-runtime/vendor/README.md +8 -4
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +7 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +5 -3
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +1 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +1 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +6 -6
- package/ts/package.json +236 -16
- package/ts/scripts/map-tools-vendor-package-proof.ts +152 -31
- package/ts/scripts/state-cli.ts +418 -51
- package/ts/src/artifact-gate.ts +561 -15
- package/ts/src/benchmark-environment-bridge.ts +37 -20
- package/ts/src/bookable-facts.ts +183 -7
- package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
- package/ts/src/booking-surface/dsh-planner.ts +240 -32
- package/ts/src/booking-surface/dsh-plugin.js +17 -1
- package/ts/src/booking-surface/error-codes.ts +75 -0
- package/ts/src/booking-surface/recovery-chain.ts +409 -0
- package/ts/src/booking-surface/server.ts +84 -51
- package/ts/src/booking-surface/validation.ts +2 -0
- package/ts/src/contracts.ts +12 -2
- package/ts/src/dsh-llm.ts +4 -8
- package/ts/src/index.ts +609 -90
- package/ts/src/loop.ts +116 -7
- package/ts/src/memory-capture.ts +227 -3
- package/ts/src/mock-llm.ts +4 -8
- package/ts/src/model.ts +88 -2
- package/ts/src/state-ledger.ts +47 -29
- package/ts/src/subagent-job-id-guard.ts +72 -0
- package/ts/src/time-anchor.ts +217 -13
- package/ts/src/unified.ts +316 -54
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Booking Copilot unavailable/changed 恢复链契约层(issue #142 非门控切片)。
|
|
3
|
+
*
|
|
4
|
+
* ⚠ 非运行时激活:纯函数契约层,不发起供应商调用、不读凭据、不接线 runtime seam——
|
|
5
|
+
* #142 合并 gate 中「四 surface 真实库存」与「真实 Checkout/QueryOrders 证据」仍是
|
|
6
|
+
* no-spend 门控项(等真实供应商/UAT)。本层只把恢复链的**结构化处置**固化为可离线
|
|
7
|
+
* 证明的契约:检测(识别分类)→ 恢复(受控重试)→ 用户沟通(显式披露)→ 审计(可对账)。
|
|
8
|
+
* 检测/恢复的合法性判定仍归 availability reducer(ADR-23 availability-policy.ts)所有;
|
|
9
|
+
* 本层是它的用户沟通与审计投影,不替代、不绕过。
|
|
10
|
+
*
|
|
11
|
+
* 词汇复用(不发明新通道):
|
|
12
|
+
* - 分类闭集复用 ActionReceipt.status 闭集(changed/unavailable/no_match/applied/partial/
|
|
13
|
+
* stale/failed/needs_input/unsupported,contracts.ts)与 offer.availability/gap 观测形态;
|
|
14
|
+
* - 每酒店有界恢复预算复用 availability-policy 的 MAX_OFFER_QUERIES_PER_HOTEL /
|
|
15
|
+
* MAX_OFFER_CHECKS_PER_HOTEL(RECOVERY_REUSED_BUDGETS 冻结引用,防两处漂移);
|
|
16
|
+
* - 恢复轮次的模型调用语义对齐 planner 三次调用预算(dsh-planner attempt < 3):
|
|
17
|
+
* 每个触发源最多 3 次计入预算的恢复轮,第 4 次结构性拒绝、必须降级;
|
|
18
|
+
* - 审计事件命名沿用账本 booking.copilot.* 命名空间(runtime.ts 事件 kind 惯例);
|
|
19
|
+
* 链校验形态对齐 booking-saga 的 sagaTraceViolations(开立→推进→终态吸收)。
|
|
20
|
+
*
|
|
21
|
+
* 红线(全部结构性编码):
|
|
22
|
+
* - changed/unavailable 必须显式分类——不静默当确认,也不静默丢弃;
|
|
23
|
+
* - 不可确认证据只产生 inconclusive,不得宣称「市场无房」(与 availability reducer 同红线);
|
|
24
|
+
* - 恢复命中与用户原选择不同酒店/报价 = 替换,必须先出显式披露——静默换房换航结构性拒绝;
|
|
25
|
+
* - 恢复耗尽只能显式降级并向用户披露,不得静默替换或假称确认;
|
|
26
|
+
* - 版本位移(v1→v2)的解决必须携带非空变化说明——不静默换版;
|
|
27
|
+
* - 每条审计事件带 receiptDigest 可回溯;resolved/degraded 为吸收终态,其后不得再有事件;
|
|
28
|
+
* - 纯函数:每次推进返回新状态,原状态不可变。
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { createHash } from 'node:crypto'
|
|
32
|
+
import type { ActionReceipt } from './contracts.ts'
|
|
33
|
+
import { MAX_OFFER_CHECKS_PER_HOTEL, MAX_OFFER_QUERIES_PER_HOTEL } from './availability-policy.ts'
|
|
34
|
+
|
|
35
|
+
export const RECOVERY_CHAIN_SCHEMA = 'booking_recovery_chain.v1'
|
|
36
|
+
|
|
37
|
+
/** 每个触发源最多 3 次计入预算的恢复轮——对齐 planner 三次调用预算的语义(dsh-planner attempt < 3)。 */
|
|
38
|
+
export const MAX_RECOVERY_MODEL_TURNS_PER_TRIGGER = 3
|
|
39
|
+
export const MAX_RECOVERY_BUDGET_NOTE =
|
|
40
|
+
'3 counted model turns per trigger, aligned with the planner three-call attempt budget (dsh-planner); ' +
|
|
41
|
+
'per-hotel query/check budgets stay policy-owned (availability-policy MAX_OFFER_QUERIES_PER_HOTEL / MAX_OFFER_CHECKS_PER_HOTEL)'
|
|
42
|
+
|
|
43
|
+
/** 恢复重试计入的既有预算(冻结引用 availability-policy 常量,防两处漂移)。 */
|
|
44
|
+
export const RECOVERY_REUSED_BUDGETS = {
|
|
45
|
+
offerQueriesPerHotel: MAX_OFFER_QUERIES_PER_HOTEL,
|
|
46
|
+
offerChecksPerHotel: MAX_OFFER_CHECKS_PER_HOTEL,
|
|
47
|
+
modelTurnsPerTrigger: MAX_RECOVERY_MODEL_TURNS_PER_TRIGGER,
|
|
48
|
+
} as const
|
|
49
|
+
|
|
50
|
+
/** 检测分类(闭集;与 ActionReceipt.status 词汇一一对应,不发明新通道)。 */
|
|
51
|
+
export type RecoveryClassification =
|
|
52
|
+
| { kind: 'confirmed'; offerRef: string; offerVersionRef: string; verifiedOfferRef: string }
|
|
53
|
+
| { kind: 'changed'; offerRef: string; checkedOfferVersionRef: string; currentOfferVersionRef?: string; changedFactRefs: string[]; reason: string }
|
|
54
|
+
| { kind: 'unavailable'; offerRef: string; offerVersionRef: string; reason: string }
|
|
55
|
+
| { kind: 'inconclusive'; reason: string }
|
|
56
|
+
| { kind: 'unrelated'; observationKind: ActionReceipt['observation']['kind']; reason: string }
|
|
57
|
+
|
|
58
|
+
export type RecoveryChainPhase = 'detected' | 'recovering' | 'resolved' | 'degraded'
|
|
59
|
+
|
|
60
|
+
/** 受控恢复的路径闭集:换版重查 / 同酒店重查 / 替代候选——恢复不开新的无界重试通道。 */
|
|
61
|
+
export type RecoveryApproach = 'recheck_version' | 'requery_same_hotel' | 'alternative_candidate'
|
|
62
|
+
const RECOVERY_APPROACHES: ReadonlySet<string> = new Set(['recheck_version', 'requery_same_hotel', 'alternative_candidate'])
|
|
63
|
+
|
|
64
|
+
export interface RecoverySelectionRef { hotelRef: string; offerRef: string; offerVersionRef: string }
|
|
65
|
+
|
|
66
|
+
export type RecoveryAuditEventKind =
|
|
67
|
+
| 'recovery.detected'
|
|
68
|
+
| 'recovery.attempt'
|
|
69
|
+
| 'recovery.disclosed'
|
|
70
|
+
| 'recovery.resolved'
|
|
71
|
+
| 'recovery.degraded'
|
|
72
|
+
|
|
73
|
+
/** 审计事件(账本 booking.copilot.* 命名空间惯例;receiptDigest 证据链,不落敏感原值)。 */
|
|
74
|
+
export interface RecoveryAuditEvent {
|
|
75
|
+
at: string
|
|
76
|
+
kind: RecoveryAuditEventKind
|
|
77
|
+
from: RecoveryChainPhase
|
|
78
|
+
to: RecoveryChainPhase
|
|
79
|
+
reason: string
|
|
80
|
+
/** sha256(分类输入 receipt 的规范 JSON)——可回溯原始凭证 */
|
|
81
|
+
receiptDigest?: string
|
|
82
|
+
target?: RecoverySelectionRef
|
|
83
|
+
substitution?: boolean
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface RecoveryChainState {
|
|
87
|
+
readonly schemaVersion: typeof RECOVERY_CHAIN_SCHEMA
|
|
88
|
+
readonly taskId: string
|
|
89
|
+
readonly contextRef: string
|
|
90
|
+
readonly trigger: 'changed' | 'unavailable' | 'inconclusive'
|
|
91
|
+
readonly triggerReason: string
|
|
92
|
+
/** 用户原选择:替换判定的不变基线 */
|
|
93
|
+
readonly originalSelection: RecoverySelectionRef
|
|
94
|
+
readonly countedModelTurns: number
|
|
95
|
+
readonly phase: RecoveryChainPhase
|
|
96
|
+
readonly substitutionDisclosed: boolean
|
|
97
|
+
readonly resolvedTo?: RecoverySelectionRef
|
|
98
|
+
readonly history: readonly RecoveryAuditEvent[]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function isNonEmpty(value: unknown): value is string { return typeof value === 'string' && value.trim().length > 0 }
|
|
102
|
+
function requireBinding(key: string, value: unknown): string {
|
|
103
|
+
if (!isNonEmpty(value)) throw new Error(`recovery_binding_missing:${key}`)
|
|
104
|
+
return value
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function stable(value: unknown): unknown {
|
|
108
|
+
if (Array.isArray(value)) return value.map(stable)
|
|
109
|
+
if (!value || typeof value !== 'object') return value
|
|
110
|
+
return Object.fromEntries(Object.keys(value as Record<string, unknown>).sort().map((key) => [key, stable((value as Record<string, unknown>)[key])]))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** 分类输入 receipt 的规范摘要(键排序后 sha256;证据链锚点)。 */
|
|
114
|
+
export function receiptDigestOf(receipt: ActionReceipt): string {
|
|
115
|
+
return createHash('sha256').update(JSON.stringify(stable(receipt))).digest('hex')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 检测(识别分类):把一张 action receipt 分类到恢复链闭集。
|
|
121
|
+
* - offer.availability 观测:完整分类 + 连贯性守卫(与 availability reducer 同一套不变式,
|
|
122
|
+
* 违例抛 availability_receipt_incoherent / availability_observation_required 同名错误);
|
|
123
|
+
* - offers.state 观测:query seam,generation 折叠归 policy 所有 → unrelated;
|
|
124
|
+
* - gap 观测:不可确认 → inconclusive(不得宣称市场无房);
|
|
125
|
+
* - 其余观测上的 changed/unavailable/no_match 是错配凭证 → fail-closed 抛错;
|
|
126
|
+
* 其余状态(applied/partial/…)落在其他 seam → unrelated。
|
|
127
|
+
*/
|
|
128
|
+
export function classifyRecoveryTrigger(receipt: ActionReceipt): RecoveryClassification {
|
|
129
|
+
const observation = receipt.observation
|
|
130
|
+
if (observation.kind === 'offers.state') return { kind: 'unrelated', observationKind: observation.kind, reason: 'query seam: generation folding is availability-policy owned' }
|
|
131
|
+
if (observation.kind === 'gap') return { kind: 'inconclusive', reason: `gap observation (${observation.code}): unconfirmable evidence — not_claim_market_empty` }
|
|
132
|
+
if (observation.kind !== 'offer.availability') {
|
|
133
|
+
if (['changed', 'unavailable', 'no_match'].includes(receipt.status)) throw new Error('availability_observation_required')
|
|
134
|
+
return { kind: 'unrelated', observationKind: observation.kind, reason: `receipt status ${receipt.status} rides a non-offer observation` }
|
|
135
|
+
}
|
|
136
|
+
const { offerRef, checkedOfferVersionRef, available, currentOfferVersionRef, verifiedOfferRef, changedFactRefs } = observation
|
|
137
|
+
// 连贯性守卫(镜像 recordOfferCheckReceipt 的 incoherent 判定,fail-closed)
|
|
138
|
+
if (available && (receipt.resultContract.outcome === 'empty' || ['unavailable', 'no_match', 'failed', 'stale', 'unsupported'].includes(receipt.status))) throw new Error('availability_receipt_incoherent')
|
|
139
|
+
if (!available && (verifiedOfferRef || currentOfferVersionRef)) throw new Error('availability_receipt_incoherent')
|
|
140
|
+
if (['unavailable', 'no_match'].includes(receipt.status) && receipt.resultContract.outcome !== 'empty') throw new Error('availability_receipt_incoherent')
|
|
141
|
+
if (receipt.status === 'applied' && available && currentOfferVersionRef === checkedOfferVersionRef && verifiedOfferRef && changedFactRefs.length === 0) {
|
|
142
|
+
return { kind: 'confirmed', offerRef, offerVersionRef: checkedOfferVersionRef, verifiedOfferRef }
|
|
143
|
+
}
|
|
144
|
+
if (receipt.status === 'changed' && available && currentOfferVersionRef && currentOfferVersionRef !== checkedOfferVersionRef) {
|
|
145
|
+
return { kind: 'changed', offerRef, checkedOfferVersionRef, currentOfferVersionRef, changedFactRefs, reason: `supplier superseded ${checkedOfferVersionRef} with ${currentOfferVersionRef} (${changedFactRefs.join(',') || 'unspecified facts'})` }
|
|
146
|
+
}
|
|
147
|
+
if (['unavailable', 'no_match'].includes(receipt.status) && !available) {
|
|
148
|
+
return { kind: 'unavailable', offerRef, offerVersionRef: checkedOfferVersionRef, reason: `complete negative evidence (${receipt.status}, empty outcome)` }
|
|
149
|
+
}
|
|
150
|
+
return { kind: 'inconclusive', reason: `status ${receipt.status}: unconfirmable evidence — partial/stale/failed/gap cannot claim market empty (not_claim_market_empty)` }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function isSelection(value: unknown): value is RecoverySelectionRef {
|
|
154
|
+
const candidate = value as RecoverySelectionRef
|
|
155
|
+
return Boolean(candidate && isNonEmpty(candidate.hotelRef) && isNonEmpty(candidate.offerRef) && isNonEmpty(candidate.offerVersionRef))
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function sameOfferTarget(a: RecoverySelectionRef, b: RecoverySelectionRef): boolean {
|
|
159
|
+
return a.hotelRef === b.hotelRef && a.offerRef === b.offerRef
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 开立恢复链:检测出 changed/unavailable/inconclusive 触发后建链(fail-closed 绑定键 +
|
|
164
|
+
* 首条 recovery.detected 审计事件)。confirmed 无需恢复链、unrelated 无触发,均拒绝开链。
|
|
165
|
+
*/
|
|
166
|
+
export function openRecoveryChain(seed: {
|
|
167
|
+
taskId: string
|
|
168
|
+
contextRef: string
|
|
169
|
+
receipt: ActionReceipt
|
|
170
|
+
originalSelection: RecoverySelectionRef
|
|
171
|
+
at: string
|
|
172
|
+
}): RecoveryChainState {
|
|
173
|
+
const taskId = requireBinding('taskId', seed.taskId)
|
|
174
|
+
const contextRef = requireBinding('contextRef', seed.contextRef)
|
|
175
|
+
if (!isNonEmpty(seed.at)) throw new Error('recovery_binding_missing:at')
|
|
176
|
+
const originalSelection = seed.originalSelection
|
|
177
|
+
if (!isSelection(originalSelection)) throw new Error('recovery_binding_missing:originalSelection')
|
|
178
|
+
const classification = classifyRecoveryTrigger(seed.receipt)
|
|
179
|
+
if (classification.kind === 'confirmed') throw new Error('recovery_chain_not_required')
|
|
180
|
+
if (classification.kind === 'unrelated') throw new Error('recovery_trigger_absent')
|
|
181
|
+
return {
|
|
182
|
+
schemaVersion: RECOVERY_CHAIN_SCHEMA,
|
|
183
|
+
taskId,
|
|
184
|
+
contextRef,
|
|
185
|
+
trigger: classification.kind,
|
|
186
|
+
triggerReason: classification.reason,
|
|
187
|
+
originalSelection: { ...originalSelection },
|
|
188
|
+
countedModelTurns: 0,
|
|
189
|
+
phase: 'detected',
|
|
190
|
+
substitutionDisclosed: false,
|
|
191
|
+
history: [{
|
|
192
|
+
at: seed.at,
|
|
193
|
+
kind: 'recovery.detected',
|
|
194
|
+
from: 'detected',
|
|
195
|
+
to: 'detected',
|
|
196
|
+
reason: classification.reason,
|
|
197
|
+
receiptDigest: receiptDigestOf(seed.receipt),
|
|
198
|
+
target: { ...originalSelection },
|
|
199
|
+
}],
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 受控恢复推进:一步一凭证(重查/换版复查/替代候选的检查结果)。
|
|
205
|
+
* - confirmed → resolved;目标与原选择不同酒店/报价且未先披露 = 静默替换,结构性拒绝;
|
|
206
|
+
* - 其余分类 → 计入预算的恢复轮(countedModelTurns + 1,含确认轮);超 3 次拒绝、必须降级;
|
|
207
|
+
* - resolved/degraded 为吸收终态,其后不得再有 attempt。
|
|
208
|
+
*/
|
|
209
|
+
export function attemptRecoveryStep(
|
|
210
|
+
state: RecoveryChainState,
|
|
211
|
+
attempt: { receipt: ActionReceipt; target: RecoverySelectionRef; approach: RecoveryApproach },
|
|
212
|
+
at: string,
|
|
213
|
+
): RecoveryChainState {
|
|
214
|
+
if (state.phase === 'resolved' || state.phase === 'degraded') throw new Error('recovery_chain_terminal')
|
|
215
|
+
if (!RECOVERY_APPROACHES.has(attempt.approach)) throw new Error('recovery_approach_invalid')
|
|
216
|
+
if (!isNonEmpty(at)) throw new Error('recovery_binding_missing:at')
|
|
217
|
+
if (!isSelection(attempt.target)) throw new Error('recovery_binding_missing:target')
|
|
218
|
+
const classification = classifyRecoveryTrigger(attempt.receipt)
|
|
219
|
+
if (classification.kind === 'unrelated') throw new Error('recovery_trigger_absent')
|
|
220
|
+
if (classification.kind === 'confirmed') {
|
|
221
|
+
if (!sameOfferTarget(attempt.target, state.originalSelection) && !state.substitutionDisclosed) {
|
|
222
|
+
throw new Error('recovery_silent_swap_forbidden')
|
|
223
|
+
}
|
|
224
|
+
const versionShifted = attempt.target.offerVersionRef !== state.originalSelection.offerVersionRef
|
|
225
|
+
return {
|
|
226
|
+
...state,
|
|
227
|
+
phase: 'resolved',
|
|
228
|
+
resolvedTo: { ...attempt.target },
|
|
229
|
+
history: [...state.history, {
|
|
230
|
+
at,
|
|
231
|
+
kind: 'recovery.resolved',
|
|
232
|
+
from: state.phase,
|
|
233
|
+
to: 'resolved',
|
|
234
|
+
reason: `confirmed via ${attempt.approach}${versionShifted ? ' on superseded version' : ''}`,
|
|
235
|
+
receiptDigest: receiptDigestOf(attempt.receipt),
|
|
236
|
+
target: { ...attempt.target },
|
|
237
|
+
substitution: !sameOfferTarget(attempt.target, state.originalSelection),
|
|
238
|
+
}],
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (state.countedModelTurns + 1 > MAX_RECOVERY_MODEL_TURNS_PER_TRIGGER) {
|
|
242
|
+
throw new Error(`recovery_model_turn_budget_exhausted (${MAX_RECOVERY_MODEL_TURNS_PER_TRIGGER} counted turns per trigger; degrade explicitly instead of retrying)`)
|
|
243
|
+
}
|
|
244
|
+
const reason = classification.reason
|
|
245
|
+
return {
|
|
246
|
+
...state,
|
|
247
|
+
phase: 'recovering',
|
|
248
|
+
countedModelTurns: state.countedModelTurns + 1,
|
|
249
|
+
history: [...state.history, {
|
|
250
|
+
at,
|
|
251
|
+
kind: 'recovery.attempt',
|
|
252
|
+
from: state.phase,
|
|
253
|
+
to: 'recovering',
|
|
254
|
+
reason: `${attempt.approach}: ${classification.kind}: ${reason}`,
|
|
255
|
+
receiptDigest: receiptDigestOf(attempt.receipt),
|
|
256
|
+
target: { ...attempt.target },
|
|
257
|
+
}],
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* 替换披露:恢复将落在与用户原选择不同的酒店/报价上时,必须在确认前显式披露。
|
|
263
|
+
* 空披露文本拒绝——「显式说明变化,不静默换房换航」的结构化落点。
|
|
264
|
+
*/
|
|
265
|
+
export function discloseSubstitution(
|
|
266
|
+
state: RecoveryChainState,
|
|
267
|
+
disclosure: { target: RecoverySelectionRef; disclosureText: string; at: string },
|
|
268
|
+
): RecoveryChainState {
|
|
269
|
+
if (state.phase === 'resolved' || state.phase === 'degraded') throw new Error('recovery_chain_terminal')
|
|
270
|
+
if (!isNonEmpty(disclosure.disclosureText)) throw new Error('recovery_disclosure_required')
|
|
271
|
+
if (!isSelection(disclosure.target)) throw new Error('recovery_binding_missing:target')
|
|
272
|
+
if (!isNonEmpty(disclosure.at)) throw new Error('recovery_binding_missing:at')
|
|
273
|
+
return {
|
|
274
|
+
...state,
|
|
275
|
+
substitutionDisclosed: true,
|
|
276
|
+
history: [...state.history, {
|
|
277
|
+
at: disclosure.at,
|
|
278
|
+
kind: 'recovery.disclosed',
|
|
279
|
+
from: state.phase,
|
|
280
|
+
to: state.phase,
|
|
281
|
+
reason: disclosure.disclosureText.slice(0, 500),
|
|
282
|
+
target: { ...disclosure.target },
|
|
283
|
+
substitution: true,
|
|
284
|
+
}],
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* 显式降级:恢复预算耗尽(或有界候选全负/不可确认)时,只能携非空用户披露降级结束——
|
|
290
|
+
* 静默降级拒绝;降级后链吸收,不得再推进。
|
|
291
|
+
*/
|
|
292
|
+
export function degradeRecoveryChain(
|
|
293
|
+
state: RecoveryChainState,
|
|
294
|
+
degrade: { reason: string; disclosureText: string; at: string },
|
|
295
|
+
): RecoveryChainState {
|
|
296
|
+
if (state.phase === 'resolved' || state.phase === 'degraded') throw new Error('recovery_chain_terminal')
|
|
297
|
+
if (!isNonEmpty(degrade.reason)) throw new Error('recovery_binding_missing:reason')
|
|
298
|
+
if (!isNonEmpty(degrade.disclosureText)) throw new Error('recovery_degrade_requires_disclosure')
|
|
299
|
+
if (!isNonEmpty(degrade.at)) throw new Error('recovery_binding_missing:at')
|
|
300
|
+
return {
|
|
301
|
+
...state,
|
|
302
|
+
phase: 'degraded',
|
|
303
|
+
history: [...state.history,
|
|
304
|
+
{
|
|
305
|
+
at: degrade.at,
|
|
306
|
+
kind: 'recovery.disclosed',
|
|
307
|
+
from: state.phase,
|
|
308
|
+
to: state.phase,
|
|
309
|
+
reason: degrade.disclosureText.slice(0, 500),
|
|
310
|
+
substitution: false,
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
at: degrade.at,
|
|
314
|
+
kind: 'recovery.degraded',
|
|
315
|
+
from: state.phase,
|
|
316
|
+
to: 'degraded',
|
|
317
|
+
reason: degrade.reason.slice(0, 500),
|
|
318
|
+
},
|
|
319
|
+
],
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** 用户沟通投影:结构化披露(substitution/versionShifted/确定性说明文本)。 */
|
|
324
|
+
export function buildUserDisclosure(state: RecoveryChainState): {
|
|
325
|
+
substitution: boolean
|
|
326
|
+
versionShifted: boolean
|
|
327
|
+
text: string
|
|
328
|
+
} {
|
|
329
|
+
if (state.phase === 'resolved' && state.resolvedTo) {
|
|
330
|
+
const substitution = !sameOfferTarget(state.resolvedTo, state.originalSelection)
|
|
331
|
+
const versionShifted = state.resolvedTo.offerVersionRef !== state.originalSelection.offerVersionRef
|
|
332
|
+
if (substitution) {
|
|
333
|
+
return {
|
|
334
|
+
substitution,
|
|
335
|
+
versionShifted,
|
|
336
|
+
text: `Availability changed: original ${state.originalSelection.hotelRef}/${state.originalSelection.offerRef} could not be confirmed (${state.triggerReason}). The substitute ${state.resolvedTo.hotelRef}/${state.resolvedTo.offerRef} is disclosed explicitly for your approval — no silent swap.`,
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (versionShifted) {
|
|
340
|
+
return {
|
|
341
|
+
substitution,
|
|
342
|
+
versionShifted,
|
|
343
|
+
text: `Availability changed: ${state.originalSelection.offerRef} moved from version ${state.originalSelection.offerVersionRef} to ${state.resolvedTo.offerVersionRef} at the supplier; re-confirmed on the new version.`,
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return { substitution, versionShifted, text: 'Original selection confirmed after recovery.' }
|
|
347
|
+
}
|
|
348
|
+
if (state.phase === 'degraded') {
|
|
349
|
+
return { substitution: false, versionShifted: false, text: `Recovery exhausted for ${state.originalSelection.hotelRef}/${state.originalSelection.offerRef}: ${state.triggerReason}. Task ended explicitly with no substitute booked.` }
|
|
350
|
+
}
|
|
351
|
+
return { substitution: false, versionShifted: false, text: `Recovery in progress: supplier reported ${state.trigger} — ${state.triggerReason}` }
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* 审计链校验(形态对齐 booking-saga 的 sagaTraceViolations;空数组 = 链合法):
|
|
356
|
+
* - 必须以恰好一条 recovery.detected 开链;
|
|
357
|
+
* - resolved/degraded 至多各一次且为吸收终态——其后不得有任何事件;
|
|
358
|
+
* - attempt/disclosed 只许出现在终态前;
|
|
359
|
+
* - resolved 目标与 detected 原目标不同酒店/报价时,之前必须有 substitution=true 的披露;
|
|
360
|
+
* - degraded 之前必须有披露事件(静默降级违例)。
|
|
361
|
+
*/
|
|
362
|
+
export function recoveryAuditViolations(events: ReadonlyArray<RecoveryAuditEvent>): string[] {
|
|
363
|
+
const violations: string[] = []
|
|
364
|
+
let opened = false
|
|
365
|
+
let detectedTarget: RecoverySelectionRef | undefined
|
|
366
|
+
let terminal: 'resolved' | 'degraded' | undefined
|
|
367
|
+
let disclosedSubstitution = false
|
|
368
|
+
let disclosedSeen = false
|
|
369
|
+
events.forEach((event, index) => {
|
|
370
|
+
const at = `#${index} ${event.kind}`
|
|
371
|
+
if (terminal) { violations.push(`${at}: 终态(${terminal})后出现事件(吸收态)`) ; return }
|
|
372
|
+
switch (event.kind) {
|
|
373
|
+
case 'recovery.detected': {
|
|
374
|
+
if (opened) { violations.push(`${at}: 重复开链`); return }
|
|
375
|
+
opened = true
|
|
376
|
+
detectedTarget = event.target
|
|
377
|
+
return
|
|
378
|
+
}
|
|
379
|
+
case 'recovery.attempt': {
|
|
380
|
+
if (!opened) { violations.push(`${at}: 开链前的 attempt`); return }
|
|
381
|
+
return
|
|
382
|
+
}
|
|
383
|
+
case 'recovery.disclosed': {
|
|
384
|
+
if (!opened) { violations.push(`${at}: 开链前的披露`); return }
|
|
385
|
+
disclosedSeen = true
|
|
386
|
+
if (event.substitution) disclosedSubstitution = true
|
|
387
|
+
return
|
|
388
|
+
}
|
|
389
|
+
case 'recovery.resolved': {
|
|
390
|
+
if (!opened) { violations.push(`${at}: 开链前的 resolved`); return }
|
|
391
|
+
if (detectedTarget && event.target && !sameOfferTarget(event.target, detectedTarget) && !disclosedSubstitution) {
|
|
392
|
+
violations.push(`${at}: 静默替换(resolved 目标偏离原选择且无披露)`)
|
|
393
|
+
}
|
|
394
|
+
terminal = 'resolved'
|
|
395
|
+
return
|
|
396
|
+
}
|
|
397
|
+
case 'recovery.degraded': {
|
|
398
|
+
if (!opened) { violations.push(`${at}: 开链前的 degraded`); return }
|
|
399
|
+
if (!disclosedSeen) violations.push(`${at}: 静默降级(degraded 前无披露)`)
|
|
400
|
+
terminal = 'degraded'
|
|
401
|
+
return
|
|
402
|
+
}
|
|
403
|
+
default:
|
|
404
|
+
violations.push(`${at}: 未知事件 kind`)
|
|
405
|
+
}
|
|
406
|
+
})
|
|
407
|
+
if (!opened) violations.push('链未以 recovery.detected 开立')
|
|
408
|
+
return violations
|
|
409
|
+
}
|
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
type BookingPlannerSessionFactory,
|
|
35
35
|
} from './runtime.ts'
|
|
36
36
|
import { validateBookingSurface } from './validation.ts'
|
|
37
|
-
import { normalizeBookingErrorCode, safeBookingErrorMessage } from './error-codes.ts'
|
|
37
|
+
import { extractBookingDispatchReason, normalizeBookingErrorCode, safeBookingErrorMessage } from './error-codes.ts'
|
|
38
38
|
|
|
39
39
|
export interface BookingCopilotComposition {
|
|
40
40
|
runtime: BookingCopilotTaskRuntime
|
|
@@ -63,13 +63,80 @@ export interface BookingCopilotServerHandle {
|
|
|
63
63
|
close(): Promise<void>
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
interface BookingCopilotRuntimeIdentity {
|
|
66
|
+
export interface BookingCopilotRuntimeIdentity {
|
|
67
67
|
nodeVersion: string
|
|
68
68
|
nodeModulesAbi: string
|
|
69
69
|
releaseTuple: string
|
|
70
70
|
glibcVersion: string
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/** gotry-backend 模块挂载用的流量处理依赖(startBookingCopilotServer 与 backend 模块共用) */
|
|
74
|
+
export interface BookingCopilotTrafficDeps {
|
|
75
|
+
apiKey: string
|
|
76
|
+
composition: BookingCopilotComposition
|
|
77
|
+
maxBodyBytes: number
|
|
78
|
+
artifactId?: string
|
|
79
|
+
ingressMode: BookingCopilotIngressMode
|
|
80
|
+
runningIdentity: BookingCopilotRuntimeIdentity
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 单请求处理闭包(startBookingCopilotServer 的 createServer 回调原样摘出):
|
|
85
|
+
* 探活(/healthz /status)+ 唯一 turn 路由 + 鉴权 + schema 头校验。
|
|
86
|
+
* gotry-backend 的 booking-copilot 模块复用同一闭包,保证两条挂载路径行为逐字一致。
|
|
87
|
+
*/
|
|
88
|
+
export function bookingCopilotTrafficHandler(deps: BookingCopilotTrafficDeps): (req: IncomingMessage, res: ServerResponse) => Promise<void> {
|
|
89
|
+
const { apiKey, composition, maxBodyBytes, artifactId, ingressMode, runningIdentity } = deps
|
|
90
|
+
return async (req, res) => {
|
|
91
|
+
const isProbe = req.method === 'GET' && (req.url === '/healthz' || req.url === '/status')
|
|
92
|
+
if (!isProbe && (req.method !== 'POST' || req.url !== '/a2a/booking-copilot/turn')) {
|
|
93
|
+
sendJson(res, 404, { error: { code: 'not_found' } })
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
const auth = String(req.headers.authorization ?? '')
|
|
97
|
+
if (!safeSecretEqual(auth, `Bearer ${apiKey}`)) {
|
|
98
|
+
sendJson(res, 401, { error: { code: 'unauthorized' } })
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
if (isProbe) {
|
|
102
|
+
res.setHeader(BOOKING_SURFACE_VERSION_HEADER, BOOKING_SURFACE_SCHEMA_VERSION)
|
|
103
|
+
res.setHeader(BOOKING_SURFACE_SCHEMA_SHA256_HEADER, BOOKING_SURFACE_SCHEMA_SHA256)
|
|
104
|
+
if (artifactId) res.setHeader('X-GoTry-Artifact-ID', artifactId)
|
|
105
|
+
res.setHeader('X-GoTry-Node-Version', runningIdentity.nodeVersion)
|
|
106
|
+
res.setHeader('X-GoTry-Node-Modules-ABI', runningIdentity.nodeModulesAbi)
|
|
107
|
+
res.setHeader('X-GoTry-Release-Tuple', runningIdentity.releaseTuple)
|
|
108
|
+
if (runningIdentity.glibcVersion) res.setHeader('X-GoTry-Glibc-Version', runningIdentity.glibcVersion)
|
|
109
|
+
const acceptedTurnKinds = ingressMode === 'bff-ingress-binding'
|
|
110
|
+
? [...BOOKING_COPILOT_ACCEPTED_TURN_KINDS, 'user.turn.ingress' as const]
|
|
111
|
+
: [...BOOKING_COPILOT_ACCEPTED_TURN_KINDS]
|
|
112
|
+
const healthBody: Record<string, unknown> = {
|
|
113
|
+
schemaVersion: BOOKING_SURFACE_SCHEMA_VERSION,
|
|
114
|
+
schemaSha256: BOOKING_SURFACE_SCHEMA_SHA256,
|
|
115
|
+
status: 'ready',
|
|
116
|
+
ingressMode,
|
|
117
|
+
acceptedTurnKinds,
|
|
118
|
+
}
|
|
119
|
+
res.setHeader('X-GoTry-Ingress-Mode', ingressMode)
|
|
120
|
+
res.setHeader('X-GoTry-Accepted-Turn-Kinds', acceptedTurnKinds.join(','))
|
|
121
|
+
sendJson(res, 200, healthBody)
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
const schemaVersion = String(req.headers[BOOKING_SURFACE_VERSION_HEADER] ?? '')
|
|
125
|
+
const schemaHash = String(req.headers[BOOKING_SURFACE_SCHEMA_SHA256_HEADER] ?? '')
|
|
126
|
+
if (schemaVersion !== BOOKING_SURFACE_SCHEMA_VERSION || schemaHash !== BOOKING_SURFACE_SCHEMA_SHA256) {
|
|
127
|
+
sendJson(res, 409, {
|
|
128
|
+
error: {
|
|
129
|
+
code: 'booking_surface_schema_mismatch',
|
|
130
|
+
expectedVersion: BOOKING_SURFACE_SCHEMA_VERSION,
|
|
131
|
+
expectedSchemaSha256: BOOKING_SURFACE_SCHEMA_SHA256,
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
await handleBookingCopilotRequest(req, res, composition, maxBodyBytes)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
73
140
|
const sessionsByComposition = new WeakMap<BookingCopilotComposition, Map<string, ReturnType<BookingPlannerSessionFactory>>>()
|
|
74
141
|
const decisionFlightsByComposition = new WeakMap<BookingCopilotComposition, Map<string, Promise<BookingSurfaceEvent[]>>>()
|
|
75
142
|
function sessionsFor(composition: BookingCopilotComposition): Map<string, ReturnType<BookingPlannerSessionFactory>> {
|
|
@@ -91,7 +158,7 @@ async function runDecisionSingleFlight(composition: BookingCopilotComposition, k
|
|
|
91
158
|
try { return await current } finally { if (flights.get(key) === current) flights.delete(key) }
|
|
92
159
|
}
|
|
93
160
|
|
|
94
|
-
function runtimeIdentity(): BookingCopilotRuntimeIdentity {
|
|
161
|
+
export function runtimeIdentity(): BookingCopilotRuntimeIdentity {
|
|
95
162
|
const report = process.report?.getReport?.() as { header?: { glibcVersionRuntime?: string } } | undefined
|
|
96
163
|
const header = report?.header
|
|
97
164
|
const glibcVersion = header?.glibcVersionRuntime ?? ''
|
|
@@ -300,7 +367,11 @@ async function handleBookingCopilotRequest(req: IncomingMessage, res: ServerResp
|
|
|
300
367
|
}
|
|
301
368
|
}
|
|
302
369
|
}
|
|
303
|
-
} catch (error) {
|
|
370
|
+
} catch (error) {
|
|
371
|
+
const code = normalizeBookingErrorCode(error)
|
|
372
|
+
console.error(JSON.stringify({ code, reason: extractBookingDispatchReason(error) }))
|
|
373
|
+
sendJson(res, 409, { error: { code } }); return
|
|
374
|
+
}
|
|
304
375
|
|
|
305
376
|
res.writeHead(200, { 'content-type': 'text/event-stream; charset=utf-8', 'cache-control': 'no-cache, no-transform', connection: 'keep-alive', 'x-content-type-options': 'nosniff', [BOOKING_SURFACE_VERSION_HEADER]: BOOKING_SURFACE_SCHEMA_VERSION, [BOOKING_SURFACE_SCHEMA_SHA256_HEADER]: BOOKING_SURFACE_SCHEMA_SHA256 })
|
|
306
377
|
try {
|
|
@@ -366,53 +437,15 @@ export function startBookingCopilotServer(options: BookingCopilotServerOptions):
|
|
|
366
437
|
const maxBodyBytes = options.maxBodyBytes ?? 1_000_000
|
|
367
438
|
const runningIdentity = runtimeIdentity()
|
|
368
439
|
|
|
369
|
-
const server = createServer(
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
return
|
|
379
|
-
}
|
|
380
|
-
if (isProbe) {
|
|
381
|
-
res.setHeader(BOOKING_SURFACE_VERSION_HEADER, BOOKING_SURFACE_SCHEMA_VERSION)
|
|
382
|
-
res.setHeader(BOOKING_SURFACE_SCHEMA_SHA256_HEADER, BOOKING_SURFACE_SCHEMA_SHA256)
|
|
383
|
-
if (options.artifactId) res.setHeader('X-GoTry-Artifact-ID', options.artifactId)
|
|
384
|
-
res.setHeader('X-GoTry-Node-Version', runningIdentity.nodeVersion)
|
|
385
|
-
res.setHeader('X-GoTry-Node-Modules-ABI', runningIdentity.nodeModulesAbi)
|
|
386
|
-
res.setHeader('X-GoTry-Release-Tuple', runningIdentity.releaseTuple)
|
|
387
|
-
if (runningIdentity.glibcVersion) res.setHeader('X-GoTry-Glibc-Version', runningIdentity.glibcVersion)
|
|
388
|
-
const acceptedTurnKinds = ingressMode === 'bff-ingress-binding'
|
|
389
|
-
? [...BOOKING_COPILOT_ACCEPTED_TURN_KINDS, 'user.turn.ingress' as const]
|
|
390
|
-
: [...BOOKING_COPILOT_ACCEPTED_TURN_KINDS]
|
|
391
|
-
const healthBody: Record<string, unknown> = {
|
|
392
|
-
schemaVersion: BOOKING_SURFACE_SCHEMA_VERSION,
|
|
393
|
-
schemaSha256: BOOKING_SURFACE_SCHEMA_SHA256,
|
|
394
|
-
status: 'ready',
|
|
395
|
-
ingressMode,
|
|
396
|
-
acceptedTurnKinds,
|
|
397
|
-
}
|
|
398
|
-
res.setHeader('X-GoTry-Ingress-Mode', ingressMode)
|
|
399
|
-
res.setHeader('X-GoTry-Accepted-Turn-Kinds', acceptedTurnKinds.join(','))
|
|
400
|
-
sendJson(res, 200, healthBody)
|
|
401
|
-
return
|
|
402
|
-
}
|
|
403
|
-
const schemaVersion = String(req.headers[BOOKING_SURFACE_VERSION_HEADER] ?? '')
|
|
404
|
-
const schemaHash = String(req.headers[BOOKING_SURFACE_SCHEMA_SHA256_HEADER] ?? '')
|
|
405
|
-
if (schemaVersion !== BOOKING_SURFACE_SCHEMA_VERSION || schemaHash !== BOOKING_SURFACE_SCHEMA_SHA256) {
|
|
406
|
-
sendJson(res, 409, {
|
|
407
|
-
error: {
|
|
408
|
-
code: 'booking_surface_schema_mismatch',
|
|
409
|
-
expectedVersion: BOOKING_SURFACE_SCHEMA_VERSION,
|
|
410
|
-
expectedSchemaSha256: BOOKING_SURFACE_SCHEMA_SHA256,
|
|
411
|
-
},
|
|
412
|
-
})
|
|
413
|
-
return
|
|
414
|
-
}
|
|
415
|
-
await handleBookingCopilotRequest(req, res, composition, maxBodyBytes)
|
|
440
|
+
const server = createServer((req, res) => {
|
|
441
|
+
void bookingCopilotTrafficHandler({
|
|
442
|
+
apiKey: options.apiKey,
|
|
443
|
+
composition,
|
|
444
|
+
maxBodyBytes,
|
|
445
|
+
...(options.artifactId !== undefined ? { artifactId: options.artifactId } : {}),
|
|
446
|
+
ingressMode,
|
|
447
|
+
runningIdentity,
|
|
448
|
+
})(req, res)
|
|
416
449
|
})
|
|
417
450
|
|
|
418
451
|
return new Promise((resolve, reject) => {
|
|
@@ -3,6 +3,8 @@ import { Ajv2020, type ValidateFunction } from 'ajv/dist/2020.js'
|
|
|
3
3
|
import { BOOKING_READ_ACTION_KINDS, BOOKING_BLOCKER_CODES, BOOKING_GAP_CODES } from './contracts.ts'
|
|
4
4
|
|
|
5
5
|
const schema = JSON.parse(readFileSync(new URL('../../../schemas/booking.surface.schema.json', import.meta.url), 'utf8')) as Record<string, unknown>
|
|
6
|
+
/** Canonical schema bytes (single source of truth) for prompt/tool derivations elsewhere. */
|
|
7
|
+
export const bookingSurfaceSchema = schema
|
|
6
8
|
const ajv = new Ajv2020({ allErrors: true, strict: true })
|
|
7
9
|
ajv.addSchema(schema)
|
|
8
10
|
const validate: ValidateFunction = ajv.compile(schema)
|
package/ts/src/contracts.ts
CHANGED
|
@@ -16,8 +16,10 @@ export interface CalendarState {
|
|
|
16
16
|
assertedWeekdays: Record<string, 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat' | 'sun'>
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export interface
|
|
20
|
-
|
|
19
|
+
export interface ScheduledWorkWindowProfile {
|
|
20
|
+
/** Legacy v1 only; v2 pack metadata owns the IANA home zone. */
|
|
21
|
+
homeTzOffsetMin?: number
|
|
22
|
+
vacation?: false
|
|
21
23
|
startMin: number
|
|
22
24
|
endMin: number
|
|
23
25
|
workdays: number[]
|
|
@@ -25,6 +27,14 @@ export interface WorkWindowProfile {
|
|
|
25
27
|
evidence: string
|
|
26
28
|
}
|
|
27
29
|
|
|
30
|
+
/** Explicit leave means this trip has no work-time restriction. */
|
|
31
|
+
export interface VacationWorkWindowProfile {
|
|
32
|
+
vacation: true
|
|
33
|
+
evidence?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type WorkWindowProfile = ScheduledWorkWindowProfile | VacationWorkWindowProfile
|
|
37
|
+
|
|
28
38
|
export interface TravelerProfile {
|
|
29
39
|
workWindow?: WorkWindowProfile
|
|
30
40
|
companions?: string[]
|
package/ts/src/dsh-llm.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type { InterviewQuestion, TravelerProfile, TripState, Turn, CalendarState
|
|
|
12
12
|
import { parseFlightPackToSpec, type JourneySpecTS } from './unified.ts'
|
|
13
13
|
import { buildTimeAnchor } from './time-anchor.ts'
|
|
14
14
|
import { buildSlotSystem, flagExpiredSlots, normalizeExtraction, type TravelSlotExtraction } from './travel-slots.ts'
|
|
15
|
+
import { mergeProfileWorkWindow } from './flight-pack-adapter.ts'
|
|
15
16
|
|
|
16
17
|
// 惰性读取(env 在调用时取值):模块顶常量会在 .env 加载前冻结(ESM import 提升),
|
|
17
18
|
// 脚本先 loadEnv 再 import 也救不了——401 错配(key 发往默认端点)的存量隐患由此根除。
|
|
@@ -82,10 +83,10 @@ function parseJsonBlock(text: string): Record<string, unknown> | null {
|
|
|
82
83
|
|
|
83
84
|
const FACTS_SYSTEM = `你是旅行规划的事实抽取器。从对话中抽取两类事实并以 JSON 返回:
|
|
84
85
|
{"calendar": {"year": 数字, "assertedWeekdays": {"YYYY-MM-DD": "mon|tue|wed|thu|fri|sat|sun"}},
|
|
85
|
-
"profile": {"workWindow": {"homeTzOffsetMin":
|
|
86
|
+
"profile": {"workWindow": {"homeTzOffsetMin": 数字(仅 legacy v1;用户未明确时可省略), "startMin": 数字, "endMin": 数字, "workdays": [0,1,2,3,4], "evidence": "用户原话"},
|
|
86
87
|
"companions": ["..."], "budgetTier": "economy|comfort|convenience",
|
|
87
88
|
"bookedResources": [{"kind": "flight|hotel", "ref": "...", "window": "..."}]}}
|
|
88
|
-
只放用户明确说过的事实;没有的字段省略。分钟数从 HH:MM
|
|
89
|
+
只放用户明确说过的事实;没有的字段省略。分钟数从 HH:MM 换算;用户未明确说 numeric offset 时不要猜、不要输出 homeTzOffsetMin;v2 pack 的 IANA home zone 属于数据层权威。
|
|
89
90
|
**休假语义(关键)**:用户说「请假/年假/不用办公/休假」→ workWindow 输出 {"vacation": true}(不是省略!省略会触发重复追问);只有用户明确给了工作时间才输出完整 workWindow 对象。只输出 JSON。`
|
|
90
91
|
|
|
91
92
|
const SKELETON_SYSTEM = `你是行程骨架抽取器。从对话中抽取行程的**骨架**——段(移动)与锚点,不包含任何班次数据(班次来自数据层,你不要编造时刻/价格/航班号)。
|
|
@@ -155,12 +156,7 @@ export function createOpenAICompatLlm(flightPackPath?: string, clock: () => Date
|
|
|
155
156
|
const a = anchorsById.get(seg.id) as { arriveByMin?: number } | undefined
|
|
156
157
|
if (a?.arriveByMin !== undefined) seg.anchors = { arriveByMin: a.arriveByMin }
|
|
157
158
|
}
|
|
158
|
-
packSpec
|
|
159
|
-
homeTzOffsetMin: state.profile.workWindow.homeTzOffsetMin,
|
|
160
|
-
startMin: state.profile.workWindow.startMin,
|
|
161
|
-
endMin: state.profile.workWindow.endMin,
|
|
162
|
-
workdays: state.profile.workWindow.workdays,
|
|
163
|
-
} : undefined
|
|
159
|
+
packSpec = mergeProfileWorkWindow(packSpec, state.profile.workWindow)
|
|
164
160
|
packSpec.budgetCny = 9000
|
|
165
161
|
return packSpec
|
|
166
162
|
},
|