@danceiny/gotry 0.0.1-rc.9 → 0.2.0-rc.28

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 (533) hide show
  1. package/README.md +172 -219
  2. package/README.zh-CN.md +222 -0
  3. package/bin/gotry-backend.js +53 -0
  4. package/bin/gotry-booking-copilot.js +53 -0
  5. package/bin/gotry-bootstrap.js +1788 -0
  6. package/bin/gotry-inner.js +733 -83
  7. package/bin/gotry-process-liveness.d.ts +27 -0
  8. package/bin/gotry-process-liveness.js +97 -0
  9. package/bin/gotry-runtime-resolution.d.ts +27 -0
  10. package/bin/gotry-runtime-resolution.js +49 -0
  11. package/bin/gotry.js +1 -1
  12. package/client/client.js +203 -0
  13. package/cordis.gotry-patch.yml +80 -56
  14. package/data/airline-airports.json +39 -0
  15. package/data/stations-12306-verify.json +524 -0
  16. package/dist/capabilities/agent-reach-deep.js +45 -122
  17. package/dist/capabilities/agent-reach.js +101 -146
  18. package/dist/capabilities/anything.js +161 -94
  19. package/dist/capabilities/artifacts.js +370 -0
  20. package/dist/capabilities/channel-health.js +131 -0
  21. package/dist/capabilities/channel-registry.js +256 -0
  22. package/dist/capabilities/doctor.js +553 -0
  23. package/dist/capabilities/effect.js +394 -0
  24. package/dist/capabilities/external-event.js +149 -0
  25. package/dist/capabilities/fact-log.js +55 -0
  26. package/dist/capabilities/flyai-config.js +337 -0
  27. package/dist/capabilities/flyai.js +947 -110
  28. package/dist/capabilities/fx-contract.js +293 -0
  29. package/dist/capabilities/geo-atlas.js +246 -0
  30. package/dist/capabilities/ground-transfer.js +676 -0
  31. package/dist/capabilities/hbcli.js +260 -123
  32. package/dist/capabilities/hotelbyte-transaction.js +194 -0
  33. package/dist/capabilities/incident-log.js +131 -61
  34. package/dist/capabilities/itinerary-artifact.js +190 -0
  35. package/dist/capabilities/itinerary-deck-artifact.js +194 -0
  36. package/dist/capabilities/itinerary-deck-export.js +339 -0
  37. package/dist/capabilities/lavish-local.js +924 -0
  38. package/dist/capabilities/model-override.js +38 -0
  39. package/dist/capabilities/opensky.js +54 -43
  40. package/dist/capabilities/provider-detail-sanitize.js +64 -0
  41. package/dist/capabilities/resilience.js +191 -0
  42. package/dist/capabilities/route-provider-conformance.js +341 -0
  43. package/dist/capabilities/session/action-cache.js +50 -44
  44. package/dist/capabilities/session/adapters/ctrip-flight.js +120 -62
  45. package/dist/capabilities/session/adapters/ctrip-hotel.js +222 -0
  46. package/dist/capabilities/session/adapters/dida-portal.js +274 -0
  47. package/dist/capabilities/session/adapters/meituan-local.js +35 -54
  48. package/dist/capabilities/session/adapters/rail-12306.js +342 -0
  49. package/dist/capabilities/session/benchmark.js +189 -0
  50. package/dist/capabilities/session/bridge-ledger.js +224 -0
  51. package/dist/capabilities/session/extension-bridge.js +636 -0
  52. package/dist/capabilities/session/extension-channel.js +109 -0
  53. package/dist/capabilities/session/extension-distribution.js +230 -0
  54. package/dist/capabilities/session/extract.js +31 -21
  55. package/dist/capabilities/session/golden-score.js +114 -0
  56. package/dist/capabilities/session/health-watch.js +215 -0
  57. package/dist/capabilities/session/read-guard.js +75 -24
  58. package/dist/capabilities/session/static-flight-golden.js +150 -0
  59. package/dist/capabilities/session/transport.js +97 -83
  60. package/dist/capabilities/session/wizard.js +117 -0
  61. package/dist/capabilities/session-consent.js +178 -0
  62. package/dist/capabilities/session-login.js +170 -0
  63. package/dist/capabilities/session-search.js +635 -44
  64. package/dist/capabilities/spawn-bounded.js +203 -0
  65. package/dist/capabilities/sponsor-plugin.js +341 -0
  66. package/dist/capabilities/visa-policy.js +140 -0
  67. package/dist/capabilities/weather.js +243 -138
  68. package/dist/data/session-golden-20.json +25 -0
  69. package/dist/data/sf-golden-manifest.json +103 -0
  70. package/dist/data/sf-static-routes.json +91 -0
  71. package/dist/scripts/action-cache-tests.js +24 -59
  72. package/dist/scripts/agent-planning-turn-deadline-e2e.js +242 -0
  73. package/dist/scripts/agent-planning-turn-deadline-tests.js +185 -0
  74. package/dist/scripts/agent-reach-deep-tests.js +30 -40
  75. package/dist/scripts/agent-reach-error-tests.js +354 -0
  76. package/dist/scripts/agent-reach-tests.js +26 -18
  77. package/dist/scripts/agent-reach-wrapper-tests.js +50 -50
  78. package/dist/scripts/anything-path-measure-tests.js +133 -0
  79. package/dist/scripts/anything-path-measure.js +150 -0
  80. package/dist/scripts/anything-tests.js +250 -60
  81. package/dist/scripts/artifact-client-contract-tests.js +220 -0
  82. package/dist/scripts/artifacts-capability-tests.js +727 -0
  83. package/dist/scripts/async-collect.js +50 -11
  84. package/dist/scripts/benchmark-environment-bridge-e2e.js +741 -0
  85. package/dist/scripts/benchmark-environment-bridge-tests.js +1452 -0
  86. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +126 -0
  87. package/dist/scripts/booking-copilot-availability-policy-tests.js +265 -0
  88. package/dist/scripts/booking-copilot-bin-proof-tests.js +66 -0
  89. package/dist/scripts/booking-copilot-cleanup-proof-helper.js +76 -0
  90. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +144 -0
  91. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +424 -0
  92. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +2839 -0
  93. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +309 -0
  94. package/dist/scripts/booking-copilot-dsh-readiness-proof.js +238 -0
  95. package/dist/scripts/booking-copilot-dsh-warmer-proof.js +126 -0
  96. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +156 -0
  97. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +70 -0
  98. package/dist/scripts/booking-copilot-intent-proof-tests.js +422 -0
  99. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +184 -0
  100. package/dist/scripts/booking-copilot-planner-live-probe.js +95 -0
  101. package/dist/scripts/booking-copilot-proof-runtime.js +42 -0
  102. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +210 -0
  103. package/dist/scripts/booking-copilot-runtime-proof-tests.js +2349 -0
  104. package/dist/scripts/booking-copilot-server-proof-tests.js +784 -0
  105. package/dist/scripts/booking-copilot-startup-proof-tests.js +94 -0
  106. package/dist/scripts/booking-executor-tests.js +80 -0
  107. package/dist/scripts/booking-recovery-chain-tests.js +182 -0
  108. package/dist/scripts/booking-saga-tests.js +125 -0
  109. package/dist/scripts/booking-surface-contract-proof-tests.js +212 -0
  110. package/dist/scripts/bootstrap-tests.js +1549 -0
  111. package/dist/scripts/bridge-events-tests.js +239 -0
  112. package/dist/scripts/bridge-ledger-tests.js +428 -0
  113. package/dist/scripts/build-changelog.js +238 -0
  114. package/dist/scripts/build-metrics-report.js +331 -0
  115. package/dist/scripts/changelog-tests.js +118 -0
  116. package/dist/scripts/channel-probe-tests.js +103 -0
  117. package/dist/scripts/channel-probe.js +169 -0
  118. package/dist/scripts/channel-registry-tests.js +208 -0
  119. package/dist/scripts/city-scenario-tier-tests.js +234 -0
  120. package/dist/scripts/companion-tests.js +25 -70
  121. package/dist/scripts/dida-cookie-probe.js +87 -0
  122. package/dist/scripts/dida-runner-stop-tests.js +311 -0
  123. package/dist/scripts/dida-sw-probe.js +114 -0
  124. package/dist/scripts/diff-test.js +17 -12
  125. package/dist/scripts/doctor-tests.js +600 -0
  126. package/dist/scripts/drill-multiuser-ledger-tests.js +1076 -0
  127. package/dist/scripts/drill-sdk-descendant-cleanup-tests.js +606 -0
  128. package/dist/scripts/drill-w2a-sensor-tests.js +788 -0
  129. package/dist/scripts/dsh-artifact-e2e.js +279 -0
  130. package/dist/scripts/dsh-artifact-web-e2e.js +675 -0
  131. package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
  132. package/dist/scripts/dsh-llm-tests.js +437 -0
  133. package/dist/scripts/dsh-runtime-closure-tests.js +182 -0
  134. package/dist/scripts/dsh-runtime-closure.js +162 -0
  135. package/dist/scripts/dsh-session-v3-migration-proof.js +109 -0
  136. package/dist/scripts/dsh-subprocess-local-proof.js +150 -0
  137. package/dist/scripts/dsh-target-closure-proof.js +178 -0
  138. package/dist/scripts/effect-tests.js +539 -0
  139. package/dist/scripts/engine-run.js +1 -3
  140. package/dist/scripts/engine-tests.js +16 -10
  141. package/dist/scripts/evaluation-cadence-tests.js +174 -0
  142. package/dist/scripts/evaluation-contract-tests.js +268 -0
  143. package/dist/scripts/extension-distribution-cli.js +39 -0
  144. package/dist/scripts/extension-distribution-tests.js +443 -0
  145. package/dist/scripts/extension-tests.js +988 -0
  146. package/dist/scripts/external-event-tests.js +350 -0
  147. package/dist/scripts/fact-gate-tests.js +869 -0
  148. package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
  149. package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
  150. package/dist/scripts/flight-malformed-tests.js +159 -0
  151. package/dist/scripts/flyai-setup-tests.js +228 -0
  152. package/dist/scripts/flyai-setup-tool-tests.js +54 -0
  153. package/dist/scripts/flyai-tests.js +692 -0
  154. package/dist/scripts/flyai-tool-registration-contract-tests.js +91 -0
  155. package/dist/scripts/fx-contract-tests.js +201 -0
  156. package/dist/scripts/g5-guard-tests.js +150 -0
  157. package/dist/scripts/g5-guard.js +193 -0
  158. package/dist/scripts/gen-lunar-anchors.js +106 -0
  159. package/dist/scripts/geo-atlas-tests.js +283 -0
  160. package/dist/scripts/gotry-backend-kernel-tests.js +206 -0
  161. package/dist/scripts/gotry-try-demo-tests.js +79 -0
  162. package/dist/scripts/gotry-try-demo.js +49 -0
  163. package/dist/scripts/ground-transfer-tests.js +956 -0
  164. package/dist/scripts/hbcli-e2e-tests.js +159 -0
  165. package/dist/scripts/hbcli-live-optin-tests.js +96 -0
  166. package/dist/scripts/hbcli-release-contract-tests.js +122 -0
  167. package/dist/scripts/hbcli-tests.js +121 -69
  168. package/dist/scripts/health-watch-cli.js +73 -0
  169. package/dist/scripts/hotel-date-gate-tests.js +496 -0
  170. package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
  171. package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
  172. package/dist/scripts/i18n-tests.js +41 -0
  173. package/dist/scripts/incident-tests.js +281 -87
  174. package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
  175. package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
  176. package/dist/scripts/issue-271-liveness-tests.js +569 -0
  177. package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
  178. package/dist/scripts/issue-289-model-retry-tests.js +441 -0
  179. package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
  180. package/dist/scripts/issue-338-home-city-e2e.js +577 -0
  181. package/dist/scripts/issue343-real-entry-e2e.js +160 -0
  182. package/dist/scripts/issue436-persisted-routing-e2e.js +247 -0
  183. package/dist/scripts/itinerary-artifact-tests.js +368 -0
  184. package/dist/scripts/itinerary-deck-artifact-tests.js +416 -0
  185. package/dist/scripts/itinerary-deck-export-tests.js +552 -0
  186. package/dist/scripts/itinerary-deck-tests.js +435 -0
  187. package/dist/scripts/itinerary-html-tests.js +426 -0
  188. package/dist/scripts/journey-tests.js +22 -27
  189. package/dist/scripts/kernel-manifest-evidence.js +109 -0
  190. package/dist/scripts/kernel-manifest-gate.js +126 -0
  191. package/dist/scripts/kernel-manifest-tests.js +212 -0
  192. package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
  193. package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
  194. package/dist/scripts/kernel-manifest.js +369 -0
  195. package/dist/scripts/lavish-local-tests.js +1226 -0
  196. package/dist/scripts/lavish-product-tools-tests.js +537 -0
  197. package/dist/scripts/ledger-repair-apply-tests.js +316 -0
  198. package/dist/scripts/ledger-repair-plan-tests.js +375 -0
  199. package/dist/scripts/ledger-tests.js +382 -246
  200. package/dist/scripts/ledger-workflow-crash.js +10 -20
  201. package/dist/scripts/m3-cohort-tests.js +714 -0
  202. package/dist/scripts/m3-cohort.js +310 -0
  203. package/dist/scripts/managed-dsh-cleanup-diagnostic-proof.js +184 -0
  204. package/dist/scripts/managed-dsh-close-failure-proof.js +172 -0
  205. package/dist/scripts/managed-dsh-run-port-proof.js +153 -0
  206. package/dist/scripts/managed-dsh-terminal-proof.js +252 -0
  207. package/dist/scripts/map-tools-vendor-package-proof.js +365 -0
  208. package/dist/scripts/memory-capture-tests.js +242 -61
  209. package/dist/scripts/memory-decay-tests.js +18 -52
  210. package/dist/scripts/memory-lifecycle-tests.js +657 -0
  211. package/dist/scripts/memory-lifecycle.js +196 -0
  212. package/dist/scripts/memory-metrics.js +24 -12
  213. package/dist/scripts/memory-value-report.js +631 -0
  214. package/dist/scripts/memory-value-tests.js +328 -0
  215. package/dist/scripts/metrics-report-tests.js +151 -0
  216. package/dist/scripts/model-override-e2e.js +137 -0
  217. package/dist/scripts/nightly-evidence-tests.js +203 -0
  218. package/dist/scripts/nightly-evidence.js +303 -0
  219. package/dist/scripts/nudge-digest.js +39 -28
  220. package/dist/scripts/observability-tests.js +381 -0
  221. package/dist/scripts/onboarding-tests.js +161 -0
  222. package/dist/scripts/opensky-check.js +18 -13
  223. package/dist/scripts/opensky-tests.js +28 -28
  224. package/dist/scripts/outcome-projection-tests.js +427 -0
  225. package/dist/scripts/persona-sim-tests.js +603 -0
  226. package/dist/scripts/persona-sim.js +1081 -0
  227. package/dist/scripts/persona-surface-guard-tests.js +49 -0
  228. package/dist/scripts/pnpm-dsh-closure-proof.js +17 -0
  229. package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
  230. package/dist/scripts/price-drift-tests.js +340 -0
  231. package/dist/scripts/price-drift-watch.js +447 -0
  232. package/dist/scripts/probe-poi-tests.js +53 -45
  233. package/dist/scripts/product-metrics.js +483 -0
  234. package/dist/scripts/publish-preverify.js +110 -0
  235. package/dist/scripts/realtime-pricing-tests.js +76 -0
  236. package/dist/scripts/recall-tests.js +513 -0
  237. package/dist/scripts/replay-async.js +22 -18
  238. package/dist/scripts/replay-real.js +18 -18
  239. package/dist/scripts/replay.js +54 -82
  240. package/dist/scripts/route-provider-conformance-tests.js +391 -0
  241. package/dist/scripts/session-attach-diagnose.js +20 -20
  242. package/dist/scripts/session-attach-poc.js +34 -37
  243. package/dist/scripts/session-benchmark.js +180 -0
  244. package/dist/scripts/session-dida-live-e2e.js +199 -0
  245. package/dist/scripts/session-extract-tests.js +14 -35
  246. package/dist/scripts/session-link-tests.js +478 -0
  247. package/dist/scripts/session-login.js +27 -46
  248. package/dist/scripts/session-tests.js +796 -96
  249. package/dist/scripts/session-zone-crash.js +49 -0
  250. package/dist/scripts/session-zone-ledger-tests.js +432 -0
  251. package/dist/scripts/session-zone-observation-tests.js +492 -0
  252. package/dist/scripts/session-zone-observe.js +135 -0
  253. package/dist/scripts/session-zone-wiring-tests.js +566 -0
  254. package/dist/scripts/session-zones-tests.js +374 -0
  255. package/dist/scripts/sf-live-benchmark.js +367 -0
  256. package/dist/scripts/sf-live-challenge-stop-tests.js +453 -0
  257. package/dist/scripts/sf-live-cli-tests.js +92 -0
  258. package/dist/scripts/sf-live-options.js +46 -0
  259. package/dist/scripts/sf-manifest-tests.js +30 -0
  260. package/dist/scripts/sf-manifest.js +33 -0
  261. package/dist/scripts/sf-soft-score-tests.js +67 -0
  262. package/dist/scripts/sf-summary-tests.js +324 -0
  263. package/dist/scripts/sf-summary.js +525 -0
  264. package/dist/scripts/share-tests.js +432 -0
  265. package/dist/scripts/skeleton-check.js +16 -14
  266. package/dist/scripts/skeleton-integration-test.js +28 -11
  267. package/dist/scripts/skills-contract-tests.js +51 -46
  268. package/dist/scripts/smoke-session-gate-tests.js +21 -0
  269. package/dist/scripts/smoke.js +690 -246
  270. package/dist/scripts/spawn-bounded-abort-tests.js +244 -0
  271. package/dist/scripts/sponsor-reuse-tests.js +262 -0
  272. package/dist/scripts/state-cli-tests.js +226 -101
  273. package/dist/scripts/state-cli.js +565 -198
  274. package/dist/scripts/state-repair-cli.js +66 -0
  275. package/dist/scripts/state-repair-tests.js +140 -0
  276. package/dist/scripts/static-golden-tests.js +165 -0
  277. package/dist/scripts/time-eval-tests.js +212 -192
  278. package/dist/scripts/travel-timeline-tests.js +29 -93
  279. package/dist/scripts/turn-handoff-collect-tests.js +169 -0
  280. package/dist/scripts/turn-handoff-collect.js +161 -0
  281. package/dist/scripts/turn-policy-tests.js +55 -0
  282. package/dist/scripts/typed-contract-canary.js +176 -0
  283. package/dist/scripts/tz-resolver-sanity.js +50 -0
  284. package/dist/scripts/tz-resolver-tests.js +584 -0
  285. package/dist/scripts/unified-tests.js +60 -20
  286. package/dist/scripts/visa-policy-tests.js +62 -0
  287. package/dist/scripts/weather-tests.js +276 -41
  288. package/dist/scripts/wish-channel-gate-tests.js +54 -0
  289. package/dist/scripts/write-gate-tests.js +522 -0
  290. package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
  291. package/dist/scripts/z3-lifecycle-tests.js +170 -0
  292. package/dist/scripts/z3-race-repeat-tests.js +39 -0
  293. package/dist/scripts/z3-race-tests.js +46 -0
  294. package/dist/src/artifact-gate.js +829 -0
  295. package/dist/src/backend/kernel.js +91 -0
  296. package/dist/src/backend/modules/booking-copilot.js +92 -0
  297. package/dist/src/backend/modules/booking-executor.js +124 -0
  298. package/dist/src/backend/modules/session-search.js +551 -0
  299. package/dist/src/benchmark-agent-conformance.js +578 -0
  300. package/dist/src/benchmark-environment-bridge.js +515 -0
  301. package/dist/src/benchmark-headless-child-diagnostics.js +166 -0
  302. package/dist/src/benchmark-tool-isolation.js +151 -0
  303. package/dist/src/bookable-facts.js +533 -0
  304. package/dist/src/booking-saga.js +97 -0
  305. package/dist/src/booking-surface/availability-policy.js +697 -0
  306. package/dist/src/booking-surface/booking-intent.js +43 -0
  307. package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
  308. package/dist/src/booking-surface/canonical-schema.js +116 -0
  309. package/dist/src/booking-surface/contracts.js +75 -0
  310. package/dist/src/booking-surface/dsh-planner.js +1854 -0
  311. package/dist/src/booking-surface/dsh-plugin.js +378 -0
  312. package/dist/src/booking-surface/dsh-worker.js +98 -0
  313. package/dist/src/booking-surface/error-codes.js +231 -0
  314. package/dist/src/booking-surface/index.js +9 -0
  315. package/dist/src/booking-surface/managed-dsh-cleanup-diagnostic.js +97 -0
  316. package/dist/src/booking-surface/managed-dsh-run-port.js +244 -0
  317. package/dist/src/booking-surface/profile.js +24 -0
  318. package/dist/src/booking-surface/recovery-chain.js +367 -0
  319. package/dist/src/booking-surface/runtime.js +1968 -0
  320. package/dist/src/booking-surface/server.js +674 -0
  321. package/dist/src/booking-surface/startup.js +168 -0
  322. package/dist/src/booking-surface/validation.js +261 -0
  323. package/dist/src/bridge.js +20 -13
  324. package/dist/src/city-scenario-tier.js +378 -0
  325. package/dist/src/companions.js +48 -74
  326. package/dist/src/contracts.js +21 -43
  327. package/dist/src/dsh-llm.js +171 -115
  328. package/dist/src/engine.js +276 -193
  329. package/dist/src/evaluation-cadence.js +97 -0
  330. package/dist/src/evaluation-contracts.js +283 -0
  331. package/dist/src/flight-pack-adapter.js +54 -0
  332. package/dist/src/flight-pack-contract.js +2 -0
  333. package/dist/src/flyai-setup-tool.js +70 -0
  334. package/dist/src/gotry-backend.js +125 -0
  335. package/dist/src/hotel-date-gate.js +118 -0
  336. package/dist/src/i18n.js +88 -0
  337. package/dist/src/index.js +2385 -881
  338. package/dist/src/itinerary-deck.js +139 -0
  339. package/dist/src/itinerary-doc-shared.js +661 -0
  340. package/dist/src/itinerary-html.js +131 -0
  341. package/dist/src/journey.js +155 -108
  342. package/dist/src/lavish-tools.js +400 -0
  343. package/dist/src/ledger-repair-apply.js +538 -0
  344. package/dist/src/ledger-repair-plan.js +463 -0
  345. package/dist/src/loop.js +331 -161
  346. package/dist/src/m3-cohort.js +1577 -0
  347. package/dist/src/memory-capture.js +180 -32
  348. package/dist/src/memory-decay.js +30 -14
  349. package/dist/src/memory-lifecycle.js +1310 -0
  350. package/dist/src/memory-utility.js +27 -40
  351. package/dist/src/mock-llm.js +36 -61
  352. package/dist/src/model.js +81 -36
  353. package/dist/src/outcome-projection.js +746 -0
  354. package/dist/src/realtime-pricing.js +103 -0
  355. package/dist/src/recall/card.js +74 -0
  356. package/dist/src/recall/evaluator.js +140 -0
  357. package/dist/src/recall/integration.js +17 -0
  358. package/dist/src/recall/tick.js +166 -0
  359. package/dist/src/session-link/plan-it-action.js +48 -0
  360. package/dist/src/session-link/session-link.js +248 -0
  361. package/dist/src/session-zone-ledger.js +312 -0
  362. package/dist/src/session-zone-observation.js +354 -0
  363. package/dist/src/session-zone-wiring.js +658 -0
  364. package/dist/src/session-zones.js +510 -0
  365. package/dist/src/share/adapters.js +51 -0
  366. package/dist/src/share/share-consent.js +76 -0
  367. package/dist/src/share/share-deck.js +58 -0
  368. package/dist/src/share/share-token.js +146 -0
  369. package/dist/src/slot-spec.js +84 -106
  370. package/dist/src/state-ledger.js +696 -524
  371. package/dist/src/state-repair.js +299 -0
  372. package/dist/src/subagent-job-id-guard.js +58 -0
  373. package/dist/src/time-anchor.js +231 -33
  374. package/dist/src/tool-packet.js +20 -8
  375. package/dist/src/travel-slots.js +58 -35
  376. package/dist/src/travel-timeline.js +44 -63
  377. package/dist/src/turn-deadline.js +287 -0
  378. package/dist/src/turn-policy.js +118 -0
  379. package/dist/src/tz-resolver.js +143 -0
  380. package/dist/src/unified.js +600 -360
  381. package/dist/src/wish-pool.js +42 -12
  382. package/dist/src/write-gate.js +495 -0
  383. package/dist/src/z3-shared.js +284 -0
  384. package/extension/README.md +21 -0
  385. package/extension/README.zh-CN.md +21 -0
  386. package/extension/background.js +463 -0
  387. package/extension/content-bridge.js +270 -0
  388. package/extension/content-main.js +111 -0
  389. package/extension/manifest.json +60 -0
  390. package/package.json +407 -14
  391. package/schemas/booking.intent.schema.json +193 -0
  392. package/schemas/booking.surface.schema.json +2622 -0
  393. package/ts/capabilities/agent-reach-deep.ts +114 -0
  394. package/ts/capabilities/agent-reach.ts +198 -0
  395. package/ts/capabilities/anything.ts +282 -0
  396. package/ts/capabilities/artifacts.ts +428 -0
  397. package/ts/capabilities/fact-log.ts +54 -0
  398. package/ts/capabilities/flyai-config.ts +388 -0
  399. package/ts/capabilities/flyai.ts +1274 -0
  400. package/ts/capabilities/hbcli.ts +253 -71
  401. package/ts/capabilities/incident-log.ts +88 -59
  402. package/ts/capabilities/itinerary-artifact.ts +214 -0
  403. package/ts/capabilities/itinerary-deck-artifact.ts +218 -0
  404. package/ts/capabilities/itinerary-deck-export.ts +378 -0
  405. package/ts/capabilities/opensky.ts +121 -0
  406. package/ts/capabilities/session/action-cache.ts +148 -0
  407. package/ts/capabilities/session/adapters/ctrip-flight.ts +181 -0
  408. package/ts/capabilities/session/adapters/ctrip-hotel.ts +238 -0
  409. package/ts/capabilities/session/adapters/dida-portal.ts +338 -0
  410. package/ts/capabilities/session/adapters/meituan-local.ts +70 -0
  411. package/ts/capabilities/session/adapters/rail-12306.ts +392 -0
  412. package/ts/capabilities/session/benchmark.ts +276 -0
  413. package/ts/capabilities/session/bridge-ledger.ts +333 -0
  414. package/ts/capabilities/session/extension-bridge.ts +794 -0
  415. package/ts/capabilities/session/extension-channel.ts +144 -0
  416. package/ts/capabilities/session/extension-distribution.ts +266 -0
  417. package/ts/capabilities/session/extract.ts +54 -0
  418. package/ts/capabilities/session/golden-score.ts +139 -0
  419. package/ts/capabilities/session/health-watch.ts +264 -0
  420. package/ts/capabilities/session/read-guard.ts +141 -0
  421. package/ts/capabilities/session/static-flight-golden.ts +209 -0
  422. package/ts/capabilities/session/transport.ts +146 -0
  423. package/ts/capabilities/session/wizard.ts +140 -0
  424. package/ts/capabilities/session-consent.ts +211 -0
  425. package/ts/capabilities/session-login.ts +208 -0
  426. package/ts/capabilities/session-search.ts +893 -0
  427. package/ts/capabilities/weather.ts +330 -0
  428. package/ts/dsh-runtime/vendor/README.md +68 -0
  429. package/ts/dsh-runtime/vendor/dsh-map-tools/LICENSE +21 -0
  430. package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +405 -0
  431. package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +404 -0
  432. package/ts/dsh-runtime/vendor/dsh-map-tools/client/client.js +1584 -0
  433. package/ts/dsh-runtime/vendor/dsh-map-tools/cordis.patch.yml +5 -0
  434. package/ts/dsh-runtime/vendor/dsh-map-tools/icon.svg +5 -0
  435. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/amap.js +595 -0
  436. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/nominatim.js +63 -0
  437. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/osrm.js +58 -0
  438. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/photon.js +55 -0
  439. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-file.js +87 -0
  440. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-route.js +131 -0
  441. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config.js +16 -0
  442. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/geo.js +232 -0
  443. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/index.js +120 -0
  444. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +40 -0
  445. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/staticmap-route.js +115 -0
  446. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/geocode.js +127 -0
  447. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/poi.js +84 -0
  448. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/routes.js +267 -0
  449. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/amap.d.ts +90 -0
  450. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/nominatim.d.ts +17 -0
  451. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/osrm.d.ts +15 -0
  452. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/photon.d.ts +22 -0
  453. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-file.d.ts +33 -0
  454. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-route.d.ts +23 -0
  455. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config.d.ts +23 -0
  456. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/geo.d.ts +61 -0
  457. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/index.d.ts +18 -0
  458. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +14 -0
  459. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/staticmap-route.d.ts +31 -0
  460. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/geocode.d.ts +11 -0
  461. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/poi.d.ts +9 -0
  462. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/routes.d.ts +75 -0
  463. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/types.d.ts +68 -0
  464. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types.js +16 -0
  465. package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +97 -0
  466. package/ts/package.json +293 -3
  467. package/ts/scripts/async-collect.ts +91 -0
  468. package/ts/scripts/map-tools-vendor-package-proof.ts +404 -0
  469. package/ts/scripts/skeleton-check.ts +1 -1
  470. package/ts/scripts/skeleton-integration-test.ts +23 -1
  471. package/ts/scripts/state-cli.ts +586 -0
  472. package/ts/scripts/turn-handoff-collect.ts +178 -0
  473. package/ts/src/artifact-gate.ts +991 -0
  474. package/ts/src/benchmark-agent-conformance.ts +619 -0
  475. package/ts/src/benchmark-environment-bridge.ts +538 -0
  476. package/ts/src/benchmark-headless-child-diagnostics.ts +184 -0
  477. package/ts/src/benchmark-tool-isolation.ts +166 -0
  478. package/ts/src/bookable-facts.ts +837 -0
  479. package/ts/src/booking-saga.ts +133 -0
  480. package/ts/src/booking-surface/availability-policy.ts +522 -0
  481. package/ts/src/booking-surface/booking-intent.ts +86 -0
  482. package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
  483. package/ts/src/booking-surface/canonical-schema.js +116 -0
  484. package/ts/src/booking-surface/contracts.ts +475 -0
  485. package/ts/src/booking-surface/dsh-planner.ts +1949 -0
  486. package/ts/src/booking-surface/dsh-plugin.js +378 -0
  487. package/ts/src/booking-surface/dsh-worker.ts +110 -0
  488. package/ts/src/booking-surface/error-codes.ts +241 -0
  489. package/ts/src/booking-surface/index.ts +9 -0
  490. package/ts/src/booking-surface/managed-dsh-cleanup-diagnostic.ts +151 -0
  491. package/ts/src/booking-surface/managed-dsh-run-port.ts +273 -0
  492. package/ts/src/booking-surface/profile.ts +42 -0
  493. package/ts/src/booking-surface/recovery-chain.ts +409 -0
  494. package/ts/src/booking-surface/runtime.ts +1793 -0
  495. package/ts/src/booking-surface/server.ts +733 -0
  496. package/ts/src/booking-surface/startup.ts +201 -0
  497. package/ts/src/booking-surface/validation.ts +217 -0
  498. package/ts/src/companions.ts +113 -0
  499. package/ts/src/contracts.ts +17 -3
  500. package/ts/src/dsh-llm.ts +152 -17
  501. package/ts/src/flyai-setup-tool.ts +77 -0
  502. package/ts/src/index.ts +2123 -200
  503. package/ts/src/itinerary-deck.ts +189 -0
  504. package/ts/src/itinerary-doc-shared.ts +741 -0
  505. package/ts/src/itinerary-html.ts +171 -0
  506. package/ts/src/loop.ts +234 -26
  507. package/ts/src/memory-capture.ts +283 -0
  508. package/ts/src/memory-decay.ts +55 -0
  509. package/ts/src/memory-utility.ts +70 -0
  510. package/ts/src/mock-llm.ts +4 -8
  511. package/ts/src/model.ts +88 -2
  512. package/ts/src/recall/card.ts +106 -0
  513. package/ts/src/recall/evaluator.ts +170 -0
  514. package/ts/src/recall/integration.ts +35 -0
  515. package/ts/src/recall/tick.ts +213 -0
  516. package/ts/src/session-link/plan-it-action.ts +78 -0
  517. package/ts/src/session-link/session-link.ts +275 -0
  518. package/ts/src/share/adapters.ts +93 -0
  519. package/ts/src/share/share-consent.ts +105 -0
  520. package/ts/src/share/share-deck.ts +85 -0
  521. package/ts/src/share/share-token.ts +166 -0
  522. package/ts/src/slot-spec.ts +172 -0
  523. package/ts/src/state-ledger.ts +1166 -0
  524. package/ts/src/subagent-job-id-guard.ts +72 -0
  525. package/ts/src/time-anchor.ts +338 -0
  526. package/ts/src/tool-packet.ts +32 -0
  527. package/ts/src/travel-slots.ts +219 -0
  528. package/ts/src/travel-timeline.ts +83 -0
  529. package/ts/src/turn-deadline.ts +366 -0
  530. package/ts/src/turn-policy.ts +154 -0
  531. package/ts/src/unified.ts +444 -105
  532. package/ts/src/wish-pool.ts +79 -0
  533. package/dist/scripts/session-attach-wait.js +0 -41
@@ -1,110 +1,110 @@
1
+ /**
2
+ * 统一行程模型 TS 版(与 py/gotry_feasibility/unified.py 对齐;Python 为 oracle)。
3
+ * 行程 = Segment 序列;选择单元是 SegmentOption(目的地或具体班次)。
4
+ * 本文件实现:类型 + 双旧输入适配 + 航班链形态求解(Z3)。
5
+ * 候选形态 TS 求解(枚举过滤)与 engine 等价对账在下一迁移段。
6
+ */
1
7
  import { checkConnectivity } from '../scripts/skeleton-check.js';
2
- import { evaluateChoice, minToHhmm, hhmmToMin, requiredUsableHours, trueCostToDict, LATEST_ARRIVE_STAY_MIN } from './model.js';
3
- let z3Promise = null;
4
- async function getZ3() {
5
- if (!z3Promise) {
6
- const { init } = await import('z3-solver');
7
- z3Promise = (async ()=>(await init()).Context('main'))();
8
- }
9
- return z3Promise;
10
- }
8
+ import { evaluateChoice, minToHhmm, hhmmToMin, requiredUsableHours, trueCostToDict, LATEST_ARRIVE_STAY_MIN, doorToDoorFromMove, NonpositiveDurationError } from './model.js';
9
+ import { withZ3 } from './z3-shared.js';
10
+ import { t as i18nT } from './i18n.js';
11
+ import { resolveOffsetForLocalDate, describeResolveFailure, isKnownZoneLoose, readWallParts, wallPartsToIso } from './tz-resolver.js';
12
+ import { FLIGHT_PACK_VERSION } from './flight-pack-contract.js';
13
+ // Z3 运行时收敛到 z3-shared(单一 WASM 实例 + 单一 Context + 会话级互斥)——
14
+ // 此前模块级 z3Promise 三份并存是 WASM race/OOM 的根因(见 z3-shared.ts 头注)。
15
+ /* eslint-disable @typescript-eslint/no-explicit-any */
16
+ /** 航班数据包(data/flights_2026.json 形态)→ 段链 */
11
17
  const SEGMENT_ROUTES = {
12
- f1: 'HKG->HKT',
13
- f2: 'HKT->BKK',
14
- f3: 'BKK->KMG',
15
- f4: 'KMG->SZX',
16
- f5: 'SZX->DXB',
17
- yn1: 'SZX->LJG',
18
- yn2: 'LJG->DLU',
19
- yn3: 'DLU->LJG',
20
- yn4: 'LJG->SZX'
18
+ f1: 'HKG->HKT', f2: 'HKT->BKK', f3: 'BKK->KMG', f4: 'KMG->SZX', f5: 'SZX->DXB',
19
+ yn1: 'SZX->LJG', yn2: 'LJG->DLU', yn3: 'DLU->LJG', yn4: 'LJG->SZX',
21
20
  };
22
21
  function routeHint(id) {
23
22
  return SEGMENT_ROUTES[id];
24
23
  }
24
+ // Issue #620 注:数据包缺 buffer_min/origin_transfer_min/dest_transfer_min 时,
25
+ // 这里的 `Number(undefined)` = NaN 会一路流到求解层。NaN **不在 parse 边界补 0**
26
+ // (补零等于替用户编接驳时间,ADR-10 翻译不造数),也不在这里整包拒收
27
+ // (data/yunnan-pack.json 的 yn0 真值按 meta.reconcil 仍待 founder 校准)。
28
+ // 防线在 solveUnifiedInner 的 assertZ3IntegerInputs:进 Z3 之前逐字段点名,
29
+ // 以显式 solver_error 返回,绝不冒充「不可行」。
30
+ // ---- 适配器:旧候选用例(洱海形态:request + candidates)→ 单 choice 段 -------------
31
+ // 与 py unified.py segments_from_candidate 逐行对齐
25
32
  export function segmentsFromCandidate(req, candidates) {
26
33
  const motHard = req.motivation;
27
34
  const wakeFloor = motHard.wakeFloorMin;
28
- const options = candidates.map((c)=>({
29
- id: c.id,
30
- label: c.name,
31
- score: c.imageryMatch,
32
- bestMonths: c.bestMonths,
33
- minDays: c.minDaysForPurpose,
34
- move: {
35
- hub: c.hub,
36
- services: [
37
- ...c.servicesOut
38
- ],
39
- retServices: [
40
- ...c.servicesRet
41
- ],
42
- transfers: [
43
- ...c.destTransfers
44
- ],
45
- bufferMin: c.bufferOutMin,
46
- bufferRetMin: c.bufferRetMin,
47
- originTransferMin: 0,
48
- destTransferMin: 0
49
- },
50
- stay: {
51
- nights: req.windowDays - 1,
52
- stayCnyPerNight: c.stayCnyPerNight,
53
- localDailyCny: c.localDailyCny
54
- }
55
- }));
35
+ const options = candidates.map(c => ({
36
+ id: c.id,
37
+ label: c.name,
38
+ date: c.date,
39
+ score: c.imageryMatch,
40
+ bestMonths: c.bestMonths,
41
+ minDays: c.minDaysForPurpose,
42
+ move: {
43
+ hub: c.hub,
44
+ services: [...c.servicesOut],
45
+ retServices: [...c.servicesRet],
46
+ transfers: [...c.destTransfers],
47
+ bufferMin: c.bufferOutMin,
48
+ bufferRetMin: c.bufferRetMin,
49
+ originTransferMin: 0, // 候选形态无独立接驳(并入 destTransfers)
50
+ destTransferMin: 0,
51
+ },
52
+ stay: {
53
+ nights: req.windowDays - 1,
54
+ stayCnyPerNight: c.stayCnyPerNight,
55
+ localDailyCny: c.localDailyCny,
56
+ },
57
+ }));
56
58
  const escape = req.motivation.weights['escape_rest'] ?? 0;
57
59
  return {
58
60
  note: req.note,
59
- segments: [
60
- {
61
+ segments: [{
61
62
  id: 'dest',
62
63
  role: 'choice',
63
64
  note: '目的地选择',
64
- anchors: {
65
- wakeFloorMin: wakeFloor
66
- },
67
- options
68
- }
69
- ],
65
+ anchors: { wakeFloorMin: wakeFloor },
66
+ options,
67
+ }],
70
68
  budgetCny: req.budgetCny,
71
- defaultWakeFloorMin: wakeFloor
69
+ defaultWakeFloorMin: wakeFloor,
72
70
  };
73
71
  }
74
72
  export function parseFlightPackToSpec(pack) {
75
- const legs = pack['legs'].map((l)=>({
76
- id: String(l['id']),
77
- role: l['services'].length === 1 ? 'fixed' : 'choice',
78
- note: l['note'] ? String(l['note']) : undefined,
79
- date: l['date'] ? String(l['date']) : undefined,
80
- anchors: {
81
- arriveByMin: l['arrive_by'] ? hhmmToMin(String(l['arrive_by'])) : undefined
73
+ // Issue #343 v2:IANA path;v1(无 version 字段或 version===1)走原数值口径。
74
+ const version = pack['version'] === undefined ? 1 : Number(pack['version']);
75
+ if (version === 2)
76
+ return parseFlightPackToSpecV2(pack);
77
+ if (version !== 1) {
78
+ throw Object.assign(new Error(`parseFlightPackToSpec: unsupported pack version ${version}`), { code: 'unsupported_pack_version' });
79
+ }
80
+ return parseFlightPackToSpecV1(pack);
81
+ }
82
+ /** v1 数值口径(path 保留为 export,便于 §7-1/§6 直接复用与差分) */
83
+ export function parseFlightPackToSpecV1(pack) {
84
+ const legs = pack['legs'].map(l => ({
85
+ id: String(l['id']),
86
+ role: (l['services'].length === 1 ? 'fixed' : 'choice'),
87
+ note: l['note'] ? String(l['note']) : undefined,
88
+ date: l['date'] ? String(l['date']) : undefined,
89
+ anchors: { arriveByMin: l['arrive_by'] ? hhmmToMin(String(l['arrive_by'])) : undefined },
90
+ route: routeHint(l['id']),
91
+ options: l['services'].map(sv => ({
92
+ id: String(sv['id']),
93
+ label: `${String(sv['id'])} ${String(l['note'] ?? '').slice(0, 18)}`,
94
+ move: {
95
+ hub: String(l['hub'] ?? ''),
96
+ services: [{ id: String(sv['id']), depMin: hhmmToMin(String(sv['dep'])), arrMin: hhmmToMin(String(sv['arr'])), priceCny: Number(sv['price_cny']) }],
97
+ bufferMin: Number(l['buffer_min']),
98
+ originTransferMin: Number(l['origin_transfer_min']),
99
+ destTransferMin: Number(l['dest_transfer_min']),
100
+ redEye: Boolean(l['red_eye']),
101
+ redEyeDurationMin: Number(l['red_eye_duration_min'] ?? 0),
102
+ tzOffsetMin: Number(l['tz_offset_min'] ?? 0),
103
+ originTzOffsetMin: Number(l['origin_tz_offset_min'] ?? 480),
82
104
  },
83
- route: routeHint(l['id']),
84
- options: l['services'].map((sv)=>({
85
- id: String(sv['id']),
86
- label: `${String(sv['id'])} ${String(l['note'] ?? '').slice(0, 18)}`,
87
- move: {
88
- hub: String(l['hub'] ?? ''),
89
- services: [
90
- {
91
- id: String(sv['id']),
92
- depMin: hhmmToMin(String(sv['dep'])),
93
- arrMin: hhmmToMin(String(sv['arr'])),
94
- priceCny: Number(sv['price_cny'])
95
- }
96
- ],
97
- bufferMin: Number(l['buffer_min']),
98
- originTransferMin: Number(l['origin_transfer_min']),
99
- destTransferMin: Number(l['dest_transfer_min']),
100
- redEye: Boolean(l['red_eye']),
101
- redEyeDurationMin: Number(l['red_eye_duration_min'] ?? 0),
102
- tzOffsetMin: Number(l['tz_offset_min'] ?? 0),
103
- originTzOffsetMin: Number(l['origin_tz_offset_min'] ?? 480)
104
- }
105
- }))
106
- }));
107
- const meta = pack['meta'] ?? {};
105
+ })),
106
+ }));
107
+ const meta = (pack['meta'] ?? {});
108
108
  const ww = meta['work_window'];
109
109
  const spec = {
110
110
  segments: legs,
@@ -112,82 +112,262 @@ export function parseFlightPackToSpec(pack) {
112
112
  homeTzOffsetMin: Number(ww['home_tz_offset_min']),
113
113
  startMin: Number(ww['start_min']),
114
114
  endMin: Number(ww['end_min']),
115
- workdays: ww['workdays'] ?? [
116
- 0,
117
- 1,
118
- 2,
119
- 3,
120
- 4
121
- ]
122
- } : undefined
115
+ workdays: ww['workdays'] ?? [0, 1, 2, 3, 4],
116
+ } : undefined,
117
+ [FLIGHT_PACK_VERSION]: 1,
123
118
  };
124
- for (const l of pack['legs']){
125
- const seg = spec.segments.find((s)=>s.id === l['id']);
126
- for (const svc of l['services']){
119
+ // M-1:班次的星期标注挂到 Option(缺省=不受工作窗口约束)
120
+ for (const l of pack['legs']) {
121
+ const seg = spec.segments.find(s => s.id === l['id']);
122
+ for (const svc of l['services']) {
127
123
  if (svc['weekday']) {
128
- const opt = seg.options.find((o)=>o.id === svc['id']);
129
- if (opt) opt.depWeekday = String(svc['weekday']);
124
+ const opt = seg.options.find(o => o.id === svc['id']);
125
+ if (opt)
126
+ opt.depWeekday = String(svc['weekday']);
130
127
  }
131
128
  }
132
129
  }
133
130
  return spec;
134
131
  }
135
- const WEEKDAY_IDX = {
136
- mon: 0,
137
- tue: 1,
138
- wed: 2,
139
- thu: 3,
140
- fri: 4,
141
- sat: 5,
142
- sun: 6
143
- };
144
- const WEEKDAY_CN = {
145
- mon: '一',
146
- tue: '二',
147
- wed: '三',
148
- thu: '四',
149
- fri: '五',
150
- sat: '六',
151
- sun: '日'
152
- };
132
+ /** Issue #343 v2:IANA zone + 当地日期解析;数值 tz_* 字段不可用(混合形态解析时拒收)。 */
133
+ export function parseFlightPackToSpecV2(pack) {
134
+ const legsRaw = pack['legs'];
135
+ if (!Array.isArray(legsRaw) || legsRaw.length === 0) {
136
+ throw Object.assign(new Error('parseFlightPackToSpecV2: legs missing/empty'), { code: 'v2_legs_missing' });
137
+ }
138
+ const meta = (pack['meta'] ?? {});
139
+ const ww = meta['work_window'];
140
+ const legs = [];
141
+ for (const l of legsRaw) {
142
+ // v2 不允许混合数值 tz_offset_min/origin_tz_offset_min(早 fail)
143
+ if ('tz_offset_min' in l || 'origin_tz_offset_min' in l) {
144
+ throw Object.assign(new Error(`parseFlightPackToSpecV2: leg ${l['id']} has legacy tz_offset_min/origin_tz_offset_min;v2 pack must use iana_*_zone + *_local_date`), { code: 'v2_legacy_offset_mixed' });
145
+ }
146
+ const legDate = l['date'] ? String(l['date']) : '';
147
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(legDate)) {
148
+ throw Object.assign(new Error(`parseFlightPackToSpecV2: leg ${l['id']} requires ISO date`), { code: 'v2_leg_date_missing' });
149
+ }
150
+ const ianaDepZone = String(l['iana_dep_zone'] ?? '');
151
+ const ianaArrZone = String(l['iana_arr_zone'] ?? '');
152
+ if (!ianaDepZone || !ianaArrZone) {
153
+ throw Object.assign(new Error(`parseFlightPackToSpecV2: leg ${l['id']} needs iana_dep_zone and iana_arr_zone`), { code: 'v2_zone_missing' });
154
+ }
155
+ const options = l['services'].map(sv => {
156
+ const svcDepDate = sv['dep_local_date'] ? String(sv['dep_local_date']) : legDate;
157
+ const svcArrDate = sv['arr_local_date'] ? String(sv['arr_local_date']) : legDate;
158
+ const depZone = sv['iana_dep_zone'] ? String(sv['iana_dep_zone']) : ianaDepZone;
159
+ const arrZone = sv['iana_arr_zone'] ? String(sv['iana_arr_zone']) : ianaArrZone;
160
+ const depR = resolveOffsetForLocalDate(depZone, svcDepDate, String(sv['dep']));
161
+ if (!depR.ok) {
162
+ throw Object.assign(new Error(`parseFlightPackToSpecV2: dep zone/date failure: ${describeResolveFailure(depR)}`), { code: 'v2_dep_resolve_failed' });
163
+ }
164
+ const arrR = resolveOffsetForLocalDate(arrZone, svcArrDate, String(sv['arr']));
165
+ if (!arrR.ok) {
166
+ throw Object.assign(new Error(`parseFlightPackToSpecV2: arr zone/date failure: ${describeResolveFailure(arrR)}`), { code: 'v2_arr_resolve_failed' });
167
+ }
168
+ // Issue #343 root 复核:d2d canonical 输入必须在 parse 边界校验严格 chronologically after,
169
+ // 否则把病态数据喂给 model.doorToDoorFromMove → throw NonpositiveDurationError → 被
170
+ // solveUnified WASM guard 误报为 wasm_runtime_error。
171
+ if (arrR.utcMs <= depR.utcMs) {
172
+ throw Object.assign(new Error(`parseFlightPackToSpecV2: service ${String(sv['id'])} arrival instant (${new Date(arrR.utcMs).toISOString()}) must be strictly after departure instant (${new Date(depR.utcMs).toISOString()})`), { code: 'v2_arr_not_after_dep' });
173
+ }
174
+ // tzOffsetMin = arr - dep(positive = 抵达地更靠东)
175
+ const tzOffsetMin = arrR.offsetMin - depR.offsetMin;
176
+ return {
177
+ id: String(sv['id']),
178
+ label: `${String(sv['id'])} ${String(l['note'] ?? '').slice(0, 18)}`,
179
+ date: svcDepDate,
180
+ move: {
181
+ hub: String(l['hub'] ?? ''),
182
+ services: [{
183
+ id: String(sv['id']),
184
+ depMin: hhmmToMin(String(sv['dep'])),
185
+ arrMin: hhmmToMin(String(sv['arr'])),
186
+ priceCny: Number(sv['price_cny']),
187
+ // Issue #343 v2:把"墙上时刻"投影成真实 UTC instant 留给 evaluate / workWindow 用;
188
+ // d2d 公式走 (arrUtcMs - depUtcMs)/60000,跨日 / DST / 反向日界线都正确。
189
+ depUtcMs: depR.utcMs,
190
+ arrUtcMs: arrR.utcMs,
191
+ }],
192
+ bufferMin: Number(l['buffer_min']),
193
+ originTransferMin: Number(l['origin_transfer_min']),
194
+ destTransferMin: Number(l['dest_transfer_min']),
195
+ redEye: Boolean(l['red_eye']),
196
+ redEyeDurationMin: Number(sv['red_eye_duration_min'] ?? l['red_eye_duration_min'] ?? 0),
197
+ groundRecoveryMin: l['ground_recovery_min'] ? Number(l['ground_recovery_min']) : undefined,
198
+ tzOffsetMin,
199
+ originTzOffsetMin: depR.offsetMin,
200
+ ianaDepZone: depZone,
201
+ ianaArrZone: arrZone,
202
+ },
203
+ depWeekday: weekdayFromYmd(svcDepDate),
204
+ };
205
+ });
206
+ const role = (l['services'].length === 1 ? 'fixed' : 'choice');
207
+ legs.push({
208
+ id: String(l['id']),
209
+ role,
210
+ note: l['note'] ? String(l['note']) : undefined,
211
+ date: legDate,
212
+ anchors: { arriveByMin: l['arrive_by'] ? hhmmToMin(String(l['arrive_by'])) : undefined },
213
+ route: routeHint(l['id']),
214
+ options,
215
+ });
216
+ }
217
+ // work_window:home_zone 必需(v2 的 home 偏移走 IANA,数值字段不再权威);同时给两值报 reject。
218
+ let workWindow;
219
+ if (ww) {
220
+ const hasHomeZone = Boolean(ww['home_zone']);
221
+ const hasHomeNum = ww['home_tz_offset_min'] !== undefined;
222
+ if (hasHomeZone && hasHomeNum) {
223
+ throw Object.assign(new Error('parseFlightPackToSpecV2: work_window cannot combine home_zone and home_tz_offset_min'), { code: 'v2_ww_conflict' });
224
+ }
225
+ if (!hasHomeZone) {
226
+ throw Object.assign(new Error('parseFlightPackToSpecV2: work_window.home_zone is required for v2'), { code: 'v2_ww_home_zone_required' });
227
+ }
228
+ // 在 v2 parse 边界即时验证 home_zone 是已知 IANA zone(root counterexample #3:不许 fail-open)。
229
+ // 用 tz-resolver 的 isKnownZoneLoose:接受 Intl.DateTimeFormat 能识别的合法 alias(US/Pacific / ROK / Etc/GMT+8)。
230
+ const homeZone = String(ww['home_zone']);
231
+ if (!isKnownZoneLoose(homeZone)) {
232
+ throw Object.assign(new Error(`parseFlightPackToSpecV2: work_window.home_zone '${homeZone}' is not a known IANA zone`), { code: 'v2_home_zone_unknown' });
233
+ }
234
+ const startHHmm = ww['start'] ? String(ww['start']) : undefined;
235
+ const endHHmm = ww['end'] ? String(ww['end']) : undefined;
236
+ const startMin = ww['start_min'] !== undefined ? Number(ww['start_min']) : (startHHmm ? hhmmToMin(startHHmm) : hhmmToMin('10:00'));
237
+ const endMin = ww['end_min'] !== undefined ? Number(ww['end_min']) : (endHHmm ? hhmmToMin(endHHmm) : hhmmToMin('19:00'));
238
+ // numeric homeTzOffsetMin 仅占位:在 v2 工作窗口过滤时按 home_zone + dep_date 重算;预填 leg-date 占位 0
239
+ workWindow = {
240
+ homeTzOffsetMin: 0,
241
+ startMin,
242
+ endMin,
243
+ workdays: ww['workdays'] ?? [0, 1, 2, 3, 4],
244
+ homeZone,
245
+ startHHmm,
246
+ endHHmm,
247
+ };
248
+ }
249
+ return { segments: legs, workWindow, [FLIGHT_PACK_VERSION]: 2 };
250
+ }
251
+ const WEEKDAY_IDX = { mon: 0, tue: 1, wed: 2, thu: 3, fri: 4, sat: 5, sun: 6 };
252
+ const WEEKDAY_CN = { mon: '一', tue: '二', wed: '三', thu: '四', fri: '五', sat: '六', sun: '日' };
253
+ /** YYYY-MM-DD → mon/tue/wed/thu/fri/sat/sun;v2 路径给 option 标 depWeekday 用。 */
254
+ export function weekdayFromYmd(ymd) {
255
+ const m = ymd.match(/^(\d{4})-(\d{2})-(\d{2})$/);
256
+ if (!m)
257
+ throw new Error(`weekdayFromYmd: bad ymd ${ymd}`);
258
+ const dow = new Date(Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3]))).getUTCDay();
259
+ return ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'][dow];
260
+ }
261
+ /** M-1:工作日的工作窗口内起飞 → 排除理由;否则 null(与 py _work_window_blocks 对齐)。
262
+ *
263
+ * Issue #343 v2:home zone 存在时,必须把"出发真实 UTC instant"投影到 home zone
264
+ * 取该 zone 的 date/weekday/HH:MM(root counterexample #2:Tokyo 06:30 ≈ LA 14:30 prev day);
265
+ * 不再用 dep-local date 直接当 home date。
266
+ *
267
+ * root 评审复核:`depUtcMs` 是已知确定的 instant,直接 `readWallParts(homeZone, depUtcMs)`
268
+ * 取墙时分量 — 不要把 projected wall time 再喂给 `resolveOffsetForLocalDate` 重做 ±14h 扫描,
269
+ * 否则秋回退日 01:30 会被错误判为 overlap_ambiguous 而 fail-open(已知 instant 永远唯一)。
270
+ *
271
+ * dep/arr 的"墙上本地输入"(leg-level `dep`/`arr` 字段)由 parseFlightPackToSpecV2 在
272
+ * 解析时已用 `resolveOffsetForLocalDate` 拒过 gap/overlap,所以 leg 进 solver 时
273
+ * 两个 instant 一定 strict-after 且偏移已知。
274
+ *
275
+ * home zone 未知时由 v2 parse 边界直接抛 v2_home_zone_unknown 拒绝;
276
+ * 这里兜底只在 v1 legacy 路径出现。
277
+ */
153
278
  function workWindowBlocks(spec, option) {
154
279
  const ww = spec.workWindow;
155
- if (!ww || !option.move || !option.depWeekday) return null;
156
- if (!(ww.workdays ?? [
157
- 0,
158
- 1,
159
- 2,
160
- 3,
161
- 4
162
- ]).includes(WEEKDAY_IDX[option.depWeekday])) return null;
163
- const dep = option.move.services[0].depMin;
164
- const shift = (option.move.originTzOffsetMin ?? 480) - ww.homeTzOffsetMin;
165
- const start = ww.startMin + shift, end = ww.endMin + shift;
166
- if (start <= dep && dep <= end) {
167
- const hhmm = (m)=>`${String(Math.floor(m / 60)).padStart(2, '0')}:${String(m % 60).padStart(2, '0')}`;
168
- return `周${WEEKDAY_CN[option.depWeekday]} ${hhmm(dep)} 起飞落在工作窗口(当地 ${hhmm(start)}-${hhmm(end)})内`;
280
+ if (!ww || !option.move || !option.depWeekday)
281
+ return null;
282
+ const svc = option.move.services[0];
283
+ const dep = svc.depMin;
284
+ const homeZone = ww.homeZone;
285
+ let homeOffsetMin = ww.homeTzOffsetMin;
286
+ let homeDepMin;
287
+ let homeDepWeekday;
288
+ if (homeZone) {
289
+ if (typeof svc.depUtcMs !== 'number') {
290
+ // v2 路径必然填了 depUtcMs;没填 = 数据病,按 red flag 排除该 option。
291
+ return i18nT('un.workwindow_unresolvable_reason', {
292
+ reason: `home zone ${homeZone} requires UTC dep instant (v2 path bug)`,
293
+ });
294
+ }
295
+ // 已知 instant 直接投影到 home zone 的墙时 — 无歧义,不必也不应重走 ±14h 扫描。
296
+ const homeParts = readWallParts(homeZone, svc.depUtcMs);
297
+ const homeIso = wallPartsToIso(homeParts);
298
+ homeDepMin = hhmmToMin(homeIso.hhmm);
299
+ homeDepWeekday = weekdayFromYmd(homeIso.ymd);
300
+ }
301
+ const effectiveDep = homeDepMin ?? dep;
302
+ const effectiveWeekday = homeDepWeekday ?? option.depWeekday;
303
+ if (!((ww.workdays ?? [0, 1, 2, 3, 4]).includes(WEEKDAY_IDX[effectiveWeekday])))
304
+ return null;
305
+ // v2:startMin/endMin 与 homeDepMin 都是 home-zone 墙时分量(已 IANA 归一),
306
+ // 直接比较;不再做 originTzOffsetMin 偏移(root 复核该 shift 是 v1 数值残留,会让窗口错位)。
307
+ // v1 路径(无 homeZone, 走数值 homeTzOffsetMin)保留 shift 兼容旧 spec。
308
+ let start, end;
309
+ if (homeZone) {
310
+ start = ww.startMin;
311
+ end = ww.endMin;
312
+ }
313
+ else {
314
+ const shift = (option.move.originTzOffsetMin ?? 480) - homeOffsetMin;
315
+ start = ww.startMin + shift;
316
+ end = ww.endMin + shift;
317
+ }
318
+ if (start <= effectiveDep && effectiveDep <= end) {
319
+ const hhmm = (m) => `${String(Math.floor(m / 60)).padStart(2, '0')}:${String(m % 60).padStart(2, '0')}`;
320
+ return i18nT('un.workwindow_reason', {
321
+ wd: WEEKDAY_CN[effectiveWeekday] ?? '', dep: hhmm(effectiveDep), start: hhmm(start), end: hhmm(end),
322
+ });
169
323
  }
170
324
  return null;
171
325
  }
326
+ /** D-5:时区感知的段核算(与 py unified._evaluate_option_move 对齐)。
327
+ *
328
+ * Issue #343 d2d 算术唯一权威入口 = model.ts 的 `doorToDoorFromMove`。
329
+ * v2 路径会传 Service.depUtcMs/arrUtcMs(由 parseFlightPackToSpecV2 用 tz-resolver 投出),
330
+ * 此时 d2d 用真实 UTC instant 差 — 跨日 / 反向日界线 / DST 切换都正确;v2 arr<=dep 抛
331
+ * NonpositiveDurationError → 该 option 当作不可行(由 solveUnifiedInner 转 unsat_core)。
332
+ * v1 数值口径无 UTC 字段,走老 (arrMin - depMin) - tzOffsetMin;字节不变。
333
+ */
172
334
  function evaluateOptionMove(segId, mv) {
173
335
  const svc = mv.services[0];
336
+ let d2dMin;
337
+ try {
338
+ d2dMin = doorToDoorFromMove(svc, mv).doorToDoorMin;
339
+ }
340
+ catch (e) {
341
+ if (e instanceof NonpositiveDurationError) {
342
+ // d2d 病态输入 → 把这条 option 抛红 flag;这里走结构化 reject 而非吞错。
343
+ throw Object.assign(new Error(`evaluateOptionMove ${segId}/${svc.id}: ${e.message}`), {
344
+ code: e.code,
345
+ kind: e.kind,
346
+ });
347
+ }
348
+ throw e;
349
+ }
174
350
  const wake = svc.depMin - mv.bufferMin - mv.originTransferMin;
175
- const trueFlight = svc.arrMin - svc.depMin - (mv.tzOffsetMin ?? 0);
176
- const d2d = mv.bufferMin + mv.originTransferMin + trueFlight + mv.destTransferMin;
177
351
  const wakeDisplay = wake < 0 ? `${minToHhmm(wake + 1440)}(前一日)` : minToHhmm(wake);
178
352
  const arriveStay = svc.arrMin + mv.destTransferMin;
179
353
  let energy;
180
354
  if (mv.redEye && (mv.redEyeDurationMin ?? 0) > 0) {
181
355
  const sleepH = ((mv.redEyeDurationMin ?? 0) - 60) / 60;
356
+ // D-6 校准:机上睡眠上限 75;落地接驳(destTransferMin)乘车补眠回血(1h≈+5%,上限 80)
182
357
  const groundH = (mv.groundRecoveryMin ?? mv.destTransferMin ?? 0) / 60;
183
358
  const recovery = Math.min(5, 5 * groundH);
184
359
  energy = Math.max(30, Math.min(75 + recovery, 80, 30 + 8 * sleepH + recovery));
185
- } else {
360
+ }
361
+ else {
186
362
  energy = 100 - 2 * 8;
187
- if (wake < 5 * 60) energy -= 30;
188
- else if (wake < 6 * 60) energy -= 25;
189
- if (arriveStay > 21 * 60) energy -= 10;
190
- if (d2d > 6 * 60) energy -= 10;
363
+ if (wake < 5 * 60)
364
+ energy -= 30;
365
+ else if (wake < 6 * 60)
366
+ energy -= 25;
367
+ if (arriveStay > 21 * 60)
368
+ energy -= 10;
369
+ if (d2dMin > 6 * 60)
370
+ energy -= 10;
191
371
  energy = Math.max(0, energy);
192
372
  }
193
373
  return {
@@ -196,153 +376,238 @@ function evaluateOptionMove(segId, mv) {
196
376
  wake: wakeDisplay,
197
377
  wakeMin: wake,
198
378
  arrive_stay: minToHhmm(arriveStay),
199
- door_to_door: `${Math.floor(d2d / 60)}h${String(d2d % 60).padStart(2, '0')}m`,
200
- d2d_min: d2d,
379
+ door_to_door: `${Math.floor(d2dMin / 60)}h${String(d2dMin % 60).padStart(2, '0')}m`,
380
+ d2d_min: d2dMin,
201
381
  energy_pct: Math.round(energy),
202
- price_cny: svc.priceCny
382
+ price_cny: svc.priceCny,
203
383
  };
204
384
  }
385
+ function releaseZ3(resource) {
386
+ if (typeof resource?.release === 'function')
387
+ resource.release();
388
+ }
389
+ /** Issue #620:进 Z3 前的输入类型闸。`Int.val(NaN|Infinity|小数)` 在 WASM 里是
390
+ * `Assertion failed`,不是一个可解释的判定——所以在构造 AST 之前抛可定位的类型化错误。 */
391
+ export class SolverInputError extends Error {
392
+ code = 'solver_input_not_integer';
393
+ constructor(message) {
394
+ super(message);
395
+ this.name = 'SolverInputError';
396
+ }
397
+ }
398
+ /** 把所有将要变成 `Int.val(...)` 的数验成有限整数;违例逐项点名(段/候选/字段)。 */
399
+ function assertZ3IntegerInputs(spec, wakeFloorMin) {
400
+ const bad = [];
401
+ const check = (where, value) => {
402
+ // NaN/Infinity 经 JSON.stringify 会变成 "null",诊断信息就丢了——数值一律按原文印。
403
+ if (!Number.isInteger(value))
404
+ bad.push(`${where}=${typeof value === 'number' ? String(value) : JSON.stringify(value)}`);
405
+ };
406
+ check('wakeFloorMin', wakeFloorMin);
407
+ if (spec.budgetCny !== undefined)
408
+ check('budgetCny', spec.budgetCny);
409
+ for (const seg of spec.segments) {
410
+ if (seg.anchors?.arriveByMin !== undefined)
411
+ check(`${seg.id}.anchors.arriveByMin`, seg.anchors.arriveByMin);
412
+ if (seg.anchors?.departAfterMin !== undefined)
413
+ check(`${seg.id}.anchors.departAfterMin`, seg.anchors.departAfterMin);
414
+ // 段级 wake/arriveStay 编码只取 options[0].move 的接驳口径(见下方 o0)。
415
+ const o0 = seg.options[0]?.move;
416
+ if (o0) {
417
+ check(`${seg.id}.bufferMin`, o0.bufferMin);
418
+ check(`${seg.id}.originTransferMin`, o0.originTransferMin);
419
+ check(`${seg.id}.destTransferMin`, o0.destTransferMin);
420
+ }
421
+ for (const opt of seg.options) {
422
+ const svc = opt.move?.services[0];
423
+ if (!svc) {
424
+ bad.push(`${seg.id}/${opt.id}.services[0]=missing`);
425
+ continue;
426
+ }
427
+ check(`${seg.id}/${opt.id}.depMin`, svc.depMin);
428
+ check(`${seg.id}/${opt.id}.arrMin`, svc.arrMin);
429
+ check(`${seg.id}/${opt.id}.priceCny`, svc.priceCny);
430
+ }
431
+ }
432
+ if (bad.length > 0) {
433
+ throw new SolverInputError(`solveUnified: Z3 整数编码只接受有限整数,下列输入不是(issue #620):${bad.slice(0, 8).join('、')}`
434
+ + (bad.length > 8 ? `(共 ${bad.length} 处)` : ''));
435
+ }
436
+ }
437
+ /** 航班链形态求解:按 Option 选择,锚点命名约束,core 剥竖线(D-2 修复) */
205
438
  export async function solveUnified(spec) {
439
+ // 求解器失败防护:z3-solver 加载/求解异常不让它穿透到进程层把 dsh 杀掉。
440
+ // 但也**不冒充判定**(issue #620):崩溃以 solver_error 显式返回,unsat_core 保持缺席,
441
+ // 否则「引擎故障」会被下游当成「你的行程不可行」,理由位置上还放着机器 token。
442
+ // 候选形态走 solveChoiceSegment 不经过这里——这里是显式航班链路径。
206
443
  try {
207
444
  return await solveUnifiedInner(spec);
208
- } catch (e) {
209
- console.error('[gotry] solveUnified failed (likely wasm thread race):', e.message?.slice(0, 200));
210
- return {
211
- feasible: false,
212
- unsat_core: [
213
- 'wasm_runtime_error'
214
- ],
215
- red_flags: [
216
- 'WASM 求解器异常,建议下次用候选形态(枚举)重试'
217
- ]
218
- };
445
+ }
446
+ catch (e) {
447
+ const err = e instanceof Error ? e : new Error(String(e));
448
+ const code = err instanceof SolverInputError ? err.code : 'solver_runtime_error';
449
+ const message = (err.message ?? '').slice(0, 200);
450
+ // 措辞纪律:不再写「likely wasm thread race」——#620 证明它也可能是确定性的输入错误。
451
+ console.error(`[gotry] solveUnified aborted — ${code}(求解器失败,不是「不可行」判定):`, message);
452
+ return { feasible: false, solver_error: { code, message } };
219
453
  }
220
454
  }
221
455
  async function solveUnifiedInner(spec) {
456
+ // M-1:求解前的工作窗口确定性预过滤(与 py 对齐),排除理由入记录
457
+ // 骨架层(§7-1):三值语义标注——枢纽间否定只降权不排除(骨架滞后会错杀 EK329)
222
458
  const skeletonNotes = [];
223
459
  const exclusions = [];
224
- for (const seg of spec.segments){
460
+ for (const seg of spec.segments) {
225
461
  if (spec.skeletonHub) {
462
+ // 段级骨架查询:route 提示(如 "HKG->HKT")优先,否则跳过
226
463
  const route = seg.route;
227
464
  if (route) {
228
- const [a, b] = route.split('->').map((s)=>s.trim());
465
+ const [a, b] = route.split('->').map(s => s.trim());
229
466
  const verdict = await checkConnectivity(a, b);
230
467
  skeletonNotes.push(`${seg.id}: ${verdict.evidence}`);
231
468
  }
232
469
  }
233
- const kept = seg.options.filter((o)=>{
470
+ const kept = seg.options.filter(o => {
234
471
  const reason = workWindowBlocks(spec, o);
235
- if (reason) exclusions.push({
236
- segment: seg.id,
237
- option: o.id,
238
- reason
239
- });
472
+ if (reason)
473
+ exclusions.push({ segment: seg.id, option: o.id, reason });
240
474
  return !reason;
241
475
  });
242
476
  seg.options = kept;
243
477
  if (kept.length === 0) {
244
- return {
245
- feasible: false,
246
- unsat_core: [
247
- `${seg.id}:work_window`
248
- ],
249
- work_window_exclusions: exclusions
250
- };
478
+ return { feasible: false, unsat_core: [`${seg.id}:work_window`], work_window_exclusions: exclusions };
251
479
  }
252
480
  }
253
- const z3 = await getZ3();
254
- const { Bool, If, Int, Solver, Sum } = z3;
255
- const wakeFloor = spec.defaultWakeFloorMin ?? hhmmToMin('06:00');
256
- const allSels = {};
257
- const assertions = {};
258
- const svcOf = {};
259
- for (const seg of spec.segments){
260
- const sels = seg.options.map((_, i)=>Bool.const(`${seg.id}_o${i}`));
261
- allSels[seg.id] = sels;
262
- const svcs = seg.options.map((o)=>o.move.services[0]);
263
- svcOf[seg.id] = svcs;
264
- const pick = (attr)=>Sum(...sels.map((s, i)=>If(s, Int.val(svcs[i][attr]), Int.val(0))));
265
- const dep = pick('depMin'), arr = pick('arrMin');
266
- const o0 = seg.options[0].move;
267
- const wake = dep.sub(Int.val(o0.bufferMin + o0.originTransferMin));
268
- const arriveStay = arr.add(Int.val(o0.destTransferMin));
269
- if (seg.anchors?.arriveByMin !== undefined) assertions[`${seg.id}:arrive_by`] = arriveStay.le(Int.val(seg.anchors.arriveByMin));
270
- if (seg.anchors?.departAfterMin !== undefined) assertions[`${seg.id}:depart_after`] = dep.ge(Int.val(seg.anchors.departAfterMin));
271
- if (!seg.options.some((o)=>o.move?.redEye)) assertions[`${seg.id}:wake_floor`] = wake.ge(Int.val(wakeFloor));
272
- }
273
- const total = Sum(...spec.segments.flatMap((seg)=>allSels[seg.id].map((s, i)=>If(s, Int.val(svcOf[seg.id][i].priceCny), Int.val(0)))));
274
- if (spec.budgetCny !== undefined) assertions['total:budget'] = total.le(Int.val(spec.budgetCny));
275
- const exactlyOne = (sels)=>Sum(...sels.map((s)=>If(s, Int.val(1), Int.val(0)))).eq(Int.val(1));
276
- const coreOf = (s)=>{
277
- const v = s.unsatCore();
278
- const out = [];
279
- for(let i = 0; i < v.length(); i++)out.push(String(v.get(i)).replace(/^\|/, '').replace(/\|$/, ''));
280
- return out;
281
- };
282
- const s = new Solver();
283
- for (const sel of Object.values(allSels))s.add(exactlyOne(sel));
284
- for (const [name, expr] of Object.entries(assertions))s.addAndTrack(expr, name);
285
- const report = async (model)=>{
286
- const out = [];
287
- for (const seg of spec.segments){
288
- for(let i = 0; i < allSels[seg.id].length; i++){
289
- const v = await maybeAwait(model.eval(allSels[seg.id][i], true));
290
- if (String(v) !== 'true') continue;
291
- const o = seg.options[i];
292
- out.push({
293
- leg: seg.id,
294
- ...evaluateOptionMove(seg.id, o.move)
295
- });
296
- break;
297
- }
481
+ // Issue #620:类型闸在 Z3 会话之外先跑——非整数输入不许进 WASM(进去就是
482
+ // `Assertion failed`,还会污染共享实例)。抛 SolverInputError,由 solveUnified 归类。
483
+ const wakeFloorPre = spec.defaultWakeFloorMin ?? hhmmToMin('06:00');
484
+ assertZ3IntegerInputs(spec, wakeFloorPre);
485
+ // Z3 会话进互斥门(骨架/预过滤等网络段留在门外):solveUnifiedInner 的判定段
486
+ // 从 here 到 return 全部独占共享实例。
487
+ return withZ3('unified.solveUnifiedInner', async (z3) => {
488
+ const { Bool, If, Int, Solver, Sum } = z3;
489
+ const wakeFloor = wakeFloorPre;
490
+ const allSels = {};
491
+ const assertions = {};
492
+ const svcOf = {};
493
+ for (const seg of spec.segments) {
494
+ const sels = seg.options.map((_, i) => Bool.const(`${seg.id}_o${i}`));
495
+ allSels[seg.id] = sels;
496
+ const svcs = seg.options.map(o => o.move.services[0]);
497
+ svcOf[seg.id] = svcs;
498
+ const pick = (attr) => Sum(...sels.map((s, i) => If(s, Int.val(svcs[i][attr]), Int.val(0))));
499
+ const dep = pick('depMin'), arr = pick('arrMin');
500
+ const o0 = seg.options[0].move;
501
+ const wake = dep.sub(Int.val(o0.bufferMin + o0.originTransferMin));
502
+ const arriveStay = arr.add(Int.val(o0.destTransferMin));
503
+ if (seg.anchors?.arriveByMin !== undefined)
504
+ assertions[`${seg.id}:arrive_by`] = arriveStay.le(Int.val(seg.anchors.arriveByMin));
505
+ if (seg.anchors?.departAfterMin !== undefined)
506
+ assertions[`${seg.id}:depart_after`] = dep.ge(Int.val(seg.anchors.departAfterMin));
507
+ if (!seg.options.some(o => o.move?.redEye))
508
+ assertions[`${seg.id}:wake_floor`] = wake.ge(Int.val(wakeFloor));
298
509
  }
299
- return out;
300
- };
301
- async function maybeAwait(v) {
302
- return v instanceof Promise ? await v : v;
303
- }
304
- if (String(await s.check()) !== 'unsat') {
305
- const legs = await report(s.model());
306
- const money = legs.reduce((a, l)=>a + l.price_cny, 0);
307
- const redFlags = legs.filter((l)=>spec.segments.find((sg)=>sg.id === l.leg)?.options.some((o)=>o.move?.redEye) && l.energy_pct < 50).map((l)=>`${l.leg} 落地精力仅 ${l.energy_pct}%(红眼后直奔事务,当日不宜安排重要会议)`);
308
- return {
309
- feasible: true,
310
- money_cny: money,
311
- legs,
312
- red_flags: redFlags,
313
- work_window_exclusions: exclusions,
314
- skeleton_notes: skeletonNotes.length ? skeletonNotes : undefined
510
+ const total = Sum(...spec.segments.flatMap(seg => allSels[seg.id].map((s, i) => If(s, Int.val(svcOf[seg.id][i].priceCny), Int.val(0)))));
511
+ if (spec.budgetCny !== undefined)
512
+ assertions['total:budget'] = total.le(Int.val(spec.budgetCny));
513
+ const exactlyOne = (sels) => Sum(...sels.map((s) => If(s, Int.val(1), Int.val(0)))).eq(Int.val(1));
514
+ const coreOf = (s) => {
515
+ const v = s.unsatCore();
516
+ const out = [];
517
+ for (let i = 0; i < v.length(); i++)
518
+ out.push(String(v.get(i)).replace(/^\|/, '').replace(/\|$/, ''));
519
+ return out;
315
520
  };
316
- }
317
- const core = coreOf(s).sort();
318
- const suggestions = [];
319
- for (const name of core){
320
- const s2 = new Solver();
321
- for (const sel of Object.values(allSels))s2.add(exactlyOne(sel));
322
- for (const [n2, expr] of Object.entries(assertions)){
323
- if (n2 !== name) s2.addAndTrack(expr, n2);
521
+ const s = new Solver();
522
+ let sOpen = true;
523
+ const closeS = () => {
524
+ if (sOpen) {
525
+ releaseZ3(s);
526
+ sOpen = false;
527
+ }
528
+ };
529
+ try {
530
+ for (const sel of Object.values(allSels))
531
+ s.add(exactlyOne(sel));
532
+ for (const [name, expr] of Object.entries(assertions))
533
+ s.addAndTrack(expr, name);
534
+ const report = async (model) => {
535
+ const out = [];
536
+ for (const seg of spec.segments) {
537
+ for (let i = 0; i < allSels[seg.id].length; i++) {
538
+ const v = await maybeAwait(model.eval(allSels[seg.id][i], true));
539
+ if (String(v) !== 'true')
540
+ continue;
541
+ const o = seg.options[i];
542
+ out.push({ leg: seg.id, ...evaluateOptionMove(seg.id, o.move) });
543
+ break;
544
+ }
545
+ }
546
+ return out;
547
+ };
548
+ async function maybeAwait(v) {
549
+ return v instanceof Promise ? await v : v;
550
+ }
551
+ if (String(await s.check()) !== 'unsat') {
552
+ const model = s.model();
553
+ try {
554
+ const legs = await report(model);
555
+ const money = legs.reduce((a, l) => a + l.price_cny, 0);
556
+ const redFlags = legs
557
+ .filter(l => spec.segments.find(sg => sg.id === l.leg)?.options.some(o => o.move?.redEye) && l.energy_pct < 50)
558
+ .map(l => i18nT('un.redflag_redeye', { leg: l.leg, pct: l.energy_pct }));
559
+ return { feasible: true, money_cny: money, legs, red_flags: redFlags, work_window_exclusions: exclusions, skeleton_notes: skeletonNotes.length ? skeletonNotes : undefined };
560
+ }
561
+ finally {
562
+ releaseZ3(model);
563
+ }
564
+ }
565
+ const core = coreOf(s).sort();
566
+ const suggestions = [];
567
+ closeS();
568
+ for (const name of core) {
569
+ const s2 = new Solver();
570
+ try {
571
+ for (const sel of Object.values(allSels))
572
+ s2.add(exactlyOne(sel));
573
+ for (const [n2, expr] of Object.entries(assertions)) {
574
+ if (n2 !== name)
575
+ s2.addAndTrack(expr, n2);
576
+ }
577
+ if (String(await s2.check()) !== 'unsat') {
578
+ const model = s2.model();
579
+ try {
580
+ const legs = await report(model);
581
+ suggestions.push({ relax: name, money_cny: legs.reduce((a, l) => a + l.price_cny, 0) });
582
+ }
583
+ finally {
584
+ releaseZ3(model);
585
+ }
586
+ }
587
+ }
588
+ finally {
589
+ releaseZ3(s2);
590
+ }
591
+ }
592
+ return { feasible: false, unsat_core: core, suggestions };
324
593
  }
325
- if (String(await s2.check()) !== 'unsat') {
326
- const legs = await report(s2.model());
327
- suggestions.push({
328
- relax: name,
329
- money_cny: legs.reduce((a, l)=>a + l.price_cny, 0)
330
- });
594
+ finally {
595
+ closeS();
331
596
  }
332
- }
333
- return {
334
- feasible: false,
335
- unsat_core: core,
336
- suggestions
337
- };
597
+ });
338
598
  }
339
599
  function checkTrueCost(t, spec) {
340
600
  const fails = [];
341
- if (t.wakeMin < spec.wakeFloorMin) fails.push('wake_floor');
342
- if (t.energyArrivalPct < spec.minArrivalEnergyPct) fails.push('energy_floor');
343
- if (t.usableHours < spec.requiredUsableHours) fails.push('usable_hours');
344
- if (spec.budgetCny !== undefined && t.moneyCny > spec.budgetCny) fails.push('budget');
345
- if (t.arriveStayMin > spec.latestArriveStayMin) fails.push('arrival_before_evening');
601
+ if (t.wakeMin < spec.wakeFloorMin)
602
+ fails.push('wake_floor');
603
+ if (t.energyArrivalPct < spec.minArrivalEnergyPct)
604
+ fails.push('energy_floor');
605
+ if (t.usableHours < spec.requiredUsableHours)
606
+ fails.push('usable_hours');
607
+ if (spec.budgetCny !== undefined && t.moneyCny > spec.budgetCny)
608
+ fails.push('budget');
609
+ if (t.arriveStayMin > spec.latestArriveStayMin)
610
+ fails.push('arrival_before_evening');
346
611
  return fails;
347
612
  }
348
613
  function enumerateCombos(opt, cand, req, days) {
@@ -353,25 +618,15 @@ function enumerateCombos(opt, cand, req, days) {
353
618
  minArrivalEnergyPct: req.motivation.minArrivalEnergyPct,
354
619
  requiredUsableHours: requiredUsableHours(req.motivation),
355
620
  budgetCny: req.budgetCny,
356
- latestArriveStayMin: LATEST_ARRIVE_STAY_MIN
621
+ latestArriveStayMin: LATEST_ARRIVE_STAY_MIN,
357
622
  };
358
- for (const o of mv.services){
359
- for (const tr of mv.transfers ?? []){
360
- for (const r of mv.retServices ?? mv.services){
361
- for (const trr of mv.transfers ?? []){
362
- const ch = {
363
- outService: o,
364
- outTransfer: tr,
365
- retService: r,
366
- retTransfer: trr,
367
- days
368
- };
623
+ for (const o of mv.services) {
624
+ for (const tr of mv.transfers ?? []) {
625
+ for (const r of (mv.retServices ?? mv.services)) {
626
+ for (const trr of mv.transfers ?? []) {
627
+ const ch = { outService: o, outTransfer: tr, retService: r, retTransfer: trr, days };
369
628
  const t = evaluateChoice(cand, req, ch);
370
- out.push({
371
- choice: ch,
372
- cost: t,
373
- fails: checkTrueCost(t, spec)
374
- });
629
+ out.push({ choice: ch, cost: t, fails: checkTrueCost(t, spec) });
375
630
  }
376
631
  }
377
632
  }
@@ -381,158 +636,143 @@ function enumerateCombos(opt, cand, req, days) {
381
636
  function optionToCandidate(opt) {
382
637
  const mv = opt.move, st = opt.stay;
383
638
  return {
384
- id: opt.id,
385
- name: opt.label,
386
- hub: mv.hub,
387
- bufferOutMin: mv.bufferMin,
388
- bufferRetMin: mv.bufferRetMin ?? mv.bufferMin,
389
- servicesOut: [
390
- ...mv.services
391
- ],
392
- servicesRet: [
393
- ...mv.retServices ?? mv.services
394
- ],
395
- destTransfers: [
396
- ...mv.transfers ?? []
397
- ],
398
- stayCnyPerNight: st.stayCnyPerNight ?? 0,
399
- localDailyCny: st.localDailyCny ?? 0,
639
+ id: opt.id, name: opt.label, hub: mv.hub,
640
+ bufferOutMin: mv.bufferMin, bufferRetMin: mv.bufferRetMin ?? mv.bufferMin,
641
+ servicesOut: [...mv.services], servicesRet: [...(mv.retServices ?? mv.services)],
642
+ destTransfers: [...(mv.transfers ?? [])],
643
+ stayCnyPerNight: st.stayCnyPerNight ?? 0, localDailyCny: st.localDailyCny ?? 0,
400
644
  minDaysForPurpose: opt.minDays ?? 1,
401
- imageryMatch: opt.score ?? 0,
402
- bestMonths: opt.bestMonths ?? []
645
+ imageryMatch: opt.score ?? 0, bestMonths: opt.bestMonths ?? [],
403
646
  };
404
647
  }
648
+ /** 单 choice 段(目的地选项):逐 Option 枚举过滤,按 score 排序。与 py solve_choice_segment 等价。 */
405
649
  export function solveChoiceSegment(spec, req) {
406
- const seg = spec.segments.find((s)=>s.role === 'choice' && s.options.length > 0 && s.options[0].stay);
407
- if (!seg) throw new Error('solveChoiceSegment: no choice segment with stay found');
650
+ const seg = spec.segments.find(s => s.role === 'choice' && s.options.length > 0 && s.options[0].stay);
651
+ if (!seg)
652
+ throw new Error('solveChoiceSegment: no choice segment with stay found');
408
653
  const windowDays = req.windowDays;
409
654
  const verdicts = [];
410
- for (const opt of seg.options){
655
+ for (const opt of seg.options) {
411
656
  const cand = optionToCandidate(opt);
412
657
  const minDays = cand.minDaysForPurpose;
413
658
  const durationOk = windowDays >= minDays;
414
659
  const combos = durationOk ? enumerateCombos(opt, cand, req, windowDays) : [];
415
- const good = combos.filter((c)=>c.fails.length === 0);
660
+ const good = combos.filter(c => c.fails.length === 0);
416
661
  if (good.length > 0) {
417
- const best = good.reduce((a, b)=>a.cost.moneyCny <= b.cost.moneyCny ? a : b);
662
+ const best = good.reduce((a, b) => a.cost.moneyCny <= b.cost.moneyCny ? a : b);
418
663
  verdicts.push({
419
- candidate_id: opt.id,
420
- name: opt.label,
421
- feasible: true,
422
- imagery_match: opt.score ?? 0,
664
+ candidate_id: opt.id, name: opt.label, feasible: true, imagery_match: opt.score ?? 0,
423
665
  chosen: {
424
- out_service: best.choice.outService.id,
425
- out_transfer: best.choice.outTransfer.mode,
426
- ret_service: best.choice.retService.id,
427
- ret_transfer: best.choice.retTransfer.mode,
428
- days: best.choice.days
666
+ out_service: best.choice.outService.id, out_transfer: best.choice.outTransfer.mode,
667
+ ret_service: best.choice.retService.id, ret_transfer: best.choice.retTransfer.mode,
668
+ days: best.choice.days,
429
669
  },
430
- true_cost: trueCostToDict(best.cost)
670
+ true_cost: trueCostToDict(best.cost),
431
671
  });
432
672
  continue;
433
673
  }
434
- const blocked = durationOk ? Array.from(new Set(combos.flatMap((c)=>c.fails))).sort() : [
435
- 'duration'
436
- ];
674
+ // 不可行:归因 + duration 换长口径的 wish pool
675
+ const blocked = durationOk
676
+ ? Array.from(new Set(combos.flatMap(c => c.fails))).sort()
677
+ : ['duration'];
437
678
  const suggestions = [];
438
- for (const name of blocked){
439
- const opened = combos.filter((c)=>!c.fails.includes(name));
679
+ for (const name of blocked) {
680
+ const opened = combos.filter(c => !c.fails.includes(name));
440
681
  if (opened.length > 0) {
441
- const cheapest = opened.reduce((a, b)=>a.cost.moneyCny <= b.cost.moneyCny ? a : b);
442
- suggestions.push({
443
- relax: name,
444
- resulting_money_cny: cheapest.cost.moneyCny
445
- });
682
+ const cheapest = opened.reduce((a, b) => a.cost.moneyCny <= b.cost.moneyCny ? a : b);
683
+ suggestions.push({ relax: name, resulting_money_cny: cheapest.cost.moneyCny });
446
684
  }
447
685
  }
448
686
  let wish = null;
449
687
  if (!durationOk) {
450
688
  const longCombos = enumerateCombos(opt, cand, req, minDays);
451
- const budgetDropped = longCombos.filter((c)=>c.fails.length === 0 || c.fails.length === 1 && c.fails[0] === 'budget');
452
- const conds = {
453
- days: minDays
454
- };
689
+ const budgetDropped = longCombos.filter(c => c.fails.length === 0 || (c.fails.length === 1 && c.fails[0] === 'budget'));
690
+ const conds = { days: minDays };
455
691
  if (budgetDropped.length > 0) {
456
- conds['budget_cny'] = Math.min(...budgetDropped.map((c)=>c.cost.moneyCny));
692
+ conds['budget_cny'] = Math.min(...budgetDropped.map(c => c.cost.moneyCny));
457
693
  }
458
- if (opt.bestMonths?.length) conds['best_months'] = opt.bestMonths;
459
- wish = {
460
- name: opt.label,
461
- conditions: conds,
462
- reason: `${windowDays} 天窗口装不下(目的需 ${minDays} 天)`
463
- };
694
+ if (opt.bestMonths?.length)
695
+ conds['best_months'] = opt.bestMonths;
696
+ wish = { name: opt.label, conditions: conds, reason: i18nT('sg.wish_reason_window', { days: windowDays, minDays }) };
464
697
  }
465
698
  verdicts.push({
466
- candidate_id: opt.id,
467
- name: opt.label,
468
- feasible: false,
469
- imagery_match: opt.score ?? 0,
470
- unsat_core: blocked,
471
- suggestions,
472
- wish_pool: wish
699
+ candidate_id: opt.id, name: opt.label, feasible: false, imagery_match: opt.score ?? 0,
700
+ unsat_core: blocked, suggestions, wish_pool: wish,
473
701
  });
474
702
  }
475
- const feasible = verdicts.filter((v)=>v.feasible).sort((a, b)=>b.imagery_match - a.imagery_match);
703
+ const feasible = verdicts.filter(v => v.feasible).sort((a, b) => b.imagery_match - a.imagery_match);
476
704
  return {
477
705
  verdicts,
478
706
  recommended: feasible.length > 0 ? feasible[0]['candidate_id'] : null,
479
- answer_md: renderCandidateMarkdown(req, verdicts, feasible.length > 0 ? String(feasible[0]['candidate_id']) : null)
707
+ answer_md: renderCandidateMarkdown(req, verdicts, feasible.length > 0 ? String(feasible[0]['candidate_id']) : null),
480
708
  };
481
709
  }
482
710
  function renderCandidateMarkdown(req, verdicts, recommended) {
483
- const feasible = verdicts.filter((v)=>v['feasible']).sort((a, b)=>b['imagery_match'] - a['imagery_match']);
484
- const parked = verdicts.filter((v)=>!v['feasible']);
711
+ const feasible = verdicts.filter(v => v['feasible']).sort((a, b) => b['imagery_match'] - a['imagery_match']);
712
+ const parked = verdicts.filter(v => !v['feasible']);
485
713
  const lines = [];
486
- lines.push(`> 憧憬:${req.note}`);
487
- lines.push(`> 已识别约束:窗口 ${req.windowDays} 天 | 预算 ¥${req.budgetCny} | ` + `动机(休整改写需求 ${requiredUsableHours(req.motivation).toFixed(1)}h 有效休整)`);
714
+ lines.push(i18nT('md.header', { note: req.note }));
715
+ lines.push(i18nT('md.constraints', {
716
+ days: req.windowDays, budget: req.budgetCny,
717
+ hours: requiredUsableHours(req.motivation).toFixed(1),
718
+ }));
488
719
  lines.push('');
489
- for (const v of parked){
720
+ for (const v of parked) {
490
721
  const core = v['unsat_core'] ?? [];
491
- lines.push(`**${v['name']}:现在不行**——冲突约束:${core.join('、')}。`);
722
+ lines.push(i18nT('md.infeasible', { name: String(v['name']), core: core.join('、') }));
492
723
  const sug = (v['suggestions'] ?? [])[0];
493
- if (sug) lines.push(`- 放宽 ${sug['relax']}:约 ¥${sug['resulting_money_cny']}`);
724
+ if (sug)
725
+ lines.push(i18nT('md.relax_one', { relax: String(sug['relax']), money: String(sug['resulting_money_cny'] ?? '') }));
494
726
  const wish = v['wish_pool'];
495
727
  if (wish) {
496
728
  const conds = wish['conditions'];
497
- const budgetNote = conds['budget_cny'] ? `、约 ¥${conds['budget_cny']}` : '';
729
+ const budgetNote = conds['budget_cny'] ? i18nT('md.wish_budget', { cny: String(conds['budget_cny']) }) : '';
498
730
  const months = conds['best_months'];
499
- const season = months ? `,${months} 月最佳` : '';
500
- lines.push(`- 已放入「下一次出发」清单:需要 ${conds['days']} 天${budgetNote}${season}`);
731
+ const season = months ? i18nT('md.wish_season', { months: String(months) }) : '';
732
+ lines.push(i18nT('md.wish_pool', { days: String(conds['days']), budgetNote, season }));
501
733
  }
502
734
  }
503
735
  lines.push('');
504
- for (const v of feasible){
736
+ for (const v of feasible) {
505
737
  const ch = v['chosen'];
506
- const t = v['true_cost'];
507
- lines.push(`**${v['name']}:可行**` + `(${ch['out_service']} 出发,${ch['out_transfer']} 接驳,` + `起床 ${t['wake']},到达精力 ${t['energy_arrival_pct']}%,` + `门到门 ${t['door_to_door_out']},有效休整 ${t['usable_hours']}h,共 ¥${t['money_cny']})`);
738
+ const tc = v['true_cost'];
739
+ lines.push(i18nT('md.feasible_unified', {
740
+ name: String(v['name']), svc: String(ch['out_service']), mode: String(ch['out_transfer']),
741
+ wake: String(tc['wake']), energy: String(tc['energy_arrival_pct']), d2d: String(tc['door_to_door_out']),
742
+ hours: String(tc['usable_hours']), money: String(tc['money_cny']),
743
+ }));
508
744
  }
509
745
  if (feasible.length) {
510
746
  lines.push('');
511
747
  const best = feasible[0];
512
748
  const alt = feasible[1];
513
- lines.push(`**建议:${best['name']}**(意象匹配 ${(best['imagery_match'] * 100).toFixed(0)}%)。` + (alt ? `备选:${alt['name']}(¥${alt['true_cost']['money_cny']},匹配 ${(alt['imagery_match'] * 100).toFixed(0)}%)。` : ''));
749
+ lines.push(i18nT('md.recommend', { best: String(best['name']), match: (best['imagery_match'] * 100).toFixed(0) })
750
+ + (alt ? i18nT('md.alt', {
751
+ name: String(alt['name']), money: String(alt['true_cost']['money_cny']),
752
+ match: (alt['imagery_match'] * 100).toFixed(0),
753
+ }) : ''));
514
754
  }
515
755
  lines.push('');
516
- lines.push('**待你决定的两个问题**:');
756
+ lines.push(i18nT('md.decide_two'));
517
757
  if (feasible.length >= 2) {
518
- lines.push(`1. ${feasible[0]['name']} 还是 ${feasible[1]['name']}?(前者更贴意象,后者更省)`);
519
- } else if (feasible.length === 1) {
520
- lines.push(`1. 就去 ${feasible[0]['name']} 吗?`);
521
- } else {
522
- lines.push('1. 所有候选都不可行——考虑放宽哪条约束?');
758
+ lines.push(i18nT('md.q_choice', { a: String(feasible[0]['name']), b: String(feasible[1]['name']) }));
759
+ }
760
+ else if (feasible.length === 1) {
761
+ lines.push(i18nT('md.q_single', { name: String(feasible[0]['name']) }));
762
+ }
763
+ else {
764
+ lines.push(i18nT('md.q_none'));
523
765
  }
524
766
  const p0 = parked[0];
525
767
  const p0wish = p0?.['wish_pool'];
526
768
  if (p0wish) {
527
769
  const conds = p0wish['conditions'];
528
- lines.push(`2. 把 ${p0['name']} 留给「下一次出发」(${conds['days']} 天起),这次先去可行的?`);
529
- } else if (feasible.length) {
770
+ lines.push(i18nT('md.q_wish', { name: String(p0['name']), days: String(conds['days']) }));
771
+ }
772
+ else if (feasible.length) {
530
773
  const ch = feasible[0]['chosen'];
531
- lines.push(`2. 出发班次选 ${ch['out_service']} 还是更晚的?`);
774
+ lines.push(i18nT('md.q_depart', { id: String(ch['out_service']), dep: '' }));
532
775
  }
533
776
  return lines.join('\n');
534
777
  }
535
778
  export { minToHhmm };
536
-
537
-
538
- //# sourceURL=/Users/bytedance/work/gotry/ts/src/unified.ts