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

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 (507) 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 +1744 -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 +135 -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 +546 -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 +509 -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/resilience.js +191 -0
  41. package/dist/capabilities/session/action-cache.js +50 -44
  42. package/dist/capabilities/session/adapters/ctrip-flight.js +120 -62
  43. package/dist/capabilities/session/adapters/ctrip-hotel.js +222 -0
  44. package/dist/capabilities/session/adapters/dida-portal.js +274 -0
  45. package/dist/capabilities/session/adapters/meituan-local.js +35 -54
  46. package/dist/capabilities/session/adapters/rail-12306.js +342 -0
  47. package/dist/capabilities/session/benchmark.js +189 -0
  48. package/dist/capabilities/session/bridge-ledger.js +224 -0
  49. package/dist/capabilities/session/extension-bridge.js +636 -0
  50. package/dist/capabilities/session/extension-channel.js +109 -0
  51. package/dist/capabilities/session/extension-distribution.js +230 -0
  52. package/dist/capabilities/session/extract.js +31 -21
  53. package/dist/capabilities/session/golden-score.js +114 -0
  54. package/dist/capabilities/session/health-watch.js +215 -0
  55. package/dist/capabilities/session/read-guard.js +75 -24
  56. package/dist/capabilities/session/static-flight-golden.js +150 -0
  57. package/dist/capabilities/session/transport.js +97 -83
  58. package/dist/capabilities/session/wizard.js +117 -0
  59. package/dist/capabilities/session-consent.js +178 -0
  60. package/dist/capabilities/session-login.js +170 -0
  61. package/dist/capabilities/session-search.js +635 -44
  62. package/dist/capabilities/spawn-bounded.js +203 -0
  63. package/dist/capabilities/sponsor-plugin.js +341 -0
  64. package/dist/capabilities/visa-policy.js +140 -0
  65. package/dist/capabilities/weather.js +243 -138
  66. package/dist/data/session-golden-20.json +25 -0
  67. package/dist/data/sf-golden-manifest.json +103 -0
  68. package/dist/data/sf-static-routes.json +91 -0
  69. package/dist/scripts/action-cache-tests.js +24 -59
  70. package/dist/scripts/agent-planning-turn-deadline-e2e.js +242 -0
  71. package/dist/scripts/agent-planning-turn-deadline-tests.js +185 -0
  72. package/dist/scripts/agent-reach-deep-tests.js +30 -40
  73. package/dist/scripts/agent-reach-error-tests.js +354 -0
  74. package/dist/scripts/agent-reach-tests.js +26 -18
  75. package/dist/scripts/agent-reach-wrapper-tests.js +50 -50
  76. package/dist/scripts/anything-tests.js +116 -60
  77. package/dist/scripts/artifact-client-contract-tests.js +220 -0
  78. package/dist/scripts/artifacts-capability-tests.js +727 -0
  79. package/dist/scripts/async-collect.js +50 -11
  80. package/dist/scripts/benchmark-environment-bridge-e2e.js +741 -0
  81. package/dist/scripts/benchmark-environment-bridge-tests.js +1452 -0
  82. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +126 -0
  83. package/dist/scripts/booking-copilot-availability-policy-tests.js +265 -0
  84. package/dist/scripts/booking-copilot-bin-proof-tests.js +66 -0
  85. package/dist/scripts/booking-copilot-cleanup-proof-helper.js +76 -0
  86. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +144 -0
  87. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +424 -0
  88. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +2839 -0
  89. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +309 -0
  90. package/dist/scripts/booking-copilot-dsh-readiness-proof.js +238 -0
  91. package/dist/scripts/booking-copilot-dsh-warmer-proof.js +123 -0
  92. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +156 -0
  93. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +70 -0
  94. package/dist/scripts/booking-copilot-intent-proof-tests.js +422 -0
  95. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +184 -0
  96. package/dist/scripts/booking-copilot-planner-live-probe.js +95 -0
  97. package/dist/scripts/booking-copilot-proof-runtime.js +42 -0
  98. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +210 -0
  99. package/dist/scripts/booking-copilot-runtime-proof-tests.js +2349 -0
  100. package/dist/scripts/booking-copilot-server-proof-tests.js +784 -0
  101. package/dist/scripts/booking-copilot-startup-proof-tests.js +94 -0
  102. package/dist/scripts/booking-executor-tests.js +80 -0
  103. package/dist/scripts/booking-recovery-chain-tests.js +182 -0
  104. package/dist/scripts/booking-saga-tests.js +125 -0
  105. package/dist/scripts/booking-surface-contract-proof-tests.js +212 -0
  106. package/dist/scripts/bootstrap-tests.js +1479 -0
  107. package/dist/scripts/bridge-events-tests.js +239 -0
  108. package/dist/scripts/bridge-ledger-tests.js +428 -0
  109. package/dist/scripts/build-changelog.js +238 -0
  110. package/dist/scripts/build-metrics-report.js +331 -0
  111. package/dist/scripts/changelog-tests.js +118 -0
  112. package/dist/scripts/channel-probe-tests.js +103 -0
  113. package/dist/scripts/channel-probe.js +169 -0
  114. package/dist/scripts/channel-registry-tests.js +208 -0
  115. package/dist/scripts/companion-tests.js +25 -70
  116. package/dist/scripts/dida-cookie-probe.js +87 -0
  117. package/dist/scripts/dida-runner-stop-tests.js +311 -0
  118. package/dist/scripts/dida-sw-probe.js +114 -0
  119. package/dist/scripts/diff-test.js +17 -12
  120. package/dist/scripts/doctor-tests.js +564 -0
  121. package/dist/scripts/dsh-artifact-e2e.js +279 -0
  122. package/dist/scripts/dsh-artifact-web-e2e.js +675 -0
  123. package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
  124. package/dist/scripts/dsh-runtime-closure-tests.js +182 -0
  125. package/dist/scripts/dsh-runtime-closure.js +162 -0
  126. package/dist/scripts/dsh-session-v3-migration-proof.js +109 -0
  127. package/dist/scripts/dsh-subprocess-local-proof.js +150 -0
  128. package/dist/scripts/dsh-target-closure-proof.js +178 -0
  129. package/dist/scripts/effect-tests.js +539 -0
  130. package/dist/scripts/engine-run.js +1 -3
  131. package/dist/scripts/engine-tests.js +16 -10
  132. package/dist/scripts/evaluation-cadence-tests.js +174 -0
  133. package/dist/scripts/evaluation-contract-tests.js +268 -0
  134. package/dist/scripts/extension-distribution-cli.js +39 -0
  135. package/dist/scripts/extension-distribution-tests.js +443 -0
  136. package/dist/scripts/extension-tests.js +988 -0
  137. package/dist/scripts/external-event-tests.js +350 -0
  138. package/dist/scripts/fact-gate-tests.js +869 -0
  139. package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
  140. package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
  141. package/dist/scripts/flight-malformed-tests.js +159 -0
  142. package/dist/scripts/flyai-setup-tests.js +228 -0
  143. package/dist/scripts/flyai-setup-tool-tests.js +54 -0
  144. package/dist/scripts/flyai-tests.js +692 -0
  145. package/dist/scripts/flyai-tool-registration-contract-tests.js +91 -0
  146. package/dist/scripts/fx-contract-tests.js +201 -0
  147. package/dist/scripts/g5-guard-tests.js +131 -0
  148. package/dist/scripts/g5-guard.js +189 -0
  149. package/dist/scripts/gen-lunar-anchors.js +106 -0
  150. package/dist/scripts/geo-atlas-tests.js +283 -0
  151. package/dist/scripts/gotry-backend-kernel-tests.js +206 -0
  152. package/dist/scripts/gotry-try-demo-tests.js +79 -0
  153. package/dist/scripts/gotry-try-demo.js +49 -0
  154. package/dist/scripts/ground-transfer-tests.js +956 -0
  155. package/dist/scripts/hbcli-e2e-tests.js +159 -0
  156. package/dist/scripts/hbcli-live-optin-tests.js +96 -0
  157. package/dist/scripts/hbcli-release-contract-tests.js +122 -0
  158. package/dist/scripts/hbcli-tests.js +121 -69
  159. package/dist/scripts/health-watch-cli.js +73 -0
  160. package/dist/scripts/hotel-date-gate-tests.js +496 -0
  161. package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
  162. package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
  163. package/dist/scripts/i18n-tests.js +41 -0
  164. package/dist/scripts/incident-tests.js +281 -87
  165. package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
  166. package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
  167. package/dist/scripts/issue-271-liveness-tests.js +569 -0
  168. package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
  169. package/dist/scripts/issue-289-model-retry-tests.js +441 -0
  170. package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
  171. package/dist/scripts/issue-338-home-city-e2e.js +577 -0
  172. package/dist/scripts/issue343-real-entry-e2e.js +160 -0
  173. package/dist/scripts/issue436-persisted-routing-e2e.js +247 -0
  174. package/dist/scripts/itinerary-artifact-tests.js +368 -0
  175. package/dist/scripts/itinerary-deck-artifact-tests.js +416 -0
  176. package/dist/scripts/itinerary-deck-export-tests.js +552 -0
  177. package/dist/scripts/itinerary-deck-tests.js +435 -0
  178. package/dist/scripts/itinerary-html-tests.js +426 -0
  179. package/dist/scripts/journey-tests.js +22 -27
  180. package/dist/scripts/kernel-manifest-evidence.js +109 -0
  181. package/dist/scripts/kernel-manifest-gate.js +126 -0
  182. package/dist/scripts/kernel-manifest-tests.js +212 -0
  183. package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
  184. package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
  185. package/dist/scripts/kernel-manifest.js +369 -0
  186. package/dist/scripts/lavish-local-tests.js +1226 -0
  187. package/dist/scripts/lavish-product-tools-tests.js +537 -0
  188. package/dist/scripts/ledger-repair-apply-tests.js +316 -0
  189. package/dist/scripts/ledger-repair-plan-tests.js +375 -0
  190. package/dist/scripts/ledger-tests.js +382 -246
  191. package/dist/scripts/ledger-workflow-crash.js +10 -20
  192. package/dist/scripts/managed-dsh-cleanup-diagnostic-proof.js +184 -0
  193. package/dist/scripts/managed-dsh-close-failure-proof.js +172 -0
  194. package/dist/scripts/managed-dsh-run-port-proof.js +153 -0
  195. package/dist/scripts/managed-dsh-terminal-proof.js +252 -0
  196. package/dist/scripts/map-tools-vendor-package-proof.js +365 -0
  197. package/dist/scripts/memory-capture-tests.js +242 -61
  198. package/dist/scripts/memory-decay-tests.js +18 -52
  199. package/dist/scripts/memory-lifecycle-tests.js +657 -0
  200. package/dist/scripts/memory-lifecycle.js +196 -0
  201. package/dist/scripts/memory-metrics.js +24 -12
  202. package/dist/scripts/memory-value-report.js +631 -0
  203. package/dist/scripts/memory-value-tests.js +328 -0
  204. package/dist/scripts/metrics-report-tests.js +151 -0
  205. package/dist/scripts/model-override-e2e.js +137 -0
  206. package/dist/scripts/nightly-evidence-tests.js +161 -0
  207. package/dist/scripts/nightly-evidence.js +286 -0
  208. package/dist/scripts/nudge-digest.js +39 -28
  209. package/dist/scripts/observability-tests.js +381 -0
  210. package/dist/scripts/onboarding-tests.js +161 -0
  211. package/dist/scripts/opensky-check.js +18 -13
  212. package/dist/scripts/opensky-tests.js +28 -28
  213. package/dist/scripts/persona-surface-guard-tests.js +49 -0
  214. package/dist/scripts/pnpm-dsh-closure-proof.js +17 -0
  215. package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
  216. package/dist/scripts/price-drift-tests.js +340 -0
  217. package/dist/scripts/price-drift-watch.js +447 -0
  218. package/dist/scripts/probe-poi-tests.js +53 -45
  219. package/dist/scripts/product-metrics.js +483 -0
  220. package/dist/scripts/publish-preverify.js +110 -0
  221. package/dist/scripts/realtime-pricing-tests.js +76 -0
  222. package/dist/scripts/recall-tests.js +513 -0
  223. package/dist/scripts/replay-async.js +20 -18
  224. package/dist/scripts/replay-real.js +18 -18
  225. package/dist/scripts/replay.js +52 -82
  226. package/dist/scripts/session-attach-diagnose.js +20 -20
  227. package/dist/scripts/session-attach-poc.js +34 -37
  228. package/dist/scripts/session-benchmark.js +180 -0
  229. package/dist/scripts/session-dida-live-e2e.js +199 -0
  230. package/dist/scripts/session-extract-tests.js +14 -35
  231. package/dist/scripts/session-link-tests.js +478 -0
  232. package/dist/scripts/session-login.js +27 -46
  233. package/dist/scripts/session-tests.js +796 -96
  234. package/dist/scripts/session-zones-tests.js +358 -0
  235. package/dist/scripts/sf-live-benchmark.js +367 -0
  236. package/dist/scripts/sf-live-challenge-stop-tests.js +453 -0
  237. package/dist/scripts/sf-live-cli-tests.js +92 -0
  238. package/dist/scripts/sf-live-options.js +46 -0
  239. package/dist/scripts/sf-manifest-tests.js +30 -0
  240. package/dist/scripts/sf-manifest.js +33 -0
  241. package/dist/scripts/sf-soft-score-tests.js +67 -0
  242. package/dist/scripts/sf-summary-tests.js +324 -0
  243. package/dist/scripts/sf-summary.js +525 -0
  244. package/dist/scripts/share-tests.js +432 -0
  245. package/dist/scripts/skeleton-check.js +16 -14
  246. package/dist/scripts/skeleton-integration-test.js +28 -11
  247. package/dist/scripts/skills-contract-tests.js +51 -46
  248. package/dist/scripts/smoke-session-gate-tests.js +21 -0
  249. package/dist/scripts/smoke.js +690 -246
  250. package/dist/scripts/spawn-bounded-abort-tests.js +241 -0
  251. package/dist/scripts/sponsor-reuse-tests.js +262 -0
  252. package/dist/scripts/state-cli-tests.js +226 -101
  253. package/dist/scripts/state-cli.js +550 -199
  254. package/dist/scripts/state-repair-cli.js +66 -0
  255. package/dist/scripts/state-repair-tests.js +140 -0
  256. package/dist/scripts/static-golden-tests.js +165 -0
  257. package/dist/scripts/time-eval-tests.js +210 -192
  258. package/dist/scripts/travel-timeline-tests.js +29 -93
  259. package/dist/scripts/turn-handoff-collect-tests.js +169 -0
  260. package/dist/scripts/turn-handoff-collect.js +161 -0
  261. package/dist/scripts/turn-policy-tests.js +55 -0
  262. package/dist/scripts/typed-contract-canary.js +176 -0
  263. package/dist/scripts/tz-resolver-sanity.js +50 -0
  264. package/dist/scripts/tz-resolver-tests.js +584 -0
  265. package/dist/scripts/unified-tests.js +23 -19
  266. package/dist/scripts/visa-policy-tests.js +62 -0
  267. package/dist/scripts/weather-tests.js +276 -41
  268. package/dist/scripts/wish-channel-gate-tests.js +54 -0
  269. package/dist/scripts/write-gate-tests.js +522 -0
  270. package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
  271. package/dist/scripts/z3-lifecycle-tests.js +170 -0
  272. package/dist/scripts/z3-race-repeat-tests.js +39 -0
  273. package/dist/scripts/z3-race-tests.js +41 -0
  274. package/dist/src/artifact-gate.js +829 -0
  275. package/dist/src/backend/kernel.js +91 -0
  276. package/dist/src/backend/modules/booking-copilot.js +92 -0
  277. package/dist/src/backend/modules/booking-executor.js +124 -0
  278. package/dist/src/backend/modules/session-search.js +551 -0
  279. package/dist/src/benchmark-agent-conformance.js +578 -0
  280. package/dist/src/benchmark-environment-bridge.js +515 -0
  281. package/dist/src/benchmark-headless-child-diagnostics.js +166 -0
  282. package/dist/src/benchmark-tool-isolation.js +151 -0
  283. package/dist/src/bookable-facts.js +533 -0
  284. package/dist/src/booking-saga.js +97 -0
  285. package/dist/src/booking-surface/availability-policy.js +697 -0
  286. package/dist/src/booking-surface/booking-intent.js +43 -0
  287. package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
  288. package/dist/src/booking-surface/canonical-schema.js +116 -0
  289. package/dist/src/booking-surface/contracts.js +75 -0
  290. package/dist/src/booking-surface/dsh-planner.js +1854 -0
  291. package/dist/src/booking-surface/dsh-plugin.js +378 -0
  292. package/dist/src/booking-surface/dsh-worker.js +98 -0
  293. package/dist/src/booking-surface/error-codes.js +231 -0
  294. package/dist/src/booking-surface/index.js +9 -0
  295. package/dist/src/booking-surface/managed-dsh-cleanup-diagnostic.js +97 -0
  296. package/dist/src/booking-surface/managed-dsh-run-port.js +244 -0
  297. package/dist/src/booking-surface/profile.js +24 -0
  298. package/dist/src/booking-surface/recovery-chain.js +367 -0
  299. package/dist/src/booking-surface/runtime.js +1968 -0
  300. package/dist/src/booking-surface/server.js +674 -0
  301. package/dist/src/booking-surface/startup.js +168 -0
  302. package/dist/src/booking-surface/validation.js +261 -0
  303. package/dist/src/bridge.js +20 -13
  304. package/dist/src/companions.js +48 -74
  305. package/dist/src/contracts.js +21 -43
  306. package/dist/src/dsh-llm.js +97 -111
  307. package/dist/src/engine.js +276 -193
  308. package/dist/src/evaluation-cadence.js +97 -0
  309. package/dist/src/evaluation-contracts.js +283 -0
  310. package/dist/src/flight-pack-adapter.js +54 -0
  311. package/dist/src/flight-pack-contract.js +2 -0
  312. package/dist/src/flyai-setup-tool.js +70 -0
  313. package/dist/src/gotry-backend.js +125 -0
  314. package/dist/src/hotel-date-gate.js +118 -0
  315. package/dist/src/i18n.js +88 -0
  316. package/dist/src/index.js +2227 -882
  317. package/dist/src/itinerary-deck.js +139 -0
  318. package/dist/src/itinerary-doc-shared.js +661 -0
  319. package/dist/src/itinerary-html.js +131 -0
  320. package/dist/src/journey.js +155 -108
  321. package/dist/src/lavish-tools.js +400 -0
  322. package/dist/src/ledger-repair-apply.js +538 -0
  323. package/dist/src/ledger-repair-plan.js +463 -0
  324. package/dist/src/loop.js +319 -160
  325. package/dist/src/memory-capture.js +180 -32
  326. package/dist/src/memory-decay.js +30 -14
  327. package/dist/src/memory-lifecycle.js +1310 -0
  328. package/dist/src/memory-utility.js +27 -40
  329. package/dist/src/mock-llm.js +36 -61
  330. package/dist/src/model.js +81 -36
  331. package/dist/src/realtime-pricing.js +103 -0
  332. package/dist/src/recall/card.js +74 -0
  333. package/dist/src/recall/evaluator.js +140 -0
  334. package/dist/src/recall/integration.js +17 -0
  335. package/dist/src/recall/tick.js +166 -0
  336. package/dist/src/session-link/plan-it-action.js +48 -0
  337. package/dist/src/session-link/session-link.js +248 -0
  338. package/dist/src/session-zones.js +507 -0
  339. package/dist/src/share/adapters.js +51 -0
  340. package/dist/src/share/share-consent.js +76 -0
  341. package/dist/src/share/share-deck.js +58 -0
  342. package/dist/src/share/share-token.js +146 -0
  343. package/dist/src/slot-spec.js +84 -106
  344. package/dist/src/state-ledger.js +435 -516
  345. package/dist/src/state-repair.js +299 -0
  346. package/dist/src/subagent-job-id-guard.js +58 -0
  347. package/dist/src/time-anchor.js +231 -33
  348. package/dist/src/tool-packet.js +20 -8
  349. package/dist/src/travel-slots.js +58 -35
  350. package/dist/src/travel-timeline.js +44 -63
  351. package/dist/src/turn-deadline.js +287 -0
  352. package/dist/src/turn-policy.js +118 -0
  353. package/dist/src/tz-resolver.js +143 -0
  354. package/dist/src/unified.js +535 -359
  355. package/dist/src/wish-pool.js +42 -12
  356. package/dist/src/write-gate.js +495 -0
  357. package/dist/src/z3-shared.js +284 -0
  358. package/extension/README.md +21 -0
  359. package/extension/README.zh-CN.md +21 -0
  360. package/extension/background.js +463 -0
  361. package/extension/content-bridge.js +270 -0
  362. package/extension/content-main.js +111 -0
  363. package/extension/manifest.json +60 -0
  364. package/package.json +407 -14
  365. package/schemas/booking.intent.schema.json +193 -0
  366. package/schemas/booking.surface.schema.json +2622 -0
  367. package/ts/capabilities/agent-reach-deep.ts +114 -0
  368. package/ts/capabilities/agent-reach.ts +198 -0
  369. package/ts/capabilities/anything.ts +255 -0
  370. package/ts/capabilities/artifacts.ts +428 -0
  371. package/ts/capabilities/fact-log.ts +54 -0
  372. package/ts/capabilities/flyai-config.ts +388 -0
  373. package/ts/capabilities/flyai.ts +1274 -0
  374. package/ts/capabilities/hbcli.ts +253 -71
  375. package/ts/capabilities/incident-log.ts +88 -59
  376. package/ts/capabilities/itinerary-artifact.ts +214 -0
  377. package/ts/capabilities/itinerary-deck-artifact.ts +218 -0
  378. package/ts/capabilities/itinerary-deck-export.ts +378 -0
  379. package/ts/capabilities/opensky.ts +121 -0
  380. package/ts/capabilities/session/action-cache.ts +148 -0
  381. package/ts/capabilities/session/adapters/ctrip-flight.ts +181 -0
  382. package/ts/capabilities/session/adapters/ctrip-hotel.ts +238 -0
  383. package/ts/capabilities/session/adapters/dida-portal.ts +338 -0
  384. package/ts/capabilities/session/adapters/meituan-local.ts +70 -0
  385. package/ts/capabilities/session/adapters/rail-12306.ts +392 -0
  386. package/ts/capabilities/session/benchmark.ts +276 -0
  387. package/ts/capabilities/session/bridge-ledger.ts +333 -0
  388. package/ts/capabilities/session/extension-bridge.ts +794 -0
  389. package/ts/capabilities/session/extension-channel.ts +144 -0
  390. package/ts/capabilities/session/extension-distribution.ts +266 -0
  391. package/ts/capabilities/session/extract.ts +54 -0
  392. package/ts/capabilities/session/golden-score.ts +139 -0
  393. package/ts/capabilities/session/health-watch.ts +264 -0
  394. package/ts/capabilities/session/read-guard.ts +141 -0
  395. package/ts/capabilities/session/static-flight-golden.ts +209 -0
  396. package/ts/capabilities/session/transport.ts +146 -0
  397. package/ts/capabilities/session/wizard.ts +140 -0
  398. package/ts/capabilities/session-consent.ts +211 -0
  399. package/ts/capabilities/session-login.ts +208 -0
  400. package/ts/capabilities/session-search.ts +893 -0
  401. package/ts/capabilities/weather.ts +330 -0
  402. package/ts/dsh-runtime/vendor/README.md +68 -0
  403. package/ts/dsh-runtime/vendor/dsh-map-tools/LICENSE +21 -0
  404. package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +405 -0
  405. package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +404 -0
  406. package/ts/dsh-runtime/vendor/dsh-map-tools/client/client.js +1584 -0
  407. package/ts/dsh-runtime/vendor/dsh-map-tools/cordis.patch.yml +5 -0
  408. package/ts/dsh-runtime/vendor/dsh-map-tools/icon.svg +5 -0
  409. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/amap.js +595 -0
  410. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/nominatim.js +63 -0
  411. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/osrm.js +58 -0
  412. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/photon.js +55 -0
  413. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-file.js +87 -0
  414. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-route.js +131 -0
  415. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config.js +16 -0
  416. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/geo.js +232 -0
  417. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/index.js +120 -0
  418. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +40 -0
  419. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/staticmap-route.js +115 -0
  420. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/geocode.js +127 -0
  421. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/poi.js +84 -0
  422. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/routes.js +267 -0
  423. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/amap.d.ts +90 -0
  424. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/nominatim.d.ts +17 -0
  425. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/osrm.d.ts +15 -0
  426. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/photon.d.ts +22 -0
  427. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-file.d.ts +33 -0
  428. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-route.d.ts +23 -0
  429. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config.d.ts +23 -0
  430. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/geo.d.ts +61 -0
  431. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/index.d.ts +18 -0
  432. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +14 -0
  433. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/staticmap-route.d.ts +31 -0
  434. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/geocode.d.ts +11 -0
  435. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/poi.d.ts +9 -0
  436. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/routes.d.ts +75 -0
  437. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/types.d.ts +68 -0
  438. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types.js +16 -0
  439. package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +97 -0
  440. package/ts/package.json +293 -3
  441. package/ts/scripts/async-collect.ts +91 -0
  442. package/ts/scripts/map-tools-vendor-package-proof.ts +404 -0
  443. package/ts/scripts/skeleton-check.ts +1 -1
  444. package/ts/scripts/skeleton-integration-test.ts +23 -1
  445. package/ts/scripts/state-cli.ts +568 -0
  446. package/ts/scripts/turn-handoff-collect.ts +178 -0
  447. package/ts/src/artifact-gate.ts +991 -0
  448. package/ts/src/benchmark-agent-conformance.ts +619 -0
  449. package/ts/src/benchmark-environment-bridge.ts +538 -0
  450. package/ts/src/benchmark-headless-child-diagnostics.ts +184 -0
  451. package/ts/src/benchmark-tool-isolation.ts +166 -0
  452. package/ts/src/bookable-facts.ts +837 -0
  453. package/ts/src/booking-saga.ts +133 -0
  454. package/ts/src/booking-surface/availability-policy.ts +522 -0
  455. package/ts/src/booking-surface/booking-intent.ts +86 -0
  456. package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
  457. package/ts/src/booking-surface/canonical-schema.js +116 -0
  458. package/ts/src/booking-surface/contracts.ts +475 -0
  459. package/ts/src/booking-surface/dsh-planner.ts +1949 -0
  460. package/ts/src/booking-surface/dsh-plugin.js +378 -0
  461. package/ts/src/booking-surface/dsh-worker.ts +110 -0
  462. package/ts/src/booking-surface/error-codes.ts +241 -0
  463. package/ts/src/booking-surface/index.ts +9 -0
  464. package/ts/src/booking-surface/managed-dsh-cleanup-diagnostic.ts +151 -0
  465. package/ts/src/booking-surface/managed-dsh-run-port.ts +273 -0
  466. package/ts/src/booking-surface/profile.ts +42 -0
  467. package/ts/src/booking-surface/recovery-chain.ts +409 -0
  468. package/ts/src/booking-surface/runtime.ts +1793 -0
  469. package/ts/src/booking-surface/server.ts +733 -0
  470. package/ts/src/booking-surface/startup.ts +201 -0
  471. package/ts/src/booking-surface/validation.ts +217 -0
  472. package/ts/src/companions.ts +113 -0
  473. package/ts/src/contracts.ts +13 -3
  474. package/ts/src/dsh-llm.ts +44 -11
  475. package/ts/src/flyai-setup-tool.ts +77 -0
  476. package/ts/src/index.ts +1948 -198
  477. package/ts/src/itinerary-deck.ts +189 -0
  478. package/ts/src/itinerary-doc-shared.ts +741 -0
  479. package/ts/src/itinerary-html.ts +171 -0
  480. package/ts/src/loop.ts +223 -25
  481. package/ts/src/memory-capture.ts +283 -0
  482. package/ts/src/memory-decay.ts +55 -0
  483. package/ts/src/memory-utility.ts +70 -0
  484. package/ts/src/mock-llm.ts +4 -8
  485. package/ts/src/model.ts +88 -2
  486. package/ts/src/recall/card.ts +106 -0
  487. package/ts/src/recall/evaluator.ts +170 -0
  488. package/ts/src/recall/integration.ts +35 -0
  489. package/ts/src/recall/tick.ts +213 -0
  490. package/ts/src/session-link/plan-it-action.ts +78 -0
  491. package/ts/src/session-link/session-link.ts +275 -0
  492. package/ts/src/share/adapters.ts +93 -0
  493. package/ts/src/share/share-consent.ts +105 -0
  494. package/ts/src/share/share-deck.ts +85 -0
  495. package/ts/src/share/share-token.ts +166 -0
  496. package/ts/src/slot-spec.ts +172 -0
  497. package/ts/src/state-ledger.ts +912 -0
  498. package/ts/src/subagent-job-id-guard.ts +72 -0
  499. package/ts/src/time-anchor.ts +338 -0
  500. package/ts/src/tool-packet.ts +32 -0
  501. package/ts/src/travel-slots.ts +219 -0
  502. package/ts/src/travel-timeline.ts +83 -0
  503. package/ts/src/turn-deadline.ts +366 -0
  504. package/ts/src/turn-policy.ts +154 -0
  505. package/ts/src/unified.ts +354 -88
  506. package/ts/src/wish-pool.ts +79 -0
  507. package/dist/scripts/session-attach-wait.js +0 -41
@@ -0,0 +1,1274 @@
1
+ /**
2
+ * FlyAI 官方通道能力层(飞猪开放平台,data-sources.md §8 / RFC P0;issue #521)。
3
+ *
4
+ * 完整对接官方 @fly-ai/flyai-cli 1.0.16 的 8 个公开只读命令:
5
+ * flight/train: search-flight / search-train
6
+ * hotel: search-hotel(别名 search-hotels)
7
+ * poi: search-poi
8
+ * keyword: keyword-search(别名 fliggy-fast-search)
9
+ * ai: ai-search
10
+ * marriott: search-marriott-hotel / search-marriott-package
11
+ *
12
+ * 链路(CLI spawn,与 hbcli/weather 同构):
13
+ * gotry capabilities/flyai.ts → npx -y @fly-ai/flyai-cli <command> [opts]
14
+ * → 飞猪 MCP(实时直连官方商品库)
15
+ *
16
+ * 契约(L4 不变量):
17
+ * - 只读:8 命令全只读,交易经 jumpUrl/detailUrl 由人完成(WriteGate 同构);
18
+ * - 永不抛错:网络/超时/解析失败一律降级为结构化 verdict;
19
+ * - 证据链:成功 [实时API:flyai@ts];失败 [实时API:flyai@error@ts];
20
+ * - key 解析/endpoint 覆盖口径与官方 CLI 同源(flyai-config.ts);
21
+ * - 子进程资源有界:进程组边界统一复用 spawn-bounded.ts(与 hbcli/anything
22
+ * 同策略),主进程退出后 stderr drain 最多等 drainMs(默认 3s),随后杀残留
23
+ * 后代并以 drain-timeout fail-closed 返回——不被持管道的后代挂死,也不把
24
+ * 不完整收集写成成功库存事实。
25
+ *
26
+ * 故障闭集(verdict):
27
+ * hit / miss / auth-error(401)/ forbidden(403)/
28
+ * needs-setup(匿名试用 429 Trial limit)/ rate-limited(普通 429)/
29
+ * timeout / error(网络/Sentinel/malformed)。
30
+ */
31
+
32
+ import { closeSync, openSync, readFileSync, unlinkSync } from 'node:fs'
33
+ import { tmpdir } from 'node:os'
34
+ import { join } from 'node:path'
35
+ import { resolveFlyaiEndpoint, resolveFlyaiKey, type FlyaiKeySource } from './flyai-config.ts'
36
+ import { spawnBounded } from './spawn-bounded.ts'
37
+
38
+ export type FlyaiKind =
39
+ | 'flight'
40
+ | 'train'
41
+ | 'hotel'
42
+ | 'poi'
43
+ | 'keyword'
44
+ | 'ai'
45
+ | 'marriott-hotel'
46
+ | 'marriott-package'
47
+
48
+ export type FlyaiVerdict =
49
+ | 'hit'
50
+ | 'miss'
51
+ | 'error'
52
+ | 'needs-setup'
53
+ | 'auth-error'
54
+ | 'forbidden'
55
+ | 'rate-limited'
56
+ | 'timeout'
57
+ | 'cancelled'
58
+
59
+ /** 本机子进程生命周期终止;它不是可重试的上游故障。 */
60
+ export type FlyaiLocalTermination = 'deadline' | 'signal' | 'spawn' | 'empty-exit' | 'drain-timeout'
61
+
62
+ export interface FlyaiQuery {
63
+ kind: FlyaiKind
64
+
65
+ // ── flight / train ──
66
+ /** 出发城市/机场(中文或 ID);官方唯一必填 */
67
+ origin?: string
68
+ destination?: string
69
+ /** 出发日 YYYY-MM-DD */
70
+ depDate?: string
71
+ depDateStart?: string
72
+ depDateEnd?: string
73
+ /** 回程日 YYYY-MM-DD */
74
+ backDate?: string
75
+ /** 1=直达 2=中转 */
76
+ journeyType?: number
77
+ /** 舱位/坐席(逗号分隔) */
78
+ seatClassName?: string
79
+ /** 航班号/车次(逗号分隔) */
80
+ transportNo?: string
81
+ /** 中转城市(逗号分隔) */
82
+ transferCity?: string
83
+ depHourStart?: number
84
+ depHourEnd?: number
85
+ arrHourStart?: number
86
+ arrHourEnd?: number
87
+ /** 总时长上限(小时) */
88
+ totalDurationHour?: number
89
+ /** 最高价 CNY */
90
+ maxPrice?: number
91
+ /** 机票/火车排序 1–8;万豪套餐 price_asc/price_desc */
92
+ sortType?: string
93
+
94
+ // ── hotel / marriott-hotel ──
95
+ /** 目的地(国家/省/市/区中文) */
96
+ destName?: string
97
+ checkInDate?: string
98
+ checkOutDate?: string
99
+ /** 商业区/地标/酒店名关键词 */
100
+ keyWords?: string
101
+ /** 周边景点名 */
102
+ poiName?: string
103
+ /** 酒店/民宿/客栈 */
104
+ hotelTypes?: string
105
+ /** distance_asc / rate_desc / price_asc / price_desc / no_rank */
106
+ sort?: string
107
+ /** 1–5 逗号分隔 */
108
+ hotelStars?: string
109
+ /** king/twin/multi 逗号分隔 */
110
+ hotelBedTypes?: string
111
+
112
+ // ── poi ──
113
+ /** 景点所在城市(必填) */
114
+ cityName?: string
115
+ /** 景点等级 1–5 */
116
+ poiLevel?: number
117
+ /** 景点名关键词 */
118
+ keyword?: string
119
+ /** 景点类别(闭集,见 FLYAI_POI_CATEGORIES) */
120
+ category?: string
121
+
122
+ // ── keyword-search / ai-search ──
123
+ query?: string
124
+
125
+ // ── marriott-hotel only ──
126
+ hotelBrands?: string
127
+ hotelName?: string
128
+ /** 万豪套餐的省份或城市筛选。 */
129
+ provinceOrCity?: string
130
+
131
+ // ── 运行面 ──
132
+ /** 默认 30_000 ms(npx 冷启动 + 远端检索) */
133
+ timeoutMs?: number
134
+ /** 主进程 exit 后等待 stderr 管道释放的上限(默认 3_000 ms);超时杀组并 fail-closed */
135
+ drainMs?: number
136
+ /** 显式 CLI bin/命令(默认 npx;测试注入假脚本) */
137
+ cliBin?: string
138
+ /** cliBin 之后的前缀参数(默认 ['-y','@fly-ai/flyai-cli']) */
139
+ cliPrefixArgs?: string[]
140
+ /** 取消信号:aborted 时不 spawn;在途 abort 杀整组进程,verdict='cancelled' */
141
+ signal?: AbortSignal
142
+ }
143
+
144
+ export interface FlyaiOption {
145
+ no: string
146
+ name: string
147
+ depDateTime: string
148
+ arrDateTime: string
149
+ depStation: string
150
+ arrStation: string
151
+ durationMin: number
152
+ price: number
153
+ seatClass?: string
154
+ jumpUrl?: string
155
+ priceRaw?: string
156
+ nonstop?: boolean
157
+ journeyType?: string
158
+ /** 保留官方同票联程的完整分段,避免把中转首段冒充全程。 */
159
+ segments?: FlyaiTransportSegment[]
160
+ }
161
+
162
+ export interface FlyaiTransportSegment {
163
+ no: string
164
+ name: string
165
+ depDateTime: string
166
+ arrDateTime: string
167
+ depStation: string
168
+ arrStation: string
169
+ durationMin: number
170
+ seatClass?: string
171
+ }
172
+
173
+ export interface FlyaiHotelOption {
174
+ name: string
175
+ star?: string
176
+ price: number
177
+ priceRaw?: string
178
+ rate?: string
179
+ address?: string
180
+ poi?: string
181
+ hotelId?: string
182
+ jumpUrl?: string
183
+ mainPic?: string
184
+ score?: string
185
+ scoreDesc?: string
186
+ review?: string
187
+ brandName?: string
188
+ latitude?: string
189
+ longitude?: string
190
+ }
191
+
192
+ export interface FlyaiPoiTicketInfo {
193
+ price?: string | null
194
+ priceDate?: string
195
+ ticketName?: string
196
+ }
197
+
198
+ export interface FlyaiPoiOption {
199
+ name: string
200
+ poiId?: string
201
+ mainPic?: string
202
+ jumpUrl?: string
203
+ address?: string
204
+ freePoiStatus?: string
205
+ ticketInfo?: FlyaiPoiTicketInfo
206
+ }
207
+
208
+ export interface FlyaiKeywordOption {
209
+ title: string
210
+ jumpUrl?: string
211
+ picUrl?: string
212
+ price?: string
213
+ scoreDesc?: string
214
+ star?: string
215
+ tags?: string[]
216
+ }
217
+
218
+ export interface FlyaiMarriottPackageOption {
219
+ name: string
220
+ /** Official CLI field; name is retained as the normalized compatibility field. */
221
+ title?: string
222
+ itemId?: string
223
+ brandName?: string
224
+ hotelName?: string
225
+ cityName?: string
226
+ price?: string
227
+ detailUrl?: string
228
+ mainPic?: string
229
+ picUrl?: string
230
+ sellingPoint?: string
231
+ sellPoint?: string
232
+ }
233
+
234
+ export interface FlyaiResult {
235
+ ok: boolean
236
+ via: 'flyai' | 'flyai-error'
237
+ evidence: string
238
+ latencyMs: number
239
+ verdict: FlyaiVerdict
240
+ /** true = 已识别的上游普通网络/HTTP 5xx/429 瞬时失败,可交给 effect 层退避重试 */
241
+ retryable?: boolean
242
+ /** 存在时表示请求未得到可判定的上游响应,应 fail-closed 且只执行一次。 */
243
+ localTermination?: FlyaiLocalTermination
244
+ kind: FlyaiKind
245
+ options?: FlyaiOption[]
246
+ hotels?: FlyaiHotelOption[]
247
+ pois?: FlyaiPoiOption[]
248
+ keywords?: FlyaiKeywordOption[]
249
+ packages?: FlyaiMarriottPackageOption[]
250
+ /** ai-search 上游自由形状数据(原样透传,不猜成事实) */
251
+ aiData?: unknown
252
+ error?: string
253
+ /** 恢复/补配指引(verdict≠hit/miss 时) */
254
+ setup?: string
255
+ /** 本次调用实际生效的 key 来源 */
256
+ keySource?: FlyaiKeySource
257
+ /** true = endpoint 被 DEBUG_FLYAI_MCP_URL 覆盖 */
258
+ endpointDebug?: boolean
259
+ systemMessage?: string
260
+ }
261
+
262
+ interface RawItem {
263
+ journeys?: Array<{
264
+ journeyType?: string | number
265
+ totalDuration?: string | number
266
+ segments?: Array<{
267
+ marketingTransportName?: string
268
+ marketingTransportNo?: string
269
+ depDateTime?: string
270
+ arrDateTime?: string
271
+ depStationName?: string
272
+ arrStationName?: string
273
+ duration?: string | number
274
+ seatClassName?: string
275
+ }>
276
+ }>
277
+ adultPrice?: string
278
+ ticketPrice?: string
279
+ price?: string
280
+ jumpUrl?: string
281
+ // hotel
282
+ name?: string
283
+ shId?: string
284
+ shid?: string
285
+ star?: string
286
+ rate?: string | null
287
+ address?: string
288
+ interestsPoi?: string
289
+ nearbyPoi?: string
290
+ detailUrl?: string
291
+ mainPic?: string
292
+ score?: string | null
293
+ scoreDesc?: string
294
+ review?: string
295
+ brandName?: string
296
+ latitude?: string
297
+ longitude?: string
298
+ // poi
299
+ id?: string | number
300
+ freePoiStatus?: string
301
+ ticketInfo?: unknown
302
+ // keyword
303
+ info?: unknown
304
+ // marriott package
305
+ title?: string
306
+ itemId?: string
307
+ picUrl?: string
308
+ sellPoint?: string
309
+ hotelName?: string
310
+ cityName?: string
311
+ sellingPoint?: string
312
+ }
313
+
314
+ interface ParsedItems<T> {
315
+ options: T[]
316
+ malformedCount: number
317
+ }
318
+
319
+ function nonEmptyText(value: unknown): string | undefined {
320
+ if (typeof value !== 'string') return undefined
321
+ const text = value.trim()
322
+ return text || undefined
323
+ }
324
+
325
+ function optionalText(value: unknown, allowNull = false): string | undefined | null {
326
+ if (value === undefined) return undefined
327
+ if (allowNull && value === null) return null
328
+ return nonEmptyText(value)
329
+ }
330
+
331
+ function positiveFiniteNumber(value: unknown): number | undefined {
332
+ const number = typeof value === 'number'
333
+ ? value
334
+ : typeof value === 'string' && value.trim() ? Number(value.trim()) : Number.NaN
335
+ return Number.isFinite(number) && number > 0 ? number : undefined
336
+ }
337
+
338
+ function positiveFiniteDuration(value: unknown): number | undefined {
339
+ if (typeof value === 'number') return positiveFiniteNumber(value)
340
+ const text = nonEmptyText(value)
341
+ if (!text) return undefined
342
+ const minutes = /^(\d+(?:\.\d+)?)\s*分钟$/.exec(text)?.[1]
343
+ return positiveFiniteNumber(minutes ?? text)
344
+ }
345
+
346
+ function parseTransportPrice(value: unknown): { price: number; priceRaw?: string } | undefined {
347
+ const raw = nonEmptyText(value)
348
+ if (!raw) return undefined
349
+ const bare = raw.replace(/^¥\s*/, '')
350
+ if (/^\d+(?:\.\d+)?$/.test(bare)) {
351
+ const price = Number(bare)
352
+ return Number.isFinite(price) && price > 0 ? { price } : undefined
353
+ }
354
+ // 未鉴权态打码 1xxx/¥7xx:保留原值,绝不猜成数字。
355
+ if (/^\d+x+$/.test(bare)) return { price: 0, priceRaw: raw }
356
+ return undefined
357
+ }
358
+
359
+ /**
360
+ * 从 CLI stdout 提取首个含 data.itemList 的完整 JSON 对象。
361
+ * 扫描器识别字符串与转义;npx 前后缀日志不影响切分。
362
+ */
363
+ export function parseFlyaiItemList(stdout: string): unknown[] {
364
+ let start = -1
365
+ let depth = 0
366
+ let inString = false
367
+ let escaped = false
368
+ let sawIncompleteObject = false
369
+
370
+ for (let index = 0; index < stdout.length; index += 1) {
371
+ const char = stdout[index]!
372
+ if (start < 0) {
373
+ if (char === '{') {
374
+ start = index
375
+ depth = 1
376
+ inString = false
377
+ escaped = false
378
+ sawIncompleteObject = true
379
+ }
380
+ continue
381
+ }
382
+
383
+ if (inString) {
384
+ if (escaped) escaped = false
385
+ else if (char === '\\') escaped = true
386
+ else if (char === '"') inString = false
387
+ continue
388
+ }
389
+
390
+ if (char === '"') inString = true
391
+ else if (char === '{') depth += 1
392
+ else if (char === '}') {
393
+ depth -= 1
394
+ if (depth !== 0) continue
395
+
396
+ const candidate = stdout.slice(start, index + 1)
397
+ start = -1
398
+ sawIncompleteObject = false
399
+ try {
400
+ const parsed = JSON.parse(candidate) as { data?: { itemList?: unknown } }
401
+ if (Array.isArray(parsed.data?.itemList)) return parsed.data.itemList
402
+ } catch {
403
+ // 前缀日志里的成对花括号;继续找下一完整对象。
404
+ }
405
+ }
406
+ }
407
+
408
+ throw new Error(sawIncompleteObject
409
+ ? 'incomplete FlyAI JSON object'
410
+ : 'no complete FlyAI itemList JSON object')
411
+ }
412
+
413
+ /** 解析 stdout 中首个完整 JSON 对象(不限 itemList;ai-search 用) */
414
+ export function parseFlyaiJsonEnvelope(stdout: string): Record<string, unknown> {
415
+ let start = -1
416
+ let depth = 0
417
+ let inString = false
418
+ let escaped = false
419
+
420
+ for (let index = 0; index < stdout.length; index += 1) {
421
+ const char = stdout[index]!
422
+ if (start < 0) {
423
+ if (char === '{') { start = index; depth = 1; inString = false; escaped = false }
424
+ continue
425
+ }
426
+ if (inString) {
427
+ if (escaped) escaped = false
428
+ else if (char === '\\') escaped = true
429
+ else if (char === '"') inString = false
430
+ continue
431
+ }
432
+ if (char === '"') inString = true
433
+ else if (char === '{') depth += 1
434
+ else if (char === '}') {
435
+ depth -= 1
436
+ if (depth === 0) {
437
+ const candidate = stdout.slice(start, index + 1)
438
+ try { return JSON.parse(candidate) as Record<string, unknown> } catch { start = -1; depth = 0 }
439
+ }
440
+ }
441
+ }
442
+ throw new Error('no complete FlyAI JSON envelope')
443
+ }
444
+
445
+ interface ShResult {
446
+ code: number
447
+ stdout: string
448
+ stderr: string
449
+ error?: string
450
+ timedOut?: boolean
451
+ drainTimedOut?: boolean
452
+ /** 调用被 AbortSignal 取消(预先 abort=未 spawn;在途 abort=进程组被杀) */
453
+ cancelled?: boolean
454
+ signal?: NodeJS.Signals | null
455
+ localTermination?: FlyaiLocalTermination
456
+ }
457
+
458
+ interface FlyaiSensitiveContext {
459
+ key?: string
460
+ debugEndpoint?: string
461
+ }
462
+
463
+ interface FlyaiEnvContext {
464
+ note: { keySource: FlyaiKeySource; endpointDebug?: boolean }
465
+ sensitive: FlyaiSensitiveContext
466
+ }
467
+
468
+ const REDACTED_FLYAI_KEY = '[已隐藏 FlyAI key]'
469
+ const REDACTED_FLYAI_ENDPOINT = '?(已隐藏 endpoint 敏感参数)'
470
+
471
+ function shellUrlToken(value: string): { token: string; suffix: string } {
472
+ const match = /[),.;!?,。!?]+$/.exec(value)
473
+ const suffix = match?.[0] ?? ''
474
+ return { token: suffix ? value.slice(0, -suffix.length) : value, suffix }
475
+ }
476
+
477
+ /** 只针对实际 DEBUG endpoint 的 host 脱去 userinfo/query/fragment。 */
478
+ function redactFlyaiText(value: string, sensitive: FlyaiSensitiveContext): string {
479
+ let output = value
480
+ if (sensitive.key) output = output.split(sensitive.key).join(REDACTED_FLYAI_KEY)
481
+ if (!sensitive.debugEndpoint) return output
482
+
483
+ let debugHost: string
484
+ try {
485
+ debugHost = new URL(sensitive.debugEndpoint).host.toLowerCase()
486
+ } catch {
487
+ return output
488
+ }
489
+
490
+ return output.replace(/\bhttps?:\/\/[^\s"'<>]+/gi, (raw) => {
491
+ const { token, suffix } = shellUrlToken(raw)
492
+ try {
493
+ const url = new URL(token)
494
+ if (url.host.toLowerCase() !== debugHost) return raw
495
+ const path = url.pathname || '/'
496
+ const hasSensitivePart = Boolean(url.username || url.password || url.search || url.hash)
497
+ return `${url.protocol}//${url.host}${path}${hasSensitivePart ? REDACTED_FLYAI_ENDPOINT : ''}${suffix}`
498
+ } catch {
499
+ return raw
500
+ }
501
+ })
502
+ }
503
+
504
+ function redactFlyaiValue(value: unknown, sensitive: FlyaiSensitiveContext): unknown {
505
+ if (typeof value === 'string') return redactFlyaiText(value, sensitive)
506
+ if (Array.isArray(value)) return value.map(item => redactFlyaiValue(item, sensitive))
507
+ if (value && typeof value === 'object') {
508
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, redactFlyaiValue(item, sensitive)]))
509
+ }
510
+ return value
511
+ }
512
+
513
+ function safeFlyaiResult<T extends FlyaiResult>(result: T, sensitive: FlyaiSensitiveContext): T {
514
+ return redactFlyaiValue(result, sensitive) as T
515
+ }
516
+
517
+ /**
518
+ * 子进程 stdout 走临时文件而非管道(issue #84):管道在 CLI 异步写后
519
+ * 立即 exit 时丢尾部(实测截断 ~7.6KB,exit=0 静默);文件无此问题。
520
+ *
521
+ * 资源边界(#516):进程组/清理策略统一复用 capabilities/spawn-bounded.ts
522
+ * (与 hbcli/anything 同一进程树边界),本函数只叠加 flyai 特有的
523
+ * 文件 stdout 收集与本地终止分类——主进程 exit 后 stderr drain 最多等
524
+ * drainMs(默认 3s),超时杀组、drainTimedOut 结算,不写成成功事实。
525
+ */
526
+ function sh(
527
+ cmd: string,
528
+ args: string[],
529
+ opts: { timeoutMs: number; env?: NodeJS.ProcessEnv; drainMs?: number; signal?: AbortSignal },
530
+ ): Promise<ShResult> {
531
+ // 预先取消:不 spawn 任何进程,不产生 stdout 临时文件。
532
+ if (opts.signal?.aborted) {
533
+ return Promise.resolve({ code: -1, stdout: '', stderr: '', cancelled: true, localTermination: 'signal' })
534
+ }
535
+
536
+ const outFile = join(tmpdir(), `gotry-flyai-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.out`)
537
+ const outFd = openSync(outFile, 'w')
538
+
539
+ const readOut = (): string => {
540
+ try { return readFileSync(outFile, 'utf8') } catch { return '' }
541
+ }
542
+ const cleanupFile = (): void => {
543
+ try { closeSync(outFd) } catch { /* ignore */ }
544
+ try { unlinkSync(outFile) } catch { /* ignore */ }
545
+ }
546
+
547
+ return spawnBounded(cmd, args, {
548
+ env: opts.env ?? process.env,
549
+ cwd: process.cwd(),
550
+ timeoutMs: opts.timeoutMs,
551
+ signal: opts.signal,
552
+ stdoutFd: outFd,
553
+ drainMs: opts.drainMs ?? 3_000,
554
+ }).then((r): ShResult => {
555
+ // 必须先读出 stdout 再清理:closeSync 之后文件即删除,
556
+ // 先 cleanup 会把成功响应(含空 itemList miss)读成空串。
557
+ const stdout = readOut()
558
+ cleanupFile()
559
+ const localTermination: FlyaiLocalTermination | undefined = r.aborted
560
+ ? 'signal'
561
+ : r.timedOut
562
+ ? 'deadline'
563
+ : r.drainTimedOut
564
+ ? 'drain-timeout'
565
+ : r.error
566
+ ? 'spawn'
567
+ : r.signal
568
+ ? 'empty-exit'
569
+ : undefined
570
+ return {
571
+ code: r.code ?? -1,
572
+ stdout,
573
+ stderr: r.stderr,
574
+ ...(r.signal !== null && r.signal !== undefined ? { signal: r.signal } : {}),
575
+ ...(r.error ? { error: r.error } : {}),
576
+ ...(r.timedOut ? { timedOut: true } : {}),
577
+ ...(r.drainTimedOut ? { drainTimedOut: true } : {}),
578
+ ...(r.aborted ? { cancelled: true } : {}),
579
+ ...(localTermination ? { localTermination } : {}),
580
+ }
581
+ })
582
+ }
583
+
584
+ function localTerminationResult(
585
+ kind: FlyaiKind,
586
+ termination: FlyaiLocalTermination,
587
+ r: ShResult,
588
+ latencyMs: number,
589
+ ts: string,
590
+ envNote: { keySource: FlyaiKeySource; endpointDebug?: boolean },
591
+ ): FlyaiResult {
592
+ const detail = termination === 'spawn'
593
+ ? `启动失败${r.error ? `:${r.error}` : ''}`
594
+ : termination === 'signal'
595
+ ? `进程收到本机取消信号${r.signal ? `(${r.signal})` : ''}`
596
+ : termination === 'deadline'
597
+ ? '达到本地 deadline'
598
+ : termination === 'drain-timeout'
599
+ ? `主进程已退出(code ${r.code})但持有 stderr 的后代在 drain 时限内未释放,整组已终止——结果收集不完整`
600
+ : '进程退出但未返回可判定结果'
601
+ return {
602
+ kind, ...envNote, latencyMs, ok: false, via: 'flyai-error', verdict: 'error',
603
+ retryable: false, localTermination: termination,
604
+ evidence: `[实时API:flyai@error@${ts}] local termination: ${termination}`,
605
+ error: `FlyAI 本地终止(${detail})`,
606
+ }
607
+ }
608
+
609
+ const YMD = /^\d{4}-\d{2}-\d{2}$/
610
+
611
+ /** search-poi 闭集类别(官方 references,2026-09 核实) */
612
+ export const FLYAI_POI_CATEGORIES = [
613
+ '自然风光', '山湖田园', '森林丛林', '峡谷瀑布', '沙滩海岛', '沙漠草原',
614
+ '人文古迹', '古镇古村', '历史古迹', '园林花园', '宗教场所', '公园乐园',
615
+ '主题乐园', '水上乐园', '影视基地', '动物园', '植物园', '海洋馆', '体育场馆',
616
+ '演出赛事', '剧院剧场', '博物馆', '纪念馆', '展览馆', '地标建筑', '市集',
617
+ '文创街区', '城市观光', '户外活动', '滑雪', '漂流', '冲浪', '潜水', '露营', '温泉',
618
+ ] as const
619
+
620
+ const HOTEL_SORTS = ['distance_asc', 'rate_desc', 'price_asc', 'price_desc', 'no_rank']
621
+ const MARRIOTT_PACKAGE_SORTS = ['price_asc', 'price_desc']
622
+
623
+ function badArg(kind: FlyaiKind, error: string, ts: string): FlyaiResult {
624
+ return {
625
+ kind, ok: false, via: 'flyai-error', verdict: 'error', latencyMs: 0,
626
+ evidence: `[实时API:flyai@error@${ts}] bad args`, error,
627
+ }
628
+ }
629
+
630
+ function hourOk(value: number): boolean {
631
+ return Number.isInteger(value) && value >= 0 && value <= 23
632
+ }
633
+
634
+ /** 各 kind 参数校验 → CLI args;不合法返回 error 串 */
635
+ function buildCliArgs(q: FlyaiQuery): { args?: string[]; error?: string } {
636
+ const csv = (value?: string): string[] => value ? [value] : []
637
+ switch (q.kind) {
638
+ case 'flight':
639
+ case 'train': {
640
+ if (!(q.origin ?? '').trim()) return { error: 'origin(出发城市/机场)required' }
641
+ for (const d of [q.depDate, q.depDateStart, q.depDateEnd, q.backDate]) {
642
+ if (d && !YMD.test(d)) return { error: `日期需 YYYY-MM-DD,实际 ${d}` }
643
+ }
644
+ if (q.journeyType !== undefined && q.journeyType !== 1 && q.journeyType !== 2) {
645
+ return { error: 'journeyType 只接受 1(直达)/2(中转)' }
646
+ }
647
+ for (const h of [q.depHourStart, q.depHourEnd, q.arrHourStart, q.arrHourEnd]) {
648
+ if (h !== undefined && !hourOk(h)) return { error: '小时范围需 0–23 整数' }
649
+ }
650
+ if (q.maxPrice !== undefined && !(q.maxPrice > 0)) return { error: 'maxPrice 需为正数' }
651
+ if (q.totalDurationHour !== undefined && !(q.totalDurationHour > 0)) return { error: 'totalDurationHour 需为正数' }
652
+ const sub = q.kind === 'flight' ? 'search-flight' : 'search-train'
653
+ return {
654
+ args: [
655
+ sub, '--origin', q.origin!.trim(),
656
+ ...(q.destination?.trim() ? ['--destination', q.destination.trim()] : []),
657
+ ...(q.depDate ? ['--dep-date', q.depDate] : []),
658
+ ...(q.depDateStart ? ['--dep-date-start', q.depDateStart] : []),
659
+ ...(q.depDateEnd ? ['--dep-date-end', q.depDateEnd] : []),
660
+ ...(q.backDate ? ['--back-date', q.backDate] : []),
661
+ ...(q.journeyType !== undefined ? ['--journey-type', String(q.journeyType)] : []),
662
+ ...csv(q.seatClassName?.trim()).flatMap(v => ['--seat-class-name', v]),
663
+ ...csv(q.transportNo?.trim()) .flatMap(v => ['--transport-no', v]),
664
+ ...csv(q.transferCity?.trim()) .flatMap(v => ['--transfer-city', v]),
665
+ ...(q.depHourStart !== undefined ? ['--dep-hour-start', String(q.depHourStart)] : []),
666
+ ...(q.depHourEnd !== undefined ? ['--dep-hour-end', String(q.depHourEnd)] : []),
667
+ ...(q.arrHourStart !== undefined ? ['--arr-hour-start', String(q.arrHourStart)] : []),
668
+ ...(q.arrHourEnd !== undefined ? ['--arr-hour-end', String(q.arrHourEnd)] : []),
669
+ ...(q.totalDurationHour !== undefined ? ['--total-duration-hour', String(q.totalDurationHour)] : []),
670
+ ...(q.maxPrice !== undefined ? ['--max-price', String(q.maxPrice)] : []),
671
+ ...(q.sortType?.trim() ? ['--sort-type', q.sortType.trim()] : []),
672
+ ],
673
+ }
674
+ }
675
+ case 'hotel':
676
+ case 'marriott-hotel': {
677
+ const dest = (q.destName ?? '').trim()
678
+ if (!dest) return { error: 'destName(目的地 国家/省/市/区)required' }
679
+ const badPair = (q.checkInDate ? 1 : 0) !== (q.checkOutDate ? 1 : 0)
680
+ || (q.checkInDate && (!YMD.test(q.checkInDate) || !YMD.test(q.checkOutDate ?? '')))
681
+ if (badPair) return { error: 'checkInDate/checkOutDate 须成对且为 YYYY-MM-DD' }
682
+ if (q.sort && !HOTEL_SORTS.includes(q.sort)) return { error: `sort 取值:${HOTEL_SORTS.join('/')}` }
683
+ if (q.maxPrice !== undefined && !(q.maxPrice > 0)) return { error: 'maxPrice 需为正数' }
684
+ if (q.hotelStars && !/^[1-5](,[1-5])*$/.test(q.hotelStars)) return { error: 'hotelStars 为 1–5 逗号分隔' }
685
+ const sub = q.kind === 'hotel' ? 'search-hotel' : 'search-marriott-hotel'
686
+ return {
687
+ args: [
688
+ sub, '--dest-name', dest,
689
+ ...(q.keyWords?.trim() ? ['--key-words', q.keyWords.trim()] : []),
690
+ ...(q.poiName?.trim() ? ['--poi-name', q.poiName.trim()] : []),
691
+ ...(q.hotelTypes?.trim() ? ['--hotel-types', q.hotelTypes.trim()] : []),
692
+ ...(q.sort ? ['--sort', q.sort] : []),
693
+ ...(q.checkInDate && q.checkOutDate ? ['--check-in-date', q.checkInDate, '--check-out-date', q.checkOutDate] : []),
694
+ ...(q.hotelStars ? ['--hotel-stars', q.hotelStars] : []),
695
+ ...(q.hotelBedTypes?.trim() ? ['--hotel-bed-types', q.hotelBedTypes.trim()] : []),
696
+ ...(q.maxPrice !== undefined ? ['--max-price', String(q.maxPrice)] : []),
697
+ ...(q.kind === 'marriott-hotel'
698
+ ? [
699
+ ...(q.hotelBrands?.trim() ? ['--hotel-brands', q.hotelBrands.trim()] : []),
700
+ ...(q.hotelName?.trim() ? ['--hotel-name', q.hotelName.trim()] : []),
701
+ ]
702
+ : []),
703
+ ],
704
+ }
705
+ }
706
+ case 'poi': {
707
+ if (!(q.cityName ?? '').trim()) return { error: 'cityName(景点所在城市)required' }
708
+ if (q.poiLevel !== undefined && !(Number.isInteger(q.poiLevel) && q.poiLevel >= 1 && q.poiLevel <= 5)) {
709
+ return { error: 'poiLevel 为 1–5 整数' }
710
+ }
711
+ if (q.category && !FLYAI_POI_CATEGORIES.includes(q.category as typeof FLYAI_POI_CATEGORIES[number])) {
712
+ return { error: `category 不在官方闭集(共 ${FLYAI_POI_CATEGORIES.length} 类)` }
713
+ }
714
+ return {
715
+ args: [
716
+ 'search-poi', '--city-name', q.cityName!.trim(),
717
+ ...(q.poiLevel !== undefined ? ['--poi-level', String(q.poiLevel)] : []),
718
+ ...(q.keyword?.trim() ? ['--keyword', q.keyword.trim()] : []),
719
+ ...(q.category ? ['--category', q.category] : []),
720
+ ],
721
+ }
722
+ }
723
+ case 'keyword':
724
+ if (!(q.query ?? '').trim()) return { error: 'query(关键词)required' }
725
+ return { args: ['keyword-search', '--query', q.query!.trim()] }
726
+ case 'ai':
727
+ if (!(q.query ?? '').trim()) return { error: 'query(完整自然语言)required' }
728
+ return { args: ['ai-search', '--query', q.query!.trim()] }
729
+ case 'marriott-package': {
730
+ if (![q.keyword, q.hotelName, q.provinceOrCity].some(value => value?.trim())) return { error: 'keyword/hotelName/provinceOrCity 至少填写一项' }
731
+ if (q.sortType && !MARRIOTT_PACKAGE_SORTS.includes(q.sortType)) {
732
+ return { error: `sortType 仅接受 ${MARRIOTT_PACKAGE_SORTS.join('/')}` }
733
+ }
734
+ return {
735
+ args: [
736
+ 'search-marriott-package',
737
+ ...(q.keyword?.trim() ? ['--keyword', q.keyword.trim()] : []),
738
+ ...(q.hotelName?.trim() ? ['--hotel-name', q.hotelName.trim()] : []),
739
+ ...(q.provinceOrCity?.trim() ? ['--province-or-city', q.provinceOrCity.trim()] : []),
740
+ ...(q.sortType ? ['--sort-type', q.sortType] : []),
741
+ ],
742
+ }
743
+ }
744
+ }
745
+ }
746
+
747
+ const KIND_LABEL: Record<FlyaiKind, string> = {
748
+ flight: 'flight', train: 'train', hotel: 'hotel', poi: 'poi',
749
+ keyword: 'keyword', ai: 'ai', 'marriott-hotel': 'marriott hotel', 'marriott-package': 'marriott package',
750
+ }
751
+
752
+ /**
753
+ * FlyAI 官方只读检索(8 命令)— 任何失败走降级;不抛错。
754
+ */
755
+ export async function flyaiSearch(q: FlyaiQuery): Promise<FlyaiResult> {
756
+ const started = Date.now()
757
+ const ts = new Date().toISOString()
758
+ const envContext = await runtimeEnvNote()
759
+ const envNote = envContext.note
760
+ const sensitive = envContext.sensitive
761
+ const safe = <T extends FlyaiResult>(result: T): T => safeFlyaiResult(result, sensitive)
762
+ const built = buildCliArgs(q)
763
+ if (built.error) return { ...badArg(q.kind, built.error, ts), ...envNote }
764
+
765
+ const prefix = q.cliPrefixArgs ?? ['-y', '@fly-ai/flyai-cli@1.0.16']
766
+ const r = await sh(q.cliBin ?? 'npx', [...prefix, ...built.args!], {
767
+ timeoutMs: q.timeoutMs ?? 30_000,
768
+ drainMs: q.drainMs,
769
+ signal: q.signal,
770
+ })
771
+ const latencyMs = Date.now() - started
772
+ const combined = `${r.stderr}\n${r.stdout}`
773
+
774
+ if (r.cancelled) {
775
+ return safe({
776
+ kind: q.kind, ...envNote, latencyMs, ok: false, via: 'flyai-error', verdict: 'cancelled',
777
+ retryable: false, localTermination: 'signal',
778
+ evidence: `[实时API:flyai@error@${ts}] cancelled by caller`,
779
+ error: q.signal?.aborted ? '调用前已取消(未发起检索)' : '检索在途被取消(进程组已终止)',
780
+ })
781
+ }
782
+
783
+ if (r.timedOut) {
784
+ return safe({
785
+ kind: q.kind, ...envNote, latencyMs, ok: false, via: 'flyai-error', verdict: 'timeout',
786
+ retryable: false, localTermination: 'deadline',
787
+ evidence: `[实时API:flyai@error@${ts}] local deadline after ${q.timeoutMs ?? 30_000}ms`,
788
+ error: `FlyAI 本地终止(达到 deadline ${q.timeoutMs ?? 30_000}ms${r.drainTimedOut ? ';stderr 后代 drain 超时' : ''})`,
789
+ setup: '本机 deadline 已终止本次检索;请调整 timeoutMs 或稍后由上层重新发起。',
790
+ })
791
+ }
792
+
793
+ // A signal/empty exit/spawn failure carries no trustworthy upstream
794
+ // response. Handle it before HTTP text classification so stale partial
795
+ // stdout (for example a 429 printed before SIGTERM) cannot trigger retry.
796
+ if (r.localTermination) {
797
+ return safe(localTerminationResult(q.kind, r.localTermination, r, latencyMs, ts, envNote))
798
+ }
799
+
800
+ if (r.error || r.code !== 0 || /HTTP\s*(4\d\d|5\d\d)/.test(combined)) {
801
+ return safe(classifyUpstreamFailure(q.kind, r, latencyMs, ts, envNote))
802
+ }
803
+
804
+ // ai-search:data 是自由形状,不走 itemList。
805
+ if (q.kind === 'ai') {
806
+ let envelope: Record<string, unknown>
807
+ try {
808
+ envelope = parseFlyaiJsonEnvelope(r.stdout)
809
+ } catch (e) {
810
+ const raw = r.stdout.replace(/\s+/g, ' ').slice(0, 160)
811
+ return safe({
812
+ kind: q.kind, ...envNote, latencyMs, ok: false, via: 'flyai-error', verdict: 'error',
813
+ evidence: `[实时API:flyai@error@${ts}] parse failed: ${raw}`,
814
+ error: `failed to parse flyai ai output (${(e as Error).message}): ${raw}`,
815
+ })
816
+ }
817
+ const systemMessage = nonEmptyText(envelope.systemMessage)
818
+ if (envelope.status !== undefined && envelope.status !== 0) {
819
+ return safe({
820
+ kind: q.kind, ...envNote, latencyMs, ok: false, via: 'flyai-error', verdict: 'error',
821
+ evidence: `[实时API:flyai@error@${ts}] ai status=${String(envelope.status)}`,
822
+ error: nonEmptyText(envelope.message) ?? 'ai-search upstream failure',
823
+ ...(systemMessage ? { systemMessage } : {}),
824
+ })
825
+ }
826
+ const aiData = envelope.data
827
+ const empty = aiData === undefined || aiData === null || aiData === ''
828
+ || (Array.isArray(aiData) && aiData.length === 0)
829
+ return safe(JSON.parse(JSON.stringify({
830
+ kind: q.kind, ...envNote, latencyMs, ok: true, via: 'flyai',
831
+ verdict: empty ? 'miss' : 'hit',
832
+ ...(empty ? {} : { aiData }),
833
+ ...(systemMessage ? { systemMessage } : {}),
834
+ evidence: `[实时API:flyai@${ts}] ai-search ${empty ? 'empty' : 'data returned'}`,
835
+ })) as FlyaiResult)
836
+ }
837
+
838
+ let items: unknown[]
839
+ try {
840
+ items = parseFlyaiItemList(r.stdout) as RawItem[]
841
+ } catch (e) {
842
+ const raw = r.stdout.replace(/\s+/g, ' ').slice(0, 160)
843
+ // Sentinel 限流:exit 0,stdout 为 {"message":"SentinelBlockException…"}
844
+ if (/Sentinel/i.test(raw)) {
845
+ return safe({
846
+ kind: q.kind, ...envNote, latencyMs, ok: false, via: 'flyai-error', verdict: 'error',
847
+ retryable: false,
848
+ evidence: `[实时API:flyai@error@${ts}] sentinel: ${raw}`,
849
+ error: `FlyAI Sentinel 限流:${raw}`,
850
+ setup: '平台 Sentinel 限流(恢复窗口未公开)——稍后重试,或改走 gotry_session_search(账号会话)。',
851
+ })
852
+ }
853
+ const reason = (e as Error).message
854
+ return safe({
855
+ kind: q.kind, ...envNote, latencyMs, ok: false, via: 'flyai-error', verdict: 'error',
856
+ retryable: false,
857
+ evidence: `[实时API:flyai@error@${ts}] parse failed(${reason}): ${raw}`,
858
+ error: `failed to parse flyai output (${reason}): ${raw}`,
859
+ })
860
+ }
861
+
862
+ return safe(parseItemListResult(q.kind, items, latencyMs, ts, envNote, r.stdout))
863
+ }
864
+
865
+ /** 当前进程 key 来源/endpoint 覆盖(仅展示,与 spawn 实际生效同源) */
866
+ async function runtimeEnvNote(): Promise<FlyaiEnvContext> {
867
+ try {
868
+ const key = resolveFlyaiKey()
869
+ const endpoint = resolveFlyaiEndpoint()
870
+ return {
871
+ note: { keySource: key.source, ...(endpoint.debug ? { endpointDebug: true } : {}) },
872
+ sensitive: { ...(key.key ? { key: key.key } : {}), ...(endpoint.debug ? { debugEndpoint: endpoint.url } : {}) },
873
+ }
874
+ } catch {
875
+ return { note: { keySource: 'none' }, sensitive: {} }
876
+ }
877
+ }
878
+
879
+ /** 非 0 退出 / HTTP 4xx-5xx 文本 → 故障闭集分类 */
880
+ function classifyUpstreamFailure(
881
+ kind: FlyaiKind,
882
+ r: ShResult,
883
+ latencyMs: number,
884
+ ts: string,
885
+ envNote: { keySource: FlyaiKeySource; endpointDebug?: boolean },
886
+ ): FlyaiResult {
887
+ const upstream = `${r.stderr}\n${r.stdout}`.replace(/\s+/g, ' ').trim()
888
+ const tag = (verdict: FlyaiVerdict, evidence: string, extra: Partial<FlyaiResult>): FlyaiResult =>
889
+ ({ kind, ...envNote, latencyMs, ok: false, via: 'flyai-error', verdict, evidence, ...extra })
890
+
891
+ if (/Invalid API key|HTTP\s*401|\b401\b/.test(upstream)) {
892
+ return tag('auth-error', `[实时API:flyai@error@${ts}] 401 invalid api key`, {
893
+ retryable: false,
894
+ error: `FlyAI 鉴权失败(401):${upstream.slice(0, 160)}`,
895
+ setup: '当前 key 无效或已吊销——在本机终端运行 `gotry setup flyai` 重新设置并验证(模型无法替你输入 key);`gotry setup flyai --clear` 可清除错误 key,清除后仍可匿名试用。',
896
+ })
897
+ }
898
+ if (/HTTP\s*403|\b403\b/.test(upstream)) {
899
+ return tag('forbidden', `[实时API:flyai@error@${ts}] 403 forbidden`, {
900
+ retryable: false,
901
+ error: `FlyAI 拒绝访问(403):${upstream.slice(0, 160)}`,
902
+ setup: '上游 403:核对该 key 的权限范围与控制台状态;如 endpoint 被 DEBUG_FLYAI_MCP_URL 覆盖,请确认指向。',
903
+ })
904
+ }
905
+ if (/Trial limit reached/.test(upstream)) {
906
+ return tag('needs-setup', `[实时API:flyai@error@${ts}] trial quota exhausted`, {
907
+ retryable: false,
908
+ error: `FlyAI 匿名试用额度已用尽:${upstream.slice(0, 160)}`,
909
+ setup: '打开 https://flyai.open.fliggy.com/console,登录后复制 API Key,本机运行 `gotry setup flyai` 配置并验证;本会话请勿盲重试 gotry_flyai_search,机/火/酒改走 gotry_session_search。',
910
+ })
911
+ }
912
+ if (/HTTP\s*429|\b429\b/.test(upstream)) {
913
+ return tag('rate-limited', `[实时API:flyai@error@${ts}] 429 rate limited`, {
914
+ retryable: true,
915
+ error: `FlyAI 普通限流(非试用达限):${upstream.slice(0, 160)}`,
916
+ setup: '上游普通限流:稍候再查,勿高频连发;仍 429 请稍后或换 gotry_session_search。',
917
+ })
918
+ }
919
+ if (/timeout|timed out|ETIMEDOUT|ECONNRESET|ECONNREFUSED|ECONNABORTED|EHOSTUNREACH|ENETUNREACH|EAI_AGAIN|socket|network|fetch failed|HTTP\s*5\d\d|\b5\d\d\b/i.test(upstream)) {
920
+ return tag('error', `[实时API:flyai@error@${ts}] transient upstream`, {
921
+ retryable: true,
922
+ error: `FlyAI 瞬时网络/上游错误:${upstream.slice(0, 160)}`,
923
+ setup: '网络抖动或上游 5xx:稍后重试;持续失败请检查本机网络或改走 gotry_session_search。',
924
+ })
925
+ }
926
+ return tag('error', `[实时API:flyai@error@${ts}] ${r.error ?? `exit ${r.code}`}`, {
927
+ retryable: false,
928
+ error: r.error ?? upstream.slice(0, 200),
929
+ })
930
+ }
931
+
932
+ /** itemList(非 ai 类)→ 对应 typed 结果;空=miss;含 malformed=整体 error */
933
+ function parseItemListResult(
934
+ kind: FlyaiKind,
935
+ items: unknown[],
936
+ latencyMs: number,
937
+ ts: string,
938
+ envNote: { keySource: FlyaiKeySource; endpointDebug?: boolean },
939
+ rawStdout: string,
940
+ ): FlyaiResult {
941
+ let parsed: ParsedItems<unknown>
942
+ switch (kind) {
943
+ case 'flight':
944
+ case 'train':
945
+ parsed = parseTransportItems(items)
946
+ break
947
+ case 'hotel':
948
+ parsed = parseHotelItems(items)
949
+ break
950
+ case 'marriott-hotel':
951
+ parsed = parseHotelItems(items)
952
+ break
953
+ case 'poi':
954
+ parsed = parsePoiItems(items)
955
+ break
956
+ case 'keyword':
957
+ parsed = parseKeywordItems(items)
958
+ break
959
+ case 'marriott-package':
960
+ parsed = parsePackageItems(items)
961
+ break
962
+ case 'ai':
963
+ parsed = { options: [], malformedCount: 0 }
964
+ break
965
+ }
966
+
967
+ const systemMessage = (() => {
968
+ try { return nonEmptyText((JSON.parse(rawStdout) as { systemMessage?: unknown }).systemMessage) } catch { return undefined }
969
+ })()
970
+
971
+ if (parsed.malformedCount > 0) {
972
+ const label = KIND_LABEL[kind]
973
+ return {
974
+ kind, ...envNote, latencyMs, ok: false, via: 'flyai-error', verdict: 'error',
975
+ retryable: false,
976
+ evidence: `[实时API:flyai@error@${ts}] ${label} itemList malformed (${parsed.malformedCount}/${items.length} items failed typed validation)`,
977
+ error: `FlyAI ${label} itemList malformed: ${parsed.malformedCount}/${items.length} item(s) failed typed validation`,
978
+ ...(systemMessage ? { systemMessage } : {}),
979
+ }
980
+ }
981
+
982
+ const count = parsed.options.length
983
+ const verdict: FlyaiVerdict = count > 0 ? 'hit' : 'miss'
984
+ const evidence = `[实时API:flyai@${ts}] ${count}/${items.length} ${KIND_LABEL[kind]} options`
985
+ const common = { kind, ...envNote, latencyMs, ok: true, via: 'flyai' as const, verdict, evidence, ...(systemMessage ? { systemMessage } : {}) }
986
+
987
+ switch (kind) {
988
+ case 'flight':
989
+ case 'train':
990
+ return { ...common, options: parsed.options as FlyaiOption[] }
991
+ case 'hotel':
992
+ case 'marriott-hotel':
993
+ return { ...common, hotels: parsed.options as FlyaiHotelOption[] }
994
+ case 'poi':
995
+ return { ...common, pois: parsed.options as FlyaiPoiOption[] }
996
+ case 'keyword':
997
+ return { ...common, keywords: parsed.options as FlyaiKeywordOption[] }
998
+ case 'marriott-package':
999
+ return { ...common, packages: parsed.options as FlyaiMarriottPackageOption[] }
1000
+ case 'ai':
1001
+ return { ...common }
1002
+ }
1003
+ }
1004
+
1005
+ // ── typed parsers ───────────────────────────────────────────────────────────────
1006
+
1007
+ /** 机/火条目 → FlyaiOption;非空列表含任一不完整条目即整体 malformed */
1008
+ function parseTransportItems(items: unknown[]): ParsedItems<FlyaiOption> {
1009
+ const options: FlyaiOption[] = []
1010
+ let malformedCount = 0
1011
+ for (const raw of items) {
1012
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
1013
+ malformedCount += 1
1014
+ continue
1015
+ }
1016
+ const it = raw as RawItem
1017
+ const journey = Array.isArray(it.journeys) ? it.journeys[0] : undefined
1018
+ const segments = journey && typeof journey === 'object' && !Array.isArray(journey) && Array.isArray(journey.segments)
1019
+ ? journey.segments
1020
+ : undefined
1021
+ if (!segments || segments.length === 0) {
1022
+ malformedCount += 1
1023
+ continue
1024
+ }
1025
+ const parsedSegments: FlyaiTransportSegment[] = []
1026
+ for (const rawSegment of segments) {
1027
+ if (!rawSegment || typeof rawSegment !== 'object' || Array.isArray(rawSegment)) continue
1028
+ const segment = rawSegment as NonNullable<NonNullable<RawItem['journeys']>[number]['segments']>[number]
1029
+ const no = nonEmptyText(segment.marketingTransportNo)
1030
+ const name = optionalText(segment.marketingTransportName)
1031
+ const depDateTime = nonEmptyText(segment.depDateTime)
1032
+ const arrDateTime = nonEmptyText(segment.arrDateTime)
1033
+ const depStation = nonEmptyText(segment.depStationName)
1034
+ const arrStation = nonEmptyText(segment.arrStationName)
1035
+ const durationMin = positiveFiniteDuration(segment.duration)
1036
+ const seatClass = optionalText(segment.seatClassName)
1037
+ if (!no || !depDateTime || !arrDateTime || !depStation || !arrStation || durationMin === undefined
1038
+ || (segment.marketingTransportName !== undefined && name === undefined)
1039
+ || (segment.seatClassName !== undefined && seatClass === undefined)) continue
1040
+ parsedSegments.push({ no, name: name ?? '', depDateTime, arrDateTime, depStation, arrStation, durationMin, seatClass: seatClass ?? undefined })
1041
+ }
1042
+ if (parsedSegments.length !== segments.length) {
1043
+ malformedCount += 1
1044
+ continue
1045
+ }
1046
+ const first = parsedSegments[0]!
1047
+ const last = parsedSegments[parsedSegments.length - 1]!
1048
+ const journeyType = optionalText(journey?.journeyType)
1049
+ const nonstop = parsedSegments.length === 1 && !/^(?:2|中转|transfer)/i.test(journeyType ?? '')
1050
+ const totalDuration = positiveFiniteDuration(journey?.totalDuration)
1051
+ ?? parsedSegments.reduce((sum, segment) => sum + segment.durationMin, 0)
1052
+ const rawPriceValue = it.adultPrice !== undefined
1053
+ ? it.adultPrice
1054
+ : it.ticketPrice !== undefined ? it.ticketPrice : it.price
1055
+ const parsedPrice = parseTransportPrice(rawPriceValue)
1056
+ const jumpUrl = optionalText(it.jumpUrl)
1057
+ if (!parsedPrice
1058
+ || (it.jumpUrl !== undefined && jumpUrl === undefined)) {
1059
+ malformedCount += 1
1060
+ continue
1061
+ }
1062
+ options.push({
1063
+ no: first.no, name: first.name, depDateTime: first.depDateTime, arrDateTime: last.arrDateTime,
1064
+ depStation: first.depStation, arrStation: last.arrStation, durationMin: totalDuration,
1065
+ price: parsedPrice.price, priceRaw: parsedPrice.priceRaw,
1066
+ seatClass: first.seatClass, jumpUrl: jumpUrl ?? undefined, nonstop, journeyType: journeyType ?? undefined,
1067
+ segments: parsedSegments,
1068
+ })
1069
+ }
1070
+ return { options, malformedCount }
1071
+ }
1072
+
1073
+ function stringField(parent: RawItem, key: keyof RawItem): string | undefined | null | 'bad' {
1074
+ const value = parent[key]
1075
+ if (value === undefined) return undefined
1076
+ if (value === null) return null
1077
+ return nonEmptyText(value) ?? 'bad'
1078
+ }
1079
+
1080
+ /** 酒店/万豪酒店条目 → FlyaiHotelOption(官方两形态字段集同构) */
1081
+ function parseHotelItems(items: unknown[]): ParsedItems<FlyaiHotelOption> {
1082
+ const options: FlyaiHotelOption[] = []
1083
+ let malformedCount = 0
1084
+ for (const raw of items) {
1085
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
1086
+ malformedCount += 1
1087
+ continue
1088
+ }
1089
+ const it = raw as RawItem
1090
+ const name = nonEmptyText(it.name)
1091
+ const star = stringField(it, 'star')
1092
+ const rawPrice = stringField(it, 'price')
1093
+ const rate = it.rate === undefined ? undefined : it.rate === null ? null : nonEmptyText(it.rate) ?? 'bad'
1094
+ const address = stringField(it, 'address')
1095
+ const poiSource = it.interestsPoi !== undefined ? it.interestsPoi : it.nearbyPoi
1096
+ const poi = poiSource === undefined ? undefined : poiSource === null ? null : nonEmptyText(poiSource) ?? 'bad'
1097
+ const hotelIdSource = it.shId !== undefined ? it.shId : it.shid
1098
+ const hotelId = hotelIdSource === undefined ? undefined : nonEmptyText(hotelIdSource) ?? (typeof hotelIdSource === 'number' ? String(hotelIdSource) : 'bad')
1099
+ const jumpUrl = stringField(it, 'detailUrl')
1100
+ const mainPic = stringField(it, 'mainPic')
1101
+ const score = it.score === undefined ? undefined : it.score === null ? null : nonEmptyText(it.score) ?? 'bad'
1102
+ const scoreDesc = stringField(it, 'scoreDesc')
1103
+ const review = stringField(it, 'review')
1104
+ const brandName = stringField(it, 'brandName')
1105
+ const latitude = stringField(it, 'latitude')
1106
+ const longitude = stringField(it, 'longitude')
1107
+
1108
+ const fields = [star, rawPrice, rate, address, poi, hotelId, jumpUrl, mainPic, score, scoreDesc, review, brandName, latitude, longitude]
1109
+ if (!name || fields.includes('bad')) {
1110
+ malformedCount += 1
1111
+ continue
1112
+ }
1113
+ const priceText = (rawPrice ?? '') as string
1114
+ const bare = priceText.replace(/^¥/, '')
1115
+ const numericPrice = Number(bare)
1116
+ options.push({
1117
+ name,
1118
+ star: star ?? undefined,
1119
+ price: /^\d+(\.\d+)?$/.test(bare) && numericPrice > 0 ? numericPrice : 0,
1120
+ priceRaw: priceText || undefined,
1121
+ rate: rate ?? undefined,
1122
+ address: address ?? undefined,
1123
+ poi: poi ?? undefined,
1124
+ hotelId: hotelId ?? undefined,
1125
+ jumpUrl: jumpUrl ?? undefined,
1126
+ mainPic: mainPic ?? undefined,
1127
+ score: score ?? undefined,
1128
+ scoreDesc: scoreDesc ?? undefined,
1129
+ review: review ?? undefined,
1130
+ brandName: brandName ?? undefined,
1131
+ latitude: latitude ?? undefined,
1132
+ longitude: longitude ?? undefined,
1133
+ })
1134
+ }
1135
+ return { options, malformedCount }
1136
+ }
1137
+
1138
+ /** 景点条目 → FlyaiPoiOption */
1139
+ function parsePoiItems(items: unknown[]): ParsedItems<FlyaiPoiOption> {
1140
+ const options: FlyaiPoiOption[] = []
1141
+ let malformedCount = 0
1142
+ for (const raw of items) {
1143
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
1144
+ malformedCount += 1
1145
+ continue
1146
+ }
1147
+ const it = raw as RawItem
1148
+ const name = nonEmptyText(it.name)
1149
+ const poiId = it.id === undefined ? undefined : nonEmptyText(it.id) ?? (typeof it.id === 'number' ? String(it.id) : 'bad')
1150
+ const mainPic = stringField(it, 'mainPic')
1151
+ const jumpUrl = stringField(it, 'jumpUrl')
1152
+ const address = stringField(it, 'address')
1153
+ const freePoiStatus = stringField(it, 'freePoiStatus')
1154
+ let ticketInfo: FlyaiPoiTicketInfo | undefined | 'bad'
1155
+ if (it.ticketInfo !== undefined && it.ticketInfo !== null) {
1156
+ if (typeof it.ticketInfo !== 'object' || Array.isArray(it.ticketInfo)) {
1157
+ ticketInfo = 'bad'
1158
+ } else {
1159
+ const t = it.ticketInfo as Record<string, unknown>
1160
+ const price = t.price === undefined ? undefined : t.price === null ? null : nonEmptyText(t.price) ?? 'bad'
1161
+ const priceDate = t.priceDate === undefined || t.priceDate === null ? undefined : nonEmptyText(t.priceDate) ?? 'bad'
1162
+ const ticketName = t.ticketName === undefined ? undefined : nonEmptyText(t.ticketName) ?? 'bad'
1163
+ if ([price, priceDate, ticketName].includes('bad')) ticketInfo = 'bad'
1164
+ else ticketInfo = { price: price ?? undefined, priceDate, ticketName }
1165
+ }
1166
+ }
1167
+
1168
+ if (!name || poiId === 'bad' || [mainPic, jumpUrl, address, freePoiStatus].includes('bad') || ticketInfo === 'bad') {
1169
+ malformedCount += 1
1170
+ continue
1171
+ }
1172
+ options.push({
1173
+ name,
1174
+ poiId: poiId ?? undefined,
1175
+ mainPic: mainPic ?? undefined,
1176
+ jumpUrl: jumpUrl ?? undefined,
1177
+ address: address ?? undefined,
1178
+ freePoiStatus: freePoiStatus ?? undefined,
1179
+ ...(ticketInfo ? { ticketInfo } : {}),
1180
+ })
1181
+ }
1182
+ return { options, malformedCount }
1183
+ }
1184
+
1185
+ /** 关键词条目 → FlyaiKeywordOption(官方形状 itemList[].info) */
1186
+ function parseKeywordItems(items: unknown[]): ParsedItems<FlyaiKeywordOption> {
1187
+ const options: FlyaiKeywordOption[] = []
1188
+ let malformedCount = 0
1189
+ for (const raw of items) {
1190
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
1191
+ malformedCount += 1
1192
+ continue
1193
+ }
1194
+ const info = (raw as { info?: unknown }).info
1195
+ if (!info || typeof info !== 'object' || Array.isArray(info)) {
1196
+ malformedCount += 1
1197
+ continue
1198
+ }
1199
+ const it = info as Record<string, unknown>
1200
+ const title = nonEmptyText(it.title)
1201
+ const str = (key: string): string | undefined | 'bad' => {
1202
+ const v = it[key]
1203
+ if (v === undefined || v === null) return undefined
1204
+ return nonEmptyText(v) ?? 'bad'
1205
+ }
1206
+ const jumpUrl = str('jumpUrl')
1207
+ const picUrl = str('picUrl')
1208
+ const price = str('price')
1209
+ const scoreDesc = str('scoreDesc')
1210
+ const star = str('star')
1211
+ let tags: string[] | undefined | 'bad'
1212
+ if (it.tags !== undefined && it.tags !== null) {
1213
+ if (!Array.isArray(it.tags) || it.tags.some(t => nonEmptyText(t) === undefined)) tags = 'bad'
1214
+ else tags = it.tags.map(t => String(t).trim())
1215
+ }
1216
+ if (!title || [jumpUrl, picUrl, price, scoreDesc, star].includes('bad') || tags === 'bad') {
1217
+ malformedCount += 1
1218
+ continue
1219
+ }
1220
+ options.push({
1221
+ title,
1222
+ jumpUrl: jumpUrl ?? undefined,
1223
+ picUrl: picUrl ?? undefined,
1224
+ price: price ?? undefined,
1225
+ scoreDesc: scoreDesc ?? undefined,
1226
+ star: star ?? undefined,
1227
+ ...(tags ? { tags } : {}),
1228
+ })
1229
+ }
1230
+ return { options, malformedCount }
1231
+ }
1232
+
1233
+ /** 万豪套餐条目 → FlyaiMarriottPackageOption */
1234
+ function parsePackageItems(items: unknown[]): ParsedItems<FlyaiMarriottPackageOption> {
1235
+ const options: FlyaiMarriottPackageOption[] = []
1236
+ let malformedCount = 0
1237
+ for (const raw of items) {
1238
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
1239
+ malformedCount += 1
1240
+ continue
1241
+ }
1242
+ const it = raw as RawItem
1243
+ const title = stringField(it, 'title')
1244
+ const name = title !== undefined ? title : stringField(it, 'name')
1245
+ const itemId = stringField(it, 'itemId')
1246
+ const mainPicSource = it.picUrl !== undefined ? 'picUrl' : 'mainPic'
1247
+ const sellingPointSource = it.sellPoint !== undefined ? 'sellPoint' : 'sellingPoint'
1248
+ const fields_ = [
1249
+ stringField(it, 'brandName'), stringField(it, 'hotelName'), stringField(it, 'cityName'),
1250
+ stringField(it, 'price'), stringField(it, 'detailUrl'), stringField(it, mainPicSource),
1251
+ stringField(it, sellingPointSource), itemId,
1252
+ ]
1253
+ if (!name || fields_.includes('bad')) {
1254
+ malformedCount += 1
1255
+ continue
1256
+ }
1257
+ const [brandName, hotelName, cityName, price, detailUrl, mainPic, sellingPoint] = fields_ as Array<string | null | undefined>
1258
+ options.push({
1259
+ name: name as string,
1260
+ ...(title && title !== 'bad' ? { title } : {}),
1261
+ ...(itemId && itemId !== 'bad' ? { itemId } : {}),
1262
+ brandName: brandName ?? undefined,
1263
+ hotelName: hotelName ?? undefined,
1264
+ cityName: cityName ?? undefined,
1265
+ price: price ?? undefined,
1266
+ detailUrl: detailUrl ?? undefined,
1267
+ mainPic: mainPic ?? undefined,
1268
+ picUrl: mainPic ?? undefined,
1269
+ sellingPoint: sellingPoint ?? undefined,
1270
+ sellPoint: sellingPoint ?? undefined,
1271
+ })
1272
+ }
1273
+ return { options, malformedCount }
1274
+ }