@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.
Files changed (327) hide show
  1. package/README.md +102 -194
  2. package/README.zh-CN.md +115 -210
  3. package/bin/gotry-backend.js +53 -0
  4. package/bin/gotry-bootstrap.js +466 -35
  5. package/bin/gotry-inner.js +255 -36
  6. package/bin/gotry-process-liveness.d.ts +27 -0
  7. package/bin/gotry-process-liveness.js +97 -0
  8. package/bin/gotry-runtime-resolution.d.ts +1 -1
  9. package/bin/gotry-runtime-resolution.js +2 -2
  10. package/client/client.js +174 -0
  11. package/cordis.gotry-patch.yml +70 -93
  12. package/dist/capabilities/agent-reach-deep.js +45 -122
  13. package/dist/capabilities/agent-reach.js +63 -148
  14. package/dist/capabilities/anything.js +89 -75
  15. package/dist/capabilities/artifacts.js +123 -104
  16. package/dist/capabilities/channel-health.js +65 -45
  17. package/dist/capabilities/channel-registry.js +108 -87
  18. package/dist/capabilities/doctor.js +357 -235
  19. package/dist/capabilities/effect.js +223 -309
  20. package/dist/capabilities/fact-log.js +28 -14
  21. package/dist/capabilities/flyai.js +245 -156
  22. package/dist/capabilities/ground-transfer.js +509 -0
  23. package/dist/capabilities/hbcli.js +144 -148
  24. package/dist/capabilities/hotelbyte-transaction.js +194 -0
  25. package/dist/capabilities/incident-log.js +131 -61
  26. package/dist/capabilities/model-override.js +30 -10
  27. package/dist/capabilities/opensky.js +54 -43
  28. package/dist/capabilities/resilience.js +50 -32
  29. package/dist/capabilities/session/action-cache.js +50 -44
  30. package/dist/capabilities/session/adapters/ctrip-flight.js +119 -62
  31. package/dist/capabilities/session/adapters/ctrip-hotel.js +125 -110
  32. package/dist/capabilities/session/adapters/dida-portal.js +206 -0
  33. package/dist/capabilities/session/adapters/meituan-local.js +35 -54
  34. package/dist/capabilities/session/adapters/rail-12306.js +169 -112
  35. package/dist/capabilities/session/benchmark.js +85 -149
  36. package/dist/capabilities/session/extension-bridge.js +320 -259
  37. package/dist/capabilities/session/extension-channel.js +57 -76
  38. package/dist/capabilities/session/extension-distribution.js +115 -119
  39. package/dist/capabilities/session/extract.js +31 -21
  40. package/dist/capabilities/session/golden-score.js +55 -33
  41. package/dist/capabilities/session/health-watch.js +146 -154
  42. package/dist/capabilities/session/read-guard.js +55 -47
  43. package/dist/capabilities/session/static-flight-golden.js +49 -36
  44. package/dist/capabilities/session/transport.js +54 -61
  45. package/dist/capabilities/session/wizard.js +63 -93
  46. package/dist/capabilities/session-consent.js +143 -47
  47. package/dist/capabilities/session-login.js +95 -126
  48. package/dist/capabilities/session-search.js +391 -253
  49. package/dist/capabilities/sponsor-plugin.js +341 -0
  50. package/dist/capabilities/visa-policy.js +87 -73
  51. package/dist/capabilities/weather.js +163 -235
  52. package/dist/scripts/action-cache-tests.js +24 -59
  53. package/dist/scripts/agent-planning-turn-deadline-e2e.js +91 -142
  54. package/dist/scripts/agent-planning-turn-deadline-tests.js +80 -176
  55. package/dist/scripts/agent-reach-deep-tests.js +30 -40
  56. package/dist/scripts/agent-reach-tests.js +26 -18
  57. package/dist/scripts/agent-reach-wrapper-tests.js +46 -50
  58. package/dist/scripts/anything-tests.js +58 -62
  59. package/dist/scripts/artifact-client-contract-tests.js +69 -0
  60. package/dist/scripts/artifacts-capability-tests.js +235 -0
  61. package/dist/scripts/async-collect.js +26 -15
  62. package/dist/scripts/benchmark-environment-bridge-e2e.js +287 -684
  63. package/dist/scripts/benchmark-environment-bridge-tests.js +818 -3123
  64. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +66 -279
  65. package/dist/scripts/booking-copilot-availability-policy-tests.js +153 -1257
  66. package/dist/scripts/booking-copilot-bin-proof-tests.js +22 -45
  67. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +64 -76
  68. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +69 -199
  69. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +393 -759
  70. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +33 -75
  71. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +60 -110
  72. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +28 -66
  73. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +72 -271
  74. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +73 -275
  75. package/dist/scripts/booking-copilot-runtime-proof-tests.js +784 -3398
  76. package/dist/scripts/booking-copilot-server-proof-tests.js +63 -168
  77. package/dist/scripts/booking-copilot-startup-proof-tests.js +38 -124
  78. package/dist/scripts/booking-executor-tests.js +80 -0
  79. package/dist/scripts/booking-recovery-chain-tests.js +182 -0
  80. package/dist/scripts/booking-saga-tests.js +61 -123
  81. package/dist/scripts/booking-surface-contract-proof-tests.js +152 -1167
  82. package/dist/scripts/bootstrap-tests.js +1383 -128
  83. package/dist/scripts/build-changelog.js +89 -130
  84. package/dist/scripts/build-metrics-report.js +135 -143
  85. package/dist/scripts/changelog-tests.js +35 -82
  86. package/dist/scripts/channel-probe-tests.js +44 -116
  87. package/dist/scripts/channel-probe.js +84 -167
  88. package/dist/scripts/channel-registry-tests.js +97 -185
  89. package/dist/scripts/companion-tests.js +25 -70
  90. package/dist/scripts/dida-cookie-probe.js +87 -0
  91. package/dist/scripts/dida-sw-probe.js +114 -0
  92. package/dist/scripts/diff-test.js +17 -12
  93. package/dist/scripts/doctor-tests.js +428 -120
  94. package/dist/scripts/dsh-artifact-e2e.js +150 -0
  95. package/dist/scripts/dsh-artifact-web-e2e.js +443 -0
  96. package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
  97. package/dist/scripts/dsh-runtime-closure-tests.js +110 -153
  98. package/dist/scripts/dsh-runtime-closure.js +75 -63
  99. package/dist/scripts/dsh-session-v3-migration-proof.js +106 -0
  100. package/dist/scripts/dsh-subprocess-local-proof.js +149 -0
  101. package/dist/scripts/dsh-target-closure-proof.js +178 -0
  102. package/dist/scripts/effect-tests.js +165 -552
  103. package/dist/scripts/engine-run.js +1 -3
  104. package/dist/scripts/engine-tests.js +16 -10
  105. package/dist/scripts/evaluation-cadence-tests.js +69 -242
  106. package/dist/scripts/evaluation-contract-tests.js +193 -499
  107. package/dist/scripts/extension-distribution-cli.js +19 -15
  108. package/dist/scripts/extension-distribution-tests.js +98 -211
  109. package/dist/scripts/extension-tests.js +366 -389
  110. package/dist/scripts/fact-gate-tests.js +725 -263
  111. package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
  112. package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
  113. package/dist/scripts/flight-malformed-tests.js +159 -0
  114. package/dist/scripts/flyai-tests.js +325 -68
  115. package/dist/scripts/g5-guard-tests.js +131 -0
  116. package/dist/scripts/g5-guard.js +189 -0
  117. package/dist/scripts/gen-lunar-anchors.js +106 -0
  118. package/dist/scripts/gotry-backend-kernel-tests.js +79 -0
  119. package/dist/scripts/ground-transfer-tests.js +956 -0
  120. package/dist/scripts/hbcli-e2e-tests.js +71 -90
  121. package/dist/scripts/hbcli-live-optin-tests.js +96 -0
  122. package/dist/scripts/hbcli-release-contract-tests.js +122 -0
  123. package/dist/scripts/hbcli-tests.js +41 -115
  124. package/dist/scripts/health-watch-cli.js +42 -28
  125. package/dist/scripts/hotel-date-gate-tests.js +496 -0
  126. package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
  127. package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
  128. package/dist/scripts/i18n-tests.js +16 -9
  129. package/dist/scripts/incident-tests.js +281 -87
  130. package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
  131. package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
  132. package/dist/scripts/issue-271-liveness-tests.js +547 -0
  133. package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
  134. package/dist/scripts/issue-289-model-retry-tests.js +441 -0
  135. package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
  136. package/dist/scripts/issue-338-home-city-e2e.js +577 -0
  137. package/dist/scripts/issue343-real-entry-e2e.js +160 -0
  138. package/dist/scripts/journey-tests.js +22 -27
  139. package/dist/scripts/kernel-manifest-evidence.js +109 -0
  140. package/dist/scripts/kernel-manifest-gate.js +126 -0
  141. package/dist/scripts/kernel-manifest-tests.js +212 -0
  142. package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
  143. package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
  144. package/dist/scripts/kernel-manifest.js +369 -0
  145. package/dist/scripts/ledger-repair-apply-tests.js +316 -0
  146. package/dist/scripts/ledger-repair-plan-tests.js +375 -0
  147. package/dist/scripts/ledger-tests.js +325 -340
  148. package/dist/scripts/ledger-workflow-crash.js +10 -20
  149. package/dist/scripts/map-tools-vendor-package-proof.js +206 -134
  150. package/dist/scripts/memory-capture-tests.js +242 -61
  151. package/dist/scripts/memory-decay-tests.js +18 -52
  152. package/dist/scripts/memory-lifecycle-tests.js +657 -0
  153. package/dist/scripts/memory-lifecycle.js +196 -0
  154. package/dist/scripts/memory-metrics.js +24 -12
  155. package/dist/scripts/memory-value-report.js +474 -222
  156. package/dist/scripts/memory-value-tests.js +328 -0
  157. package/dist/scripts/metrics-report-tests.js +62 -199
  158. package/dist/scripts/model-override-e2e.js +66 -105
  159. package/dist/scripts/nightly-evidence-tests.js +64 -89
  160. package/dist/scripts/nightly-evidence.js +108 -111
  161. package/dist/scripts/nudge-digest.js +39 -28
  162. package/dist/scripts/onboarding-tests.js +63 -88
  163. package/dist/scripts/opensky-check.js +18 -13
  164. package/dist/scripts/opensky-tests.js +28 -28
  165. package/dist/scripts/persona-surface-guard-tests.js +37 -12
  166. package/dist/scripts/pnpm-dsh-closure-proof.js +2 -5
  167. package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
  168. package/dist/scripts/price-drift-tests.js +126 -227
  169. package/dist/scripts/price-drift-watch.js +209 -253
  170. package/dist/scripts/probe-poi-tests.js +52 -45
  171. package/dist/scripts/product-metrics.js +187 -273
  172. package/dist/scripts/publish-preverify.js +47 -41
  173. package/dist/scripts/realtime-pricing-tests.js +37 -76
  174. package/dist/scripts/replay-async.js +20 -18
  175. package/dist/scripts/replay-real.js +16 -18
  176. package/dist/scripts/replay.js +52 -82
  177. package/dist/scripts/session-attach-diagnose.js +16 -12
  178. package/dist/scripts/session-attach-poc.js +34 -37
  179. package/dist/scripts/session-benchmark.js +66 -147
  180. package/dist/scripts/session-dida-live-e2e.js +151 -0
  181. package/dist/scripts/session-extract-tests.js +14 -35
  182. package/dist/scripts/session-login.js +14 -13
  183. package/dist/scripts/session-tests.js +545 -583
  184. package/dist/scripts/sf-live-benchmark.js +92 -110
  185. package/dist/scripts/sf-live-cli-tests.js +2 -9
  186. package/dist/scripts/sf-soft-score-tests.js +19 -60
  187. package/dist/scripts/sf-summary-tests.js +149 -0
  188. package/dist/scripts/sf-summary.js +466 -84
  189. package/dist/scripts/skeleton-check.js +16 -14
  190. package/dist/scripts/skeleton-integration-test.js +11 -10
  191. package/dist/scripts/skills-contract-tests.js +51 -46
  192. package/dist/scripts/smoke-session-gate-tests.js +12 -20
  193. package/dist/scripts/smoke.js +541 -422
  194. package/dist/scripts/sponsor-reuse-tests.js +262 -0
  195. package/dist/scripts/state-cli-tests.js +226 -101
  196. package/dist/scripts/state-cli.js +549 -212
  197. package/dist/scripts/static-golden-tests.js +46 -180
  198. package/dist/scripts/time-eval-tests.js +210 -192
  199. package/dist/scripts/travel-timeline-tests.js +29 -93
  200. package/dist/scripts/turn-handoff-collect-tests.js +46 -93
  201. package/dist/scripts/turn-handoff-collect.js +71 -99
  202. package/dist/scripts/turn-policy-tests.js +28 -29
  203. package/dist/scripts/typed-contract-canary.js +77 -162
  204. package/dist/scripts/tz-resolver-sanity.js +50 -0
  205. package/dist/scripts/tz-resolver-tests.js +584 -0
  206. package/dist/scripts/unified-tests.js +23 -19
  207. package/dist/scripts/visa-policy-tests.js +31 -44
  208. package/dist/scripts/weather-tests.js +121 -543
  209. package/dist/scripts/wish-channel-gate-tests.js +29 -86
  210. package/dist/scripts/write-gate-tests.js +522 -0
  211. package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
  212. package/dist/scripts/z3-lifecycle-tests.js +170 -0
  213. package/dist/scripts/z3-race-repeat-tests.js +39 -0
  214. package/dist/scripts/z3-race-tests.js +18 -12
  215. package/dist/src/artifact-gate.js +652 -221
  216. package/dist/src/backend/kernel.js +91 -0
  217. package/dist/src/backend/modules/booking-copilot.js +55 -0
  218. package/dist/src/backend/modules/booking-executor.js +124 -0
  219. package/dist/src/backend/modules/session-search.js +194 -0
  220. package/dist/src/benchmark-agent-conformance.js +296 -262
  221. package/dist/src/benchmark-environment-bridge.js +298 -413
  222. package/dist/src/benchmark-headless-child-diagnostics.js +93 -100
  223. package/dist/src/benchmark-tool-isolation.js +94 -67
  224. package/dist/src/bookable-facts.js +299 -174
  225. package/dist/src/booking-saga.js +64 -120
  226. package/dist/src/booking-surface/availability-policy.js +443 -576
  227. package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
  228. package/dist/src/booking-surface/contracts.js +24 -80
  229. package/dist/src/booking-surface/dsh-planner.js +510 -281
  230. package/dist/src/booking-surface/dsh-plugin.js +17 -1
  231. package/dist/src/booking-surface/error-codes.js +82 -10
  232. package/dist/src/booking-surface/index.js +0 -3
  233. package/dist/src/booking-surface/profile.js +10 -54
  234. package/dist/src/booking-surface/recovery-chain.js +367 -0
  235. package/dist/src/booking-surface/runtime.js +765 -891
  236. package/dist/src/booking-surface/server.js +200 -251
  237. package/dist/src/booking-surface/startup.js +58 -54
  238. package/dist/src/booking-surface/validation.js +150 -218
  239. package/dist/src/bridge.js +20 -13
  240. package/dist/src/companions.js +48 -74
  241. package/dist/src/contracts.js +21 -43
  242. package/dist/src/dsh-llm.js +82 -120
  243. package/dist/src/engine.js +253 -239
  244. package/dist/src/evaluation-cadence.js +72 -209
  245. package/dist/src/evaluation-contracts.js +220 -843
  246. package/dist/src/flight-pack-adapter.js +54 -0
  247. package/dist/src/flight-pack-contract.js +2 -0
  248. package/dist/src/gotry-backend.js +70 -0
  249. package/dist/src/hotel-date-gate.js +118 -0
  250. package/dist/src/i18n.js +18 -7
  251. package/dist/src/index.js +1425 -1629
  252. package/dist/src/journey.js +132 -81
  253. package/dist/src/ledger-repair-apply.js +538 -0
  254. package/dist/src/ledger-repair-plan.js +463 -0
  255. package/dist/src/loop.js +257 -166
  256. package/dist/src/memory-capture.js +180 -32
  257. package/dist/src/memory-decay.js +30 -14
  258. package/dist/src/memory-lifecycle.js +1310 -0
  259. package/dist/src/memory-utility.js +27 -40
  260. package/dist/src/mock-llm.js +36 -61
  261. package/dist/src/model.js +81 -36
  262. package/dist/src/realtime-pricing.js +63 -72
  263. package/dist/src/slot-spec.js +84 -106
  264. package/dist/src/state-ledger.js +310 -471
  265. package/dist/src/subagent-job-id-guard.js +58 -0
  266. package/dist/src/time-anchor.js +231 -33
  267. package/dist/src/tool-packet.js +20 -8
  268. package/dist/src/travel-slots.js +58 -35
  269. package/dist/src/travel-timeline.js +44 -63
  270. package/dist/src/turn-deadline.js +121 -127
  271. package/dist/src/turn-policy.js +61 -89
  272. package/dist/src/tz-resolver.js +143 -0
  273. package/dist/src/unified.js +504 -381
  274. package/dist/src/wish-pool.js +39 -14
  275. package/dist/src/write-gate.js +495 -0
  276. package/dist/src/z3-shared.js +273 -7
  277. package/extension/README.md +4 -3
  278. package/extension/background.js +125 -14
  279. package/extension/content-bridge.js +23 -1
  280. package/extension/content-main.js +15 -4
  281. package/extension/manifest.json +16 -8
  282. package/package.json +251 -220
  283. package/ts/capabilities/agent-reach.ts +4 -4
  284. package/ts/capabilities/anything.ts +1 -1
  285. package/ts/capabilities/artifacts.ts +46 -9
  286. package/ts/capabilities/flyai.ts +156 -37
  287. package/ts/capabilities/hbcli.ts +1 -1
  288. package/ts/capabilities/incident-log.ts +88 -59
  289. package/ts/capabilities/session/adapters/ctrip-flight.ts +99 -36
  290. package/ts/capabilities/session/adapters/dida-portal.ts +250 -0
  291. package/ts/capabilities/session/adapters/rail-12306.ts +123 -24
  292. package/ts/capabilities/session/extension-bridge.ts +226 -101
  293. package/ts/capabilities/session/extension-channel.ts +22 -9
  294. package/ts/capabilities/session/wizard.ts +2 -2
  295. package/ts/capabilities/session-consent.ts +98 -14
  296. package/ts/capabilities/session-login.ts +1 -0
  297. package/ts/capabilities/session-search.ts +315 -31
  298. package/ts/dsh-runtime/vendor/README.md +8 -4
  299. package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +7 -1
  300. package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +5 -3
  301. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +1 -1
  302. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +1 -1
  303. package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +6 -6
  304. package/ts/package.json +236 -16
  305. package/ts/scripts/map-tools-vendor-package-proof.ts +152 -31
  306. package/ts/scripts/state-cli.ts +418 -51
  307. package/ts/src/artifact-gate.ts +561 -15
  308. package/ts/src/benchmark-environment-bridge.ts +37 -20
  309. package/ts/src/bookable-facts.ts +183 -7
  310. package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
  311. package/ts/src/booking-surface/dsh-planner.ts +240 -32
  312. package/ts/src/booking-surface/dsh-plugin.js +17 -1
  313. package/ts/src/booking-surface/error-codes.ts +75 -0
  314. package/ts/src/booking-surface/recovery-chain.ts +409 -0
  315. package/ts/src/booking-surface/server.ts +84 -51
  316. package/ts/src/booking-surface/validation.ts +2 -0
  317. package/ts/src/contracts.ts +12 -2
  318. package/ts/src/dsh-llm.ts +4 -8
  319. package/ts/src/index.ts +609 -90
  320. package/ts/src/loop.ts +116 -7
  321. package/ts/src/memory-capture.ts +227 -3
  322. package/ts/src/mock-llm.ts +4 -8
  323. package/ts/src/model.ts +88 -2
  324. package/ts/src/state-ledger.ts +47 -29
  325. package/ts/src/subagent-job-id-guard.ts +72 -0
  326. package/ts/src/time-anchor.ts +217 -13
  327. 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) { sendJson(res, 409, { error: { code: normalizeBookingErrorCode(error) } }); return }
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(async (req, res) => {
370
- const isProbe = req.method === 'GET' && (req.url === '/healthz' || req.url === '/status')
371
- if (!isProbe && (req.method !== 'POST' || req.url !== '/a2a/booking-copilot/turn')) {
372
- sendJson(res, 404, { error: { code: 'not_found' } })
373
- return
374
- }
375
- const auth = String(req.headers.authorization ?? '')
376
- if (!safeSecretEqual(auth, `Bearer ${options.apiKey}`)) {
377
- sendJson(res, 401, { error: { code: 'unauthorized' } })
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)
@@ -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 WorkWindowProfile {
20
- homeTzOffsetMin: number
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": 数字, "startMin": 数字, "endMin": 数字, "workdays": [0,1,2,3,4], "evidence": "用户原话"},
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 换算;UTC+4 homeTzOffsetMin=240。
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.workWindow = state.profile.workWindow ? {
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
  },