@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
@@ -1,168 +1,181 @@
1
+ /**
2
+ * hotelbyte-cli(hbcli)能力层封装:进程内 spawn bun + hbcli --json。
3
+ *
4
+ * 拿到外部资源信息能力的三条路径:
5
+ * 1. 实时 hbcli 调用(能力强,延迟 1-3s,需凭证 + 证书有效)
6
+ * 2. 静态数据包(data/hotels_2026.json)——证据标注 [静态包:估算]
7
+ * 3. 简易降级返回(什么都不查得到时,提供"工具暂不可用"而不是抛错)
8
+ *
9
+ * 证书/凭证/网络问题一律降级,不抛到调用方——能力层契约:永远返回一种结果。
10
+ *
11
+ * §7-1/§7-2 L4 不变量:证据链标注([实时API:hbcli@<ts>] / [静态包:估算]);
12
+ * 估算必须显式标记。这是 L4 与 L1 透明卡片的接缝。
13
+ */
1
14
  import { spawn } from 'node:child_process';
2
15
  import { homedir } from 'node:os';
3
16
  import { join } from 'node:path';
17
+ /**
18
+ * hbcli 二进制候选路径(gotry setup 按官方脚本装到 ~/.local/bin/hbcli,
19
+ * symlink 指向 ~/.staicli/current/hbcli——当 PATH 不含 ~/.local/bin 时裸名
20
+ * spawn 仍 ENOENT,按已知安装位回退)。仅对默认名 'hbcli' 扩展;显式自定义
21
+ * 名(如测试注入的不存在路径)不扩展,保持配置即所用的可测性。
22
+ */
4
23
  export function hbcliBinCandidates(bin, homeDir = homedir()) {
5
- if (bin !== 'hbcli') return [
6
- bin
7
- ];
8
- return [
9
- bin,
10
- join(homeDir, '.local/bin/hbcli'),
11
- join(homeDir, '.staicli/current/hbcli')
12
- ];
24
+ if (bin !== 'hbcli')
25
+ return [bin];
26
+ return [bin, join(homeDir, '.local/bin/hbcli'), join(homeDir, '.staicli/current/hbcli')];
13
27
  }
28
+ /** 单个候选的一次 spawn 封装:失败不抛,返回降级结果(spawnError 标记 ENOENT 类失败供上层换候选) */
14
29
  function attemptHbcli(bin, args, opts) {
15
30
  const started = Date.now();
16
- return new Promise((resolve)=>{
31
+ return new Promise((resolve) => {
17
32
  let stdout = '';
18
33
  let stderr = '';
19
34
  let settled = false;
20
- const child = spawn(bin, args, {
21
- env: {
22
- ...process.env,
23
- ...opts.envVars
24
- }
25
- });
26
- const timer = setTimeout(()=>{
35
+ const child = spawn(bin, args, { env: { ...process.env, ...opts.envVars } });
36
+ const timer = setTimeout(() => {
27
37
  if (!settled) {
28
38
  settled = true;
29
39
  child.kill('SIGKILL');
30
40
  resolve({
31
- via: 'hbcli-error',
32
- exitCode: -1,
33
- result: null,
41
+ via: 'hbcli-error', exitCode: -1, result: null,
34
42
  evidence: `[实时API:hbcli@timeout@${new Date().toISOString()}]`,
35
- latencyMs: Date.now() - started,
36
- error: `timeout after ${opts.timeoutMs}ms`
43
+ latencyMs: Date.now() - started, error: `timeout after ${opts.timeoutMs}ms`,
37
44
  });
38
45
  }
39
46
  }, opts.timeoutMs);
40
- child.stdout.on('data', (d)=>{
41
- stdout += d.toString();
42
- });
43
- child.stderr.on('data', (d)=>{
44
- stderr += d.toString();
45
- });
46
- child.on('close', (code)=>{
47
- if (settled) return;
47
+ child.stdout.on('data', (d) => { stdout += d.toString(); });
48
+ child.stderr.on('data', (d) => { stderr += d.toString(); });
49
+ child.on('close', (code) => {
50
+ if (settled)
51
+ return;
48
52
  settled = true;
49
53
  clearTimeout(timer);
50
54
  const latencyMs = Date.now() - started;
51
55
  if (code !== 0) {
56
+ // 不抛错,降级返回。证据链显式标注:实时 API 调用失败+原因+时间戳。
52
57
  resolve({
53
- via: 'hbcli-error',
54
- exitCode: code ?? -1,
55
- result: null,
58
+ via: 'hbcli-error', exitCode: code ?? -1, result: null,
56
59
  evidence: `[实时API:hbcli@error@${new Date().toISOString()}]`,
57
60
  stderr: stderr.slice(0, 2000),
58
- latencyMs,
59
- error: stderr.trim().slice(0, 200) || `exit ${code}`
61
+ latencyMs, error: stderr.trim().slice(0, 200) || `exit ${code}`,
60
62
  });
61
63
  return;
62
64
  }
65
+ // 尝试 JSON 解析
63
66
  const jStart = stdout.search(/[\{\[]/);
64
67
  const jsonStr = jStart >= 0 ? stdout.slice(jStart) : '';
65
68
  let result = null;
66
69
  if (jsonStr) {
67
70
  try {
68
71
  result = JSON.parse(jsonStr);
69
- } catch {}
72
+ }
73
+ catch { /* 非 JSON 输出,留给调用方处理 */ }
70
74
  }
71
75
  resolve({
72
- via: 'hbcli-realtime',
73
- exitCode: 0,
74
- result,
76
+ via: 'hbcli-realtime', exitCode: 0, result,
75
77
  evidence: `[实时API:hbcli@${new Date().toISOString()}]`,
76
- stdout: stdout.slice(0, 2000),
77
- latencyMs
78
+ stdout: stdout.slice(0, 2000), latencyMs,
78
79
  });
79
80
  });
80
- child.on('error', (e)=>{
81
- if (settled) return;
81
+ child.on('error', (e) => {
82
+ if (settled)
83
+ return;
82
84
  settled = true;
83
85
  clearTimeout(timer);
86
+ // ENOENT (二进制不存在) 等也走降级路径;spawnError 供上层按候选路径重试
84
87
  resolve({
85
- via: 'hbcli-error',
86
- exitCode: -1,
87
- result: null,
88
+ via: 'hbcli-error', exitCode: -1, result: null,
88
89
  evidence: `[实时API:hbcli@spawn_error@${new Date().toISOString()}]`,
89
- latencyMs: Date.now() - started,
90
- error: e.message,
91
- spawnError: true
90
+ latencyMs: Date.now() - started, error: e.message, spawnError: true,
92
91
  });
93
92
  });
94
93
  });
95
94
  }
95
+ /** 通用 hbcli JSON 调用封装:失败不抛,而是返回降级结果 */
96
96
  export async function callHbcliJson(args, opts = {}) {
97
97
  const env = opts.env ?? 'uat';
98
- const envVars = {
99
- HOTELBYTE_ENV: env
100
- };
101
- if (opts.token) envVars['HOTELBYTE_TOKEN'] = opts.token;
102
- const callOpts = {
103
- timeoutMs: opts.timeoutMs ?? 15_000,
104
- env,
105
- envVars
106
- };
98
+ const envVars = { HOTELBYTE_ENV: env };
99
+ if (opts.token)
100
+ envVars['HOTELBYTE_TOKEN'] = opts.token;
101
+ const callOpts = { timeoutMs: opts.timeoutMs ?? 15_000, env, envVars };
107
102
  const candidates = hbcliBinCandidates(opts.hbcliBin ?? 'hbcli');
108
103
  let last;
109
- for (const bin of candidates){
104
+ for (const bin of candidates) {
110
105
  last = await attemptHbcli(bin, args, callOpts);
111
- if (!(last.spawnError && candidates.indexOf(bin) < candidates.length - 1)) return last;
106
+ // spawn 级失败(ENOENT )且还有候选 换下一个已知安装位;其余失败(退码/超时)无重试意义
107
+ if (!(last.spawnError && candidates.indexOf(bin) < candidates.length - 1))
108
+ return last;
112
109
  }
113
110
  return last;
114
111
  }
112
+ /** 上游 i18n 名称容错读取(zh 优先,中文优先产品口径;回退 en/ar) */
115
113
  function i18nName(v) {
116
- if (!v) return '';
117
- if (typeof v === 'string') return v;
114
+ if (!v)
115
+ return '';
116
+ if (typeof v === 'string')
117
+ return v;
118
118
  return v.zh || v.en || v.ar || '';
119
119
  }
120
+ /**
121
+ * 实时 hotelList 响应 → 模型可读紧凑行(id/名称/星级/最低价)。
122
+ * issue #195:render 面只透 summary,若 summary 不含数据,模型只看到
123
+ * 「实时返回」四个字——工具调用等于白调。id 必须在行内,hotel-rates 的入参
124
+ * 就来自这里。上游 wire:HotelListResp{list:[Hotel{id,name:{en,zh},star,minPrice:{amount,currency}}]}。
125
+ */
120
126
  function hotelLines(result, max = 8) {
121
127
  const list = result?.list;
122
- if (!Array.isArray(list) || list.length === 0) return '';
128
+ if (!Array.isArray(list) || list.length === 0)
129
+ return '';
123
130
  const lines = [];
124
- for (const item of list.slice(0, max)){
131
+ for (const item of list.slice(0, max)) {
125
132
  const h = item;
126
133
  const name = i18nName(h.name) || `hotel ${h.id ?? '?'}`;
127
134
  const star = typeof h.star === 'number' && h.star > 0 ? ` ${h.star}★` : '';
128
- const price = h.minPrice && typeof h.minPrice.amount === 'number' ? ` 低至 ${h.minPrice.amount}${h.minPrice.currency ? ` ${h.minPrice.currency}` : ''}` : '';
135
+ const price = h.minPrice && typeof h.minPrice.amount === 'number'
136
+ ? ` 低至 ${h.minPrice.amount}${h.minPrice.currency ? ` ${h.minPrice.currency}` : ''}`
137
+ : '';
129
138
  lines.push(`- id=${h.id ?? '?'} ${name}${star}${price}`);
130
139
  }
131
- if (list.length > max) lines.push(`…(共 ${list.length} 家,仅列前 ${max})`);
140
+ if (list.length > max)
141
+ lines.push(`…(共 ${list.length} 家,仅列前 ${max})`);
132
142
  return lines.join('\n');
133
143
  }
144
+ /** 高层语义化封装:酒店列表查询(down-tier to 静态包 + 证据链标注) */
134
145
  export async function searchHotels(query, opts = {}) {
135
- const hbArgs = [
136
- 'search',
137
- 'hotel-list',
138
- '--json',
139
- '--page-size',
140
- '10'
141
- ];
142
- if (query.destination) hbArgs.push('--destination-name', query.destination);
143
- if (query.checkIn) hbArgs.push('--check-in', query.checkIn);
144
- if (query.checkOut) hbArgs.push('--check-out', query.checkOut);
145
- if (query.adults) hbArgs.push('--room-occupancies', JSON.stringify([
146
- {
147
- adultCount: query.adults,
148
- childrenAges: []
149
- }
150
- ]));
146
+ // 旗标对齐上游 CLI(hotelbyte-com/hotelbyte-cli):--destination-name + --room-occupancies;
147
+ // hotel-list 支持日期旗标(--check-in/--check-out,上游帮助文档在册)——issue #195 教训:
148
+ // 「日期不传上游」会让实时价退化为当前窗口价,用户要下周五的房给的是今天的价。
149
+ const hbArgs = ['search', 'hotel-list', '--json', '--page-size', '10'];
150
+ if (query.destination)
151
+ hbArgs.push('--destination-name', query.destination);
152
+ if (query.checkIn)
153
+ hbArgs.push('--check-in', query.checkIn);
154
+ if (query.checkOut)
155
+ hbArgs.push('--check-out', query.checkOut);
156
+ if (query.adults)
157
+ hbArgs.push('--room-occupancies', JSON.stringify([{ adultCount: query.adults, childrenAges: [] }]));
151
158
  const live = await callHbcliJson(hbArgs, opts);
152
159
  if (live.via === 'hbcli-realtime') {
160
+ // summary 即模型面(render 只透 summary):必须自带紧凑数据行 + 证据链,
161
+ // 否则模型只看到「实时返回」占位一句话(issue #195 实锤)。
153
162
  const list = live.result?.list;
154
163
  const n = Array.isArray(list) ? list.length : 0;
155
- const dateTag = query.checkIn ? `入住 ${query.checkIn}${query.checkOut ? ` → 退房 ${query.checkOut}` : ''}` : '未传日期(上游按当前窗口价)';
164
+ const dateTag = query.checkIn
165
+ ? `入住 ${query.checkIn}${query.checkOut ? ` → 退房 ${query.checkOut}` : ''}`
166
+ : '未传日期(上游按当前窗口价)';
156
167
  const lines = hotelLines(live.result);
157
168
  const summary = `${query.destination}:hbcli 实时 ${n} 家(${dateTag})${lines ? `\n${lines}` : ''}\n${live.evidence}`;
158
- return {
159
- ...live,
160
- hotels: live.result,
161
- summary
162
- };
169
+ return { ...live, hotels: live.result, summary };
163
170
  }
171
+ // 降级原因人话化(issue #24):hbcli 未安装时按 gotry setup 指引(npm 安装期已
172
+ // 自动跑过官方脚本;PATH 未含 ~/.local/bin 时上方候选路径也已兜住),
173
+ // 裸 "spawn hbcli ENOENT" 读起来像工具坏了——实际静态包降级是设计行为
164
174
  const rawReason = live.error ?? live.via;
165
- const reason = /ENOENT/i.test(rawReason) ? '未安装 hbcli(可选实时源;npx gotry setup 可按官方脚本安装)' : rawReason;
175
+ const reason = /ENOENT/i.test(rawReason) ? '未安装 hbcli(可选实时源;npx @danceiny/gotry setup 可按官方脚本安装)' : rawReason;
176
+ // 降级:读静态包,按目的地过滤命中的住宿块(issue #24)——整包倾倒会把无关场景
177
+ // (深圳/普吉/曼谷/云南/大理混装)灌给模型且不指明哪块相关;包内无该目的地时明示
178
+ // 「无数据」而不是伪装成可用结果。
166
179
  const fallback = opts.fallbackPath;
167
180
  if (fallback) {
168
181
  try {
@@ -170,81 +183,64 @@ export async function searchHotels(query, opts = {}) {
170
183
  const pack = JSON.parse(await readFile(fallback, 'utf-8'));
171
184
  const kw = query.destination.trim();
172
185
  const stays = Array.isArray(pack.stays) ? pack.stays : [];
173
- const matched = kw ? stays.filter((s)=>JSON.stringify(s).includes(kw)) : stays;
186
+ const matched = kw ? stays.filter(s => JSON.stringify(s).includes(kw)) : stays;
174
187
  if (matched.length) {
175
188
  return {
176
189
  ...live,
177
- hotels: {
178
- stays: matched
179
- },
180
- summary: `${query.destination}:hbcli 实时源不可用(${reason}),已降级到静态包(公开渠道估算,非实时),命中 ${matched.length} 个住宿块:\n${JSON.stringify(matched).slice(0, 1600)}`
190
+ hotels: { stays: matched },
191
+ // summary 即模型面:静态块内容(note/options/recommendation)直接内联,
192
+ // 模型不必再猜「命中 N 块」里有什么(issue #195)
193
+ summary: `${query.destination}:hbcli 实时源不可用(${reason}),已降级到静态包(公开渠道估算,非实时),命中 ${matched.length} 个住宿块:\n${JSON.stringify(matched).slice(0, 1600)}`,
181
194
  };
182
195
  }
183
196
  return {
184
197
  ...live,
185
198
  hotels: null,
186
- summary: `${query.destination}:hbcli 实时源不可用(${reason}),且静态包无「${query.destination}」住宿数据(静态包仅覆盖内置场景)`
199
+ summary: `${query.destination}:hbcli 实时源不可用(${reason}),且静态包无「${query.destination}」住宿数据(静态包仅覆盖内置场景)`,
187
200
  };
188
- } catch {}
201
+ }
202
+ catch { /* 静态包读不到也优雅降级 */ }
189
203
  }
190
- return {
191
- ...live,
192
- summary: `${query.destination}:hbcli 实时源不可用(${reason})且无静态包(仅返回错误)`
193
- };
204
+ return { ...live, summary: `${query.destination}:hbcli 实时源不可用(${reason})且无静态包(仅返回错误)` };
194
205
  }
206
+ /** 高层语义化封装:目的地列表(无数据依赖,通常 hbcli dest 命令可独立调通) */
195
207
  export async function listDestinations(opts = {}) {
196
- const r = await callHbcliJson([
197
- 'search',
198
- 'destinations',
199
- '--json'
200
- ], opts);
201
- return {
202
- ...r,
203
- destinations: r.result
204
- };
208
+ const r = await callHbcliJson(['search', 'destinations', '--json'], opts);
209
+ return { ...r, destinations: r.result };
205
210
  }
211
+ /**
212
+ * 高层语义化封装:房型报价(hotel-rates;上游 hotelRates 创建后端 session,
213
+ * 产出带 RatePkgId 的房型列表——check-avail 与 book 的入参来源)。
214
+ *
215
+ * 价格面无静态降级(fail-closed):hotel-list 的静态包降级是定性住宿信息,
216
+ * 房价是钱——估算价格冒充实时价是红线;不可用就诚实失败,证据链标注,
217
+ * 与 bookable-facts 证据分级(live_inventory 才可进确认卡)同口径。
218
+ */
206
219
  export async function hotelRates(query, opts = {}) {
207
- const hbArgs = [
208
- 'search',
209
- 'hotel-rates',
210
- '--json',
211
- '--hotel-id',
212
- query.hotelId
213
- ];
214
- if (query.checkIn) hbArgs.push('--check-in', query.checkIn);
215
- if (query.checkOut) hbArgs.push('--check-out', query.checkOut);
216
- if (query.countryCode) hbArgs.push('--country-code', query.countryCode);
217
- if (query.nationalityCode) hbArgs.push('--nationality-code', query.nationalityCode);
218
- if (query.residencyCode) hbArgs.push('--residency-code', query.residencyCode);
219
- if (query.adults) hbArgs.push('--room-occupancies', JSON.stringify([
220
- {
221
- adultCount: query.adults,
222
- childrenAges: []
223
- }
224
- ]));
220
+ const hbArgs = ['search', 'hotel-rates', '--json', '--hotel-id', query.hotelId];
221
+ if (query.checkIn)
222
+ hbArgs.push('--check-in', query.checkIn);
223
+ if (query.checkOut)
224
+ hbArgs.push('--check-out', query.checkOut);
225
+ if (query.countryCode)
226
+ hbArgs.push('--country-code', query.countryCode);
227
+ if (query.nationalityCode)
228
+ hbArgs.push('--nationality-code', query.nationalityCode);
229
+ if (query.residencyCode)
230
+ hbArgs.push('--residency-code', query.residencyCode);
231
+ if (query.adults)
232
+ hbArgs.push('--room-occupancies', JSON.stringify([{ adultCount: query.adults, childrenAges: [] }]));
225
233
  const live = await callHbcliJson(hbArgs, opts);
226
- const summary = live.via === 'hbcli-realtime' ? `hotel ${query.hotelId}:hbcli 实时报价${query.checkIn ? `(${query.checkIn} → ${query.checkOut ?? '?'})` : ''}` : `hotel ${query.hotelId}:hbcli 实时报价不可用(${live.error ?? live.via});价格面无静态降级,fail-closed 不估算`;
227
- return {
228
- ...live,
229
- rates: live.result,
230
- summary
231
- };
234
+ const summary = live.via === 'hbcli-realtime'
235
+ ? `hotel ${query.hotelId}:hbcli 实时报价${query.checkIn ? `(${query.checkIn} → ${query.checkOut ?? '?'})` : ''}`
236
+ : `hotel ${query.hotelId}:hbcli 实时报价不可用(${live.error ?? live.via});价格面无静态降级,fail-closed 不估算`;
237
+ return { ...live, rates: live.result, summary };
232
238
  }
239
+ /** 高层语义化封装:下单前实时验价(check-avail;入参 RatePkgId 来自 hotel-rates 产物)。价格面同上:不可用即诚实失败。 */
233
240
  export async function checkAvail(query, opts = {}) {
234
- const live = await callHbcliJson([
235
- 'search',
236
- 'check-avail',
237
- '--json',
238
- '--rate-pkg-id',
239
- query.ratePkgId
240
- ], opts);
241
- const summary = live.via === 'hbcli-realtime' ? `ratePkg ${query.ratePkgId}:hbcli 实时验价(库存与价格以后端为准)` : `ratePkg ${query.ratePkgId}:hbcli 实时验价不可用(${live.error ?? live.via});价格面无静态降级,fail-closed`;
242
- return {
243
- ...live,
244
- avail: live.result,
245
- summary
246
- };
241
+ const live = await callHbcliJson(['search', 'check-avail', '--json', '--rate-pkg-id', query.ratePkgId], opts);
242
+ const summary = live.via === 'hbcli-realtime'
243
+ ? `ratePkg ${query.ratePkgId}:hbcli 实时验价(库存与价格以后端为准)`
244
+ : `ratePkg ${query.ratePkgId}:hbcli 实时验价不可用(${live.error ?? live.via});价格面无静态降级,fail-closed`;
245
+ return { ...live, avail: live.result, summary };
247
246
  }
248
-
249
-
250
- //# sourceURL=ts/capabilities/hbcli.ts
@@ -0,0 +1,194 @@
1
+ /**
2
+ * HotelByte 交易 bridge 与 unknown 查单对账契约层(issue #232 契约/模块切片)。
3
+ *
4
+ * ⚠ 非运行时激活:本模块是纯函数契约层,不 spawn 子进程、不发起网络请求、
5
+ * 不读取任何凭据,与真实 HotelByte 二进制/UAT 零接触(见 issue #318 的
6
+ * GOTRY_HBCLI_LIVE 显式 opt-in 门——真实调用面在 M5 Entry 后才允许接线,
7
+ * 且必须挂在该门之后)。一切行为由 fixture/合成数据证明(hotelbyte-reconcile-tests)。
8
+ *
9
+ * 词汇与边界(上游文档为准):
10
+ * - docs/design/write-gate-production-design.md §3(SupplierOutcome/WriteEffectIntent/
11
+ * attempt_id/fencing)、§5.3(dispatch 未知态)、§6(准入矩阵)、§7(unknown 查单/恢复窗口);
12
+ * - docs/design/fact-writegate-seam.md(读侧 fact 与写侧 SupplierOutcome 两闸分离)。
13
+ *
14
+ * 与 #231(WriteGate outbox)的接口边界:本模块只消费 outbox intent 的**引用 ID 字符串**
15
+ * (`outboxIntentRef` 占位)与审批凭证摘要,不实现、不触碰 outbox/dispatch 持久化——
16
+ * attempt 领取、fencing token、账本事务由 #231 落地;两者以该 ID 关联(#231 未合前为占位)。
17
+ *
18
+ * 对账红线(全部结构性编码,见 advanceReconciliation):
19
+ * - unknown/无法确认的查单结果必须有显式对账分类——不静默当成功,也不静默丢弃;
20
+ * - CLI exit0 非成功证明:timeout/kill/坏响应/无绑定成功/部分确认一律 unknown;
21
+ * - 查单 miss 在恢复窗口内(后端 180s Phase1 + 最长 10min Phase2)保持 unknown,禁止重订;
22
+ * - 时间届满本身不是无订单证明——只转人工,不自动 reconciled_failed;
23
+ * - 只有绑定 attempt/customerReferenceNo 的权威终态负证据(供应商终态取消/失败、
24
+ * 绑定 attempt 的人工无订单确认)才允许新 intent;
25
+ * - 同 ref 多单/参考号不匹配 = 冲突,显式 manual_reconcile;
26
+ * - 终态后出现反证只置 conflict 交人工,不静默翻转;
27
+ * - 每次推进追加审计事件(带 probeDigest),记录不可变(pure function)。
28
+ */
29
+ import { createHash } from 'node:crypto';
30
+ /** HotelByte 后端恢复窗口:Phase1 180s + Phase2 最长 10min(600s);上界 780s。 */
31
+ export const RECOVERY_WINDOW_PHASE1_SECONDS = 180;
32
+ export const RECOVERY_WINDOW_PHASE2_MAX_SECONDS = 600;
33
+ export const RECOVERY_WINDOW_TOTAL_MAX_SECONDS = RECOVERY_WINDOW_PHASE1_SECONDS + RECOVERY_WINDOW_PHASE2_MAX_SECONDS;
34
+ function requireBinding(key, value) {
35
+ const v = (value ?? '').trim();
36
+ if (!v)
37
+ throw new Error(`hotelbyte-reconcile: 对账记录缺绑定键 ${key}(fail-closed;无绑定键即无法追溯)`);
38
+ return v;
39
+ }
40
+ function probeDigest(input) {
41
+ return createHash('sha256').update(JSON.stringify(input ?? null)).digest('hex');
42
+ }
43
+ /** 恢复窗口上界:enterUnknownAt + 780s(180s Phase1 + 最长 10min Phase2)。 */
44
+ export function recoveryWindowEndsAt(enteredUnknownAt) {
45
+ return new Date(Date.parse(enteredUnknownAt) + RECOVERY_WINDOW_TOTAL_MAX_SECONDS * 1000).toISOString();
46
+ }
47
+ /**
48
+ * 进入 unknown:booking 结果不可判定(超时/被杀/坏响应/部分确认)即建对账记录。
49
+ * 绑定键缺失直接抛错——没有绑定键的对账是不可追溯的黑洞,红线拒绝。
50
+ */
51
+ export function enterUnknown(seed) {
52
+ const attemptId = requireBinding('attemptId', seed.attemptId);
53
+ const customerReferenceNo = requireBinding('customerReferenceNo', seed.customerReferenceNo);
54
+ const intentIdemKey = requireBinding('intentIdemKey', seed.intentIdemKey);
55
+ const requestFingerprintSha256 = requireBinding('requestFingerprintSha256', seed.requestFingerprintSha256);
56
+ const recoveryWindowEndsAtV = requireBinding('recoveryWindowEndsAt', seed.recoveryWindowEndsAt);
57
+ const enteredUnknownAt = requireBinding('enteredUnknownAt', seed.enteredUnknownAt);
58
+ if (Number.isNaN(Date.parse(enteredUnknownAt)) || Number.isNaN(Date.parse(recoveryWindowEndsAtV))) {
59
+ throw new Error('hotelbyte-reconcile: enteredUnknownAt/recoveryWindowEndsAt 必须是可解析时间');
60
+ }
61
+ return {
62
+ attemptId,
63
+ customerReferenceNo,
64
+ intentIdemKey,
65
+ requestFingerprintSha256,
66
+ enteredUnknownAt,
67
+ recoveryWindowEndsAt: recoveryWindowEndsAtV,
68
+ factId: seed.factId,
69
+ approvalReceiptDigest: seed.approvalReceiptDigest,
70
+ outboxIntentRef: seed.outboxIntentRef,
71
+ status: 'unknown',
72
+ nextAction: 'keep_querying',
73
+ conflict: false,
74
+ history: [{
75
+ at: enteredUnknownAt,
76
+ action: 'enter_unknown',
77
+ from: 'unknown',
78
+ to: 'unknown',
79
+ reason: 'booking 结果不可判定,进入显式对账(禁止盲目重试/重订)',
80
+ probeDigest: probeDigest(seed),
81
+ }],
82
+ };
83
+ }
84
+ function parseBookPayload(stdout) {
85
+ const start = stdout.search(/[{[]/);
86
+ if (start < 0)
87
+ return { json: null, parseError: 'no-json-marker' };
88
+ try {
89
+ const parsed = JSON.parse(stdout.slice(start));
90
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
91
+ return { json: null, parseError: 'non-object-json' };
92
+ }
93
+ return { json: parsed };
94
+ }
95
+ catch (e) {
96
+ return { json: null, parseError: `unparseable:${e.message.slice(0, 80)}` };
97
+ }
98
+ }
99
+ /**
100
+ * book 执行分类(验收 §3:CLI exit0 非成功证明;timeout/kill/坏响应/结果不确定归 unknown):
101
+ * - timedOut / signal / 非零退码且无显式业务结论 → unknown;
102
+ * - exit0 + 非 JSON → unknown;exit0 + 显式业务失败 → supplier_failed;
103
+ * - exit0 + success 但缺/错 customerReferenceNo 绑定 → unknown(不可归因即不可当成功);
104
+ * - exit0 + pending/部分确认 → unknown;
105
+ * - exit0 + success + 参考号匹配本 attempt → binding_verified。
106
+ */
107
+ export function classifyBookExecution(r, expectedReferenceNo) {
108
+ if (r.timedOut)
109
+ return { kind: 'unknown', reason: 'timeout(30s abort;后端可能仍在处理,进入 unknown 对账)' };
110
+ if (r.signal)
111
+ return { kind: 'unknown', reason: `process_killed:${r.signal}(副作用不可判定)` };
112
+ const { json, parseError } = parseBookPayload(r.stdout);
113
+ if (r.exitCode !== 0) {
114
+ if (json?.status === 'failed')
115
+ return { kind: 'supplier_failed', reason: 'nonzero-exit + explicit failed payload', payload: json };
116
+ return { kind: 'unknown', reason: `nonzero_exit:${r.exitCode ?? 'null'}(无显式业务结论,不假定无副作用${parseError ? `;${parseError}` : ''})` };
117
+ }
118
+ if (!json)
119
+ return { kind: 'unknown', reason: `exit0_non_json(坏响应,不可当成功;${parseError ?? 'empty'})` };
120
+ if (json.status === 'failed')
121
+ return { kind: 'supplier_failed', reason: 'explicit failed payload', payload: json };
122
+ if (json.status === 'success') {
123
+ const ref = typeof json.customerReferenceNo === 'string' ? json.customerReferenceNo.trim() : '';
124
+ if (!ref)
125
+ return { kind: 'unknown', reason: 'exit0_success_missing_reference(无绑定成功不可归因)' };
126
+ if (ref !== expectedReferenceNo)
127
+ return { kind: 'unknown', reason: `exit0_success_reference_mismatch(返回 ${ref},本 attempt ${expectedReferenceNo})` };
128
+ return { kind: 'binding_verified', customerReferenceNo: ref, payload: json };
129
+ }
130
+ // pending / 其它一切不确定状态:部分确认 ≠ 成功
131
+ return { kind: 'unknown', reason: `partial_or_indeterminate(status=${String(json.status ?? 'absent')})` };
132
+ }
133
+ /** 供应商终态取消/失败词汇(权威负证据来源;迟到自动取消按回执收敛)。 */
134
+ const SUPPLIER_TERMINAL_NEGATIVE = new Set(['cancelled', 'canceled', 'auto_canceled', 'failed', 'expired']);
135
+ /**
136
+ * 对账状态机推进(纯函数;原状态不可变,返回带追加审计事件的新状态)。
137
+ * 转移表对齐 write-gate-production-design §7 矩阵,红线见模块头注释。
138
+ */
139
+ export function advanceReconciliation(state, probe, now) {
140
+ const digest = probeDigest(probe);
141
+ const append = (next, action, reason) => ({
142
+ ...next,
143
+ history: [...state.history, { at: now, action, from: state.status, to: next.status, reason, probeDigest: digest }],
144
+ });
145
+ // 终态吸收 + 反证只置 conflict(不静默翻转)
146
+ if (state.status !== 'unknown') {
147
+ if (probe.kind === 'hit' && probe.orders.length > 0) {
148
+ return append({ ...state, conflict: true, nextAction: 'manual_reconcile' }, 'conflict_late_hit_after_terminal', `终态(${state.status})后出现订单证据 → 显式冲突交人工复核,不静默翻转`);
149
+ }
150
+ return { ...state };
151
+ }
152
+ if (probe.kind === 'manual_no_order') {
153
+ const evidence = (probe.operatorEvidence ?? '').trim();
154
+ if (!evidence)
155
+ throw new Error('hotelbyte-reconcile: 人工无订单确认必须携带非空操作证据(绑定 attempt 可追溯)');
156
+ return append({ ...state, status: 'reconciled_failed', nextAction: 'terminal', conflict: false }, 'manual_no_order_confirmed', `人工权威负证据:${evidence.slice(0, 200)}`);
157
+ }
158
+ const withinWindow = Date.parse(now) <= Date.parse(state.recoveryWindowEndsAt);
159
+ switch (probe.kind) {
160
+ case 'miss': {
161
+ if (withinWindow) {
162
+ return append({ ...state, status: 'unknown', nextAction: 'keep_querying', conflict: false }, 'query_miss_within_window', '恢复窗口内查无 → 保持 unknown;供应商可能迟到建单,禁止重订');
163
+ }
164
+ return append({ ...state, status: 'unknown', nextAction: 'manual_reconcile', conflict: false }, 'query_miss_after_window', '窗口届满查无 → 仍 unknown(时间届满本身不是无订单证明),转人工');
165
+ }
166
+ case 'hit': {
167
+ if (probe.orders.length > 1) {
168
+ return append({ ...state, status: 'unknown', nextAction: 'manual_reconcile', conflict: true }, 'conflict_multiple_orders', `同参考号返回 ${probe.orders.length} 笔订单 → 冲突,人工对账(不静默择一)`);
169
+ }
170
+ const order = probe.orders[0];
171
+ if (order.customerReferenceNo !== state.customerReferenceNo) {
172
+ return append({ ...state, status: 'unknown', nextAction: 'manual_reconcile', conflict: true }, 'conflict_reference_mismatch', `返回订单参考号 ${order.customerReferenceNo} ≠ 本 attempt ${state.customerReferenceNo} → 冲突,人工对账`);
173
+ }
174
+ const status = order.supplierStatus.toLowerCase();
175
+ if (SUPPLIER_TERMINAL_NEGATIVE.has(status)) {
176
+ return append({ ...state, status: 'reconciled_failed', nextAction: 'terminal', conflict: false }, 'supplier_terminal_negative', `绑定 attempt 的供应商终态(${order.supplierStatus},orderRef=${order.orderRef})→ 权威负证据,收敛 reconciled_failed`);
177
+ }
178
+ if (status === 'confirmed' || status === 'success' || status === 'completed') {
179
+ return append({ ...state, status: 'reconciled_success', nextAction: 'terminal', conflict: false }, 'supplier_terminal_success', `绑定 attempt 的供应商终态成功(orderRef=${order.orderRef})→ reconciled_success(含迟到成功,不重订)`);
180
+ }
181
+ return append({ ...state, status: 'unknown', nextAction: 'keep_querying', conflict: false }, 'hit_not_terminal', `订单存在但非终态(${order.supplierStatus})→ 保持 unknown 继续查`);
182
+ }
183
+ case 'query_failed':
184
+ return append({ ...state, status: 'unknown', nextAction: 'keep_querying', conflict: false }, 'query_failed', `查询自身失败(${probe.reason.slice(0, 120)})→ 探针失败不证明订单不存在,保持 unknown`);
185
+ case 'malformed':
186
+ return append({ ...state, status: 'unknown', nextAction: 'keep_querying', conflict: false }, 'query_malformed', `畸形响应(${probe.reason.slice(0, 120)})→ 不静默当 miss,保持 unknown`);
187
+ case 'permission_denied':
188
+ return append({ ...state, status: 'unknown', nextAction: 'manual_reconcile', conflict: false }, 'query_permission_denied', '查询权限拒绝 → 自助查单不可用,转人工对账');
189
+ }
190
+ }
191
+ /** 是否允许对同一请求发起新 intent:仅当收敛自绑定 attempt 的权威终态负证据且无未决冲突。 */
192
+ export function newIntentAllowed(state) {
193
+ return state.status === 'reconciled_failed' && !state.conflict;
194
+ }