@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
@@ -0,0 +1,1788 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * gotry 外部依赖自举(founder 2026-08-29 指令:安装 gotry 时按上游本身的方式装好外部依赖):
4
+ * hbcli(hotelbyte-cli) → npm 优先(staicli@npmjs;Node ≥ 20),原生二进制 install.sh 兜底
5
+ * agent-reach → 官方 pip 安装 git+ upstream(包内 .venv,与 z3-solver 同址原则)
6
+ * flyai → 无需安装(npx 每次自拉 @fly-ai/flyai-cli)
7
+ * dsh-better-sidebar → dsh 宿主层插件市场组件(dshmarket.com #1 UI,18.9 万周装):
8
+ * dsh web 侧栏工作台(文件浏览/Markdown/Mermaid/PDF 预览)——
9
+ * gotry 产物(工单交付 md/行程 md)的成熟查看面(issue #25)。
10
+ * 宿主层安装(dsh plugin → ~/.dsh/profiles/web),不进 gotry 依赖。
11
+ * gotry-session-bridge → 会话检索浏览器扩展(issue #21 传输层方案 C,2026-08-29 定案):
12
+ * 一次性安装替代逐连接 CDP 弹窗;已上架 Chrome Web Store
13
+ * (2026-09-02,一键装+自动更新)。**gory 不介入浏览器**——
14
+ * 商店页安装是浏览器的事;`gotry setup wizard` 仅作离线检测的
15
+ * 进程护栏(打印商店 URL + 健康探活等待),不动剪贴板、
16
+ * 不弹 osascript、不 open 浏览器。
17
+ *
18
+ * 用法:
19
+ * node bin/gotry-bootstrap.js # 显式安装(缺啥装啥;失败 exit 1)
20
+ * node bin/gotry-bootstrap.js --auto # postinstall 模式:CI/跳过开关检测,任何失败不挡安装(exit 0)
21
+ * node bin/gotry-bootstrap.js --check-only # 只探测报告,不安装(测试钩子)
22
+ * node bin/gotry-bootstrap.js --extension-from=github
23
+ * # 扩展改走 GitHub Releases 下载通道(ADR-21):
24
+ * # dist-manifest → tar.gz → SHA256 → key 钉扎 → 原子交换;
25
+ * # 任何失败显式降级回包内副本(离线确定性不变)。
26
+ * node bin/gotry-bootstrap.js calendar # 可选日历挂载开关(D-9,setup 状态面):
27
+ * # 默认写 ~/.gotry/calendar.json {"enabled":true};
28
+ * # --off 删除恢复默认不挂载;--status 只读查看。
29
+ * node bin/gotry-bootstrap.js doctor # 可选依赖体检(2026-09-02 迪拜 session 复盘):
30
+ * # 扩展/agent-reach/.venv/hbcli/flyai key/sidebar/calendar
31
+ * # 逐项只读检查 + 精确补装指引;
32
+ * # 报告落 gotry-state/doctor-report.md(侧栏工作台可预览)。
33
+ * node bin/gotry-bootstrap.js doctor --fix # 体检后按缺失项补装(复用下方三个安装器;LLM key 永不管)。
34
+ * node bin/gotry-bootstrap.js wizard # 会话扩展 onboarding 闭环(issue #21 onboarding UX,§3.3):
35
+ * # 5 步编排 + 后台 health-watch 等扩展心跳,
36
+ * # 扩展一就位 stdout 翻绿并自动重放同 query。
37
+ * # 详见 docs/rfc/user-session-data-rfc.md §3.3 / RFC P3.6。
38
+ *
39
+ * 环境开关:
40
+ * GOTRY_SETUP_SKIP=1 全部跳过
41
+ * GOTRY_SETUP_HBCLI=0 跳过 hbcli
42
+ * GOTRY_SETUP_REACH=0 跳过 agent-reach
43
+ * GOTRY_SETUP_SIDEBAR=0 跳过 dsh-better-sidebar
44
+ * GOTRY_SETUP_EXTENSION=0 跳过会话检索扩展落位
45
+ * GOTRY_EXTENSION_SOURCE=github 等效 --extension-from=github(显式 opt-in,默认 bundled)
46
+ * GOTRY_EXTENSION_RELEASE_BASE=<url> 下载通道基址覆盖(镜像/测试;缺省 GitHub 官方)
47
+ * GOTRY_ONBOARDING_HEADLESS=1 wizard 仅 stdout(SSH/CI 默认探测)
48
+ *
49
+ * 设计纪律(wizard 子命令,2026-09-02 商店上架后):
50
+ * **浏览器自己当安装器**——浏览器商店页直达,点「添加至 Chrome」即完事;gotry
51
+ * 不扮演 GUI/CLI 越界者:不动剪贴板、不动 osascript/zenity/msg、不 open 浏览器。
52
+ * dsh host UI 是 gotry 工具渲染面,needs-extension 时 URL 走 verdict 字段给 UI
53
+ * 自渲。wizard 子命令退化为「离线健康探活等待」,只输出 stdout,不挡用户。
54
+ */
55
+
56
+ import { spawn } from 'node:child_process'
57
+ import { chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs'
58
+ import { homedir } from 'node:os'
59
+ import { join, dirname } from 'node:path'
60
+ import { fileURLToPath } from 'node:url'
61
+ import { createRequire } from 'node:module'
62
+ import { createHash } from 'node:crypto'
63
+
64
+ // ── FlyAI 凭据面(纯 JS 内联;与 ts/capabilities/flyai-config.ts 成对维护,
65
+ // 口径以官方 @fly-ai/flyai-cli 1.0.16 解包事实为准。bootstrap 由 inner 以
66
+ // plain node spawn,不能 import .ts——同 doctorChecks 的既有成对模式)──────
67
+
68
+ function flyaiNonEmpty(value) {
69
+ if (typeof value !== 'string') return undefined
70
+ const text = value.trim()
71
+ return text || undefined
72
+ }
73
+ function flyaiConfigFile() { return join(homedir(), '.flyai', 'config.json') }
74
+ function flyaiVerificationFile() { return join(homedir(), '.gotry', 'flyai-verification.json') }
75
+
76
+ function readFlyaiConfigObject() {
77
+ const path = flyaiConfigFile()
78
+ if (!existsSync(path)) return { ok: true, data: {} }
79
+ let text
80
+ try { text = readFileSync(path, 'utf8') } catch (e) { return { ok: false, error: e.message } }
81
+ if (!text.trim()) return { ok: true, data: {} }
82
+ try {
83
+ const parsed = JSON.parse(text)
84
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
85
+ return { ok: false, error: 'config 顶层不是 JSON 对象' }
86
+ }
87
+ return { ok: true, data: parsed }
88
+ } catch {
89
+ return { ok: false, error: 'config JSON 解析失败(为保护凭据,不显示原文)' }
90
+ }
91
+ }
92
+
93
+ /** 官方 1.0.16 同优先级:FLYAI_API_KEY → DEBUG_FLYAI_API_KEY → config 文件 → none */
94
+ function resolveFlyaiKeyInline() {
95
+ const configPath = flyaiConfigFile()
96
+ const envKey = flyaiNonEmpty(process.env.FLYAI_API_KEY)
97
+ if (envKey) return { key: envKey, source: 'env', configPath }
98
+ const debugKey = flyaiNonEmpty(process.env.DEBUG_FLYAI_API_KEY)
99
+ if (debugKey) return { key: debugKey, source: 'env-debug', configPath }
100
+ const read = readFlyaiConfigObject()
101
+ const fileKey = read.ok ? flyaiNonEmpty(read.data.FLYAI_API_KEY) : undefined
102
+ if (fileKey) return { key: fileKey, source: 'config', configPath }
103
+ return { source: 'none', configPath }
104
+ }
105
+
106
+ function resolveFlyaiEndpointInline() {
107
+ const debugUrl = flyaiNonEmpty(process.env.DEBUG_FLYAI_MCP_URL)
108
+ if (debugUrl) return { url: debugUrl, debug: true }
109
+ return { url: 'https://flyai.open.fliggy.com/mcp', debug: false }
110
+ }
111
+
112
+ function maskFlyaiKeyInline(key) {
113
+ const text = typeof key === 'string' ? key.trim() : ''
114
+ if (!text) return ''
115
+ if (text.length <= 4) return '****'
116
+ return `****${text.slice(-4)}`
117
+ }
118
+
119
+ function isPlaceholderFlyaiKeyInline(key) {
120
+ const text = key.trim().toLowerCase()
121
+ if (!text) return true
122
+ return /^(your?-?(key|api[._-]?key)|xxxx+|<[^>]+>|sk-[a-z]*$|test|placeholder|changeme|foo|bar)$/.test(text)
123
+ }
124
+
125
+ function sha256Inline(value) {
126
+ return createHash('sha256').update(value, 'utf8').digest('hex')
127
+ }
128
+
129
+ function normalizeEndpointInline(url) {
130
+ try {
131
+ return new URL(url).href
132
+ } catch { return undefined }
133
+ }
134
+
135
+ function endpointFingerprintInline(url) {
136
+ const normalized = normalizeEndpointInline(url)
137
+ return normalized ? sha256Inline(normalized) : undefined
138
+ }
139
+
140
+ function displayEndpointInline(url) {
141
+ try {
142
+ const u = new URL(url)
143
+ return `${u.protocol}//${u.host}${u.pathname || '/'}${u.search || u.hash ? '?(已隐藏敏感参数)' : ''}`
144
+ } catch { return '<非法 endpoint>' }
145
+ }
146
+
147
+ /** 权限硬校验(POSIX):dir 0700,file(存在时)0600 */
148
+ function verifySecureModeInline(dir, file) {
149
+ if (process.platform === 'win32') return { ok: false, unsupported: true }
150
+ try {
151
+ const dirMode = statSync(dir).mode & 0o777
152
+ if (dirMode !== 0o700) return { ok: false, error: `目录权限 ${dirMode.toString(8)} ≠ 700` }
153
+ if (existsSync(file)) {
154
+ const fileMode = statSync(file).mode & 0o777
155
+ if (fileMode !== 0o600) return { ok: false, error: `文件权限 ${fileMode.toString(8)} ≠ 600` }
156
+ }
157
+ return { ok: true }
158
+ } catch (e) {
159
+ return { ok: false, error: e.message }
160
+ }
161
+ }
162
+
163
+ /**
164
+ * 官方 CLI 首次运行可能先创建 ~/.flyai 为 0755(并写入 0600 device-id)。
165
+ * 仅对当前用户拥有、且本身不是 symlink 的 .flyai 目录收紧权限;.gotry
166
+ * 仍由 atomicWriteJsonInline 严格要求预先存在时就是 0700,不走此兼容路径。
167
+ */
168
+ function prepareFlyaiConfigDirInline() {
169
+ const dir = dirname(flyaiConfigFile())
170
+ try {
171
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 })
172
+ const entry = lstatSync(dir)
173
+ if (entry.isSymbolicLink()) return { ok: false, error: '.flyai 目录是符号链接,拒绝写入' }
174
+ if (!entry.isDirectory()) return { ok: false, error: '.flyai 路径不是目录,拒绝写入' }
175
+ if (typeof process.getuid === 'function' && entry.uid !== process.getuid()) {
176
+ return { ok: false, error: '.flyai 目录不属于当前用户,拒绝写入' }
177
+ }
178
+ if ((entry.mode & 0o777) !== 0o700) chmodSync(dir, 0o700)
179
+ const after = lstatSync(dir)
180
+ if (after.isSymbolicLink() || !after.isDirectory()) return { ok: false, error: '.flyai 目录状态发生变化,拒绝写入' }
181
+ if (typeof process.getuid === 'function' && after.uid !== process.getuid()) return { ok: false, error: '.flyai 目录所有者发生变化,拒绝写入' }
182
+ return verifySecureModeInline(dir, flyaiConfigFile())
183
+ } catch (e) {
184
+ return { ok: false, error: e.message }
185
+ }
186
+ }
187
+
188
+ /**
189
+ * 原子写 JSON:tmp(mode 0600)→ 权限校验 → rename → 目标权限校验。
190
+ * finally 删除本操作 tmp:rename 失败也不留含明文残留。
191
+ */
192
+ function atomicWriteJsonInline(path, value) {
193
+ const dir = dirname(path)
194
+ let tmp
195
+ try {
196
+ const dirExisted = existsSync(dir)
197
+ mkdirSync(dir, { recursive: true })
198
+ if (!dirExisted) try { chmodSync(dir, 0o700) } catch { /* 新建目录后 chmod 失败由 mode 校验拦 */ }
199
+ const dirCheck = verifySecureModeInline(dir, path)
200
+ if (dirCheck.unsupported) return { ok: false, error: 'Windows 无 0700/0600 权限位,安全写不支持' }
201
+ if (!dirCheck.ok) return { ok: false, error: dirCheck.error }
202
+
203
+ tmp = `${path}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
204
+ writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 })
205
+ let check = verifySecureModeInline(dir, tmp)
206
+ if (!check.ok) return { ok: false, error: check.error }
207
+
208
+ renameSync(tmp, path)
209
+ tmp = undefined
210
+ check = verifySecureModeInline(dir, path)
211
+ if (!check.ok) return { ok: false, error: check.error }
212
+ return { ok: true }
213
+ } catch (e) {
214
+ return { ok: false, error: e.message }
215
+ } finally {
216
+ if (tmp) { try { rmSync(tmp, { force: true }) } catch { /* best effort */ } }
217
+ }
218
+ }
219
+
220
+ function readFlyaiVerificationInline() {
221
+ try {
222
+ const path = flyaiVerificationFile()
223
+ if (!existsSync(path)) return undefined
224
+ const v = JSON.parse(readFileSync(path, 'utf8'))
225
+ if (v?.schema !== 'gotry.flyai-verification.v1' || typeof v.keySha256 !== 'string') return undefined
226
+ return v
227
+ } catch { return undefined }
228
+ }
229
+
230
+ /**
231
+ * setup flyai 子命令(#521;唯一凭据写面,模型工具不可达):
232
+ * --stdin 非交互输入;缺省 TTY 隐藏输入(raw mode 不回显);
233
+ * --status 只读;--clear 删 config 字段(env/debug 来源清不掉如实标注)。
234
+ * 保存 = 非空/非占位 → 候选 key scrub-env 真实只读验证 → 原子写 → 验证回执。
235
+ * 验证失败/写盘失败不动旧配置。
236
+ */
237
+ async function runSetupFlyai() {
238
+ const STATUS = process.argv.includes('--status')
239
+ const CLEAR = process.argv.includes('--clear')
240
+ const STDIN = process.argv.includes('--stdin')
241
+
242
+ const printStatus = (extra = '') => {
243
+ const r = resolveFlyaiKeyInline()
244
+ const endpoint = resolveFlyaiEndpointInline()
245
+ const verification = readFlyaiVerificationInline()
246
+ say('[gotry-setup] FlyAI(飞猪官方只读检索凭据)')
247
+ say(` 来源: ${r.source}${r.source === 'env-debug' ? '(DEBUG 调试覆盖)' : ''}`)
248
+ if (r.key) say(` key: ${maskFlyaiKeyInline(r.key)}(脱敏;明文不显示)`)
249
+ say(` 配置文件: ${r.configPath}`)
250
+ say(` endpoint: ${displayEndpointInline(endpoint.url)}${endpoint.debug ? '(DEBUG_FLYAI_MCP_URL 调试覆盖)' : ''}`)
251
+ if (r.key) {
252
+ const keySha = sha256Inline(r.key)
253
+ if (verification && verification.keySha256 === keySha) {
254
+ const sourceMatch = verification.source === r.source
255
+ const endpointMatch = verification.endpointFingerprint === endpointFingerprintInline(endpoint.url)
256
+ if (verification.verdict === 'verified' && sourceMatch && endpointMatch && verification.endpointDebug === endpoint.debug) {
257
+ say(` 验证: 已验证 @ ${verification.at}${endpoint.debug ? '(调试 endpoint)' : ''}`)
258
+ } else if (verification.verdict === 'verified') {
259
+ say(' 验证: 已配置,未按当前来源/endpoint 验证(回执与当前生效上下文不匹配)')
260
+ } else {
261
+ say(` 验证: 验证失败(${verification.verdict}@ ${verification.at})——已配置不等于有效`)
262
+ }
263
+ } else {
264
+ say(' 验证: 已配置,未验证(非空不等于鉴权通过;运行不带 --status 的 setup 触发验证)')
265
+ }
266
+ } else {
267
+ say(' 验证: 未配置——可直接匿名试用(共享额度易达限),或粘贴正式 key 验证后保存')
268
+ }
269
+ if (extra) say(extra)
270
+ say(' 凭据修改仅本机命令可达: `gotry setup flyai`(隐藏输入)/ `--stdin` / `--clear`;模型工具不能传 key')
271
+ }
272
+
273
+ if (STATUS) {
274
+ printStatus()
275
+ return 0
276
+ }
277
+
278
+ if (CLEAR) {
279
+ const read = readFlyaiConfigObject()
280
+ if (!read.ok) {
281
+ say(`[gotry-setup] 原配置损坏,已拒绝操作保留原字节(${read.error});请先人工修复 ${flyaiConfigFile()}`)
282
+ return 1
283
+ }
284
+ let hadField = false
285
+ if (Object.prototype.hasOwnProperty.call(read.data, 'FLYAI_API_KEY')) {
286
+ hadField = true
287
+ const next = { ...read.data }
288
+ delete next.FLYAI_API_KEY
289
+ if (Object.keys(next).length === 0) {
290
+ rmSync(flyaiConfigFile(), { force: true })
291
+ } else {
292
+ const configDir = prepareFlyaiConfigDirInline()
293
+ if (!configDir.ok) { say(`[gotry-setup] 清除写入失败,旧配置保留:${configDir.error}`); return 1 }
294
+ const w = atomicWriteJsonInline(flyaiConfigFile(), next)
295
+ if (!w.ok) { say(`[gotry-setup] 清除写入失败,旧配置保留:${w.error}`); return 1 }
296
+ }
297
+ try { rmSync(flyaiVerificationFile(), { force: true }) } catch { /* best effort */ }
298
+ }
299
+ const now = resolveFlyaiKeyInline()
300
+ if (now.key) {
301
+ say(`[gotry-setup] config 字段${hadField ? '已删' : '本不存在'};但当前仍由来源 ${now.source}(${maskFlyaiKeyInline(now.key)})生效——文件操作清不掉 env/debug`)
302
+ } else {
303
+ say(`[gotry-setup] ✓ 已清除${hadField ? `(字段来自 ${flyaiConfigFile()})` : ''};当前匿名试用态`)
304
+ }
305
+ return 0
306
+ }
307
+
308
+ // ── save:取得候选 key ──
309
+ let candidate
310
+ if (STDIN || !process.stdin.isTTY) {
311
+ const chunks = []
312
+ for await (const chunk of process.stdin) chunks.push(chunk)
313
+ candidate = Buffer.concat(chunks).toString('utf8')
314
+ if (!STDIN && !process.stdin.isTTY) {
315
+ // 非 TTY 且未显式 --stdin:仍接受管道输入,但不进入交互隐藏模式。
316
+ }
317
+ } else {
318
+ say('[gotry-setup] FlyAI API key 设置(输入隐藏,不回显;粘贴后回车)')
319
+ say(' 打开 https://flyai.open.fliggy.com/console,登录后复制 API Key。直接回车取消。')
320
+ candidate = await hiddenInput(' key: ')
321
+ }
322
+ candidate = (candidate ?? '').trim()
323
+ if (!candidate) {
324
+ say('[gotry-setup] 未输入 key——已取消,配置未改动(仍可用匿名试用)')
325
+ return 0
326
+ }
327
+ if (isPlaceholderFlyaiKeyInline(candidate)) {
328
+ say('[gotry-setup] 拒绝:这是文档占位串,不是真实 API key;配置未改动')
329
+ return 1
330
+ }
331
+
332
+ // 候选 key 真实只读验证:scrub 掉两个覆盖 env 后注入候选,
333
+ // 保证验证请求用的是候选 key,不被进程已有 env 遮蔽。
334
+ say('[gotry-setup] 用候选 key 做只读验证(单次,不重试鉴权错误)…')
335
+ const verify = await verifyCandidateKeyInline(candidate)
336
+ if (verify.verdict !== 'hit') {
337
+ say(`[gotry-setup] ✗ 验证未通过(${verify.verdict}):${verify.error ?? ''}`)
338
+ say(' 配置未改动(失败不覆盖原有效设置)。核对 key 后重试,或 `--clear` 回退匿名。')
339
+ return 1
340
+ }
341
+
342
+ const before = readFlyaiConfigObject()
343
+ if (!before.ok) {
344
+ say(`[gotry-setup] 原配置损坏,已拒绝覆盖保留原字节(${before.error});请先人工修复 ${flyaiConfigFile()}`)
345
+ return 1
346
+ }
347
+ const hadConfigFile = existsSync(flyaiConfigFile())
348
+ const configDir = prepareFlyaiConfigDirInline()
349
+ if (!configDir.ok) {
350
+ say(`[gotry-setup] ✗ 配置目录不安全,旧配置保留:${configDir.error}`)
351
+ return 1
352
+ }
353
+ const write = atomicWriteJsonInline(flyaiConfigFile(), { ...before.data, FLYAI_API_KEY: candidate })
354
+ if (!write.ok) {
355
+ say(`[gotry-setup] ✗ 写入失败,旧配置保留:${write.error}`)
356
+ return 1
357
+ }
358
+
359
+ const epAfter = resolveFlyaiEndpointInline()
360
+ const endpointFingerprint = endpointFingerprintInline(epAfter.url)
361
+ if (!endpointFingerprint) {
362
+ const rollback = hadConfigFile
363
+ ? atomicWriteJsonInline(flyaiConfigFile(), before.data)
364
+ : (() => { try { rmSync(flyaiConfigFile(), { force: true }); return { ok: true } } catch (e) { return { ok: false, error: e.message } } })()
365
+ say(`[gotry-setup] ✗ endpoint 非法,未生成验证回执;旧配置${rollback.ok ? '已保留' : `回滚失败:${rollback.error}`}`)
366
+ return 1
367
+ }
368
+ const receipt = {
369
+ schema: 'gotry.flyai-verification.v1',
370
+ // 验证的是刚注入并准备写入 config 的候选 key;环境覆盖不能冒充候选来源。
371
+ source: 'config',
372
+ keySha256: sha256Inline(candidate),
373
+ maskedKey: maskFlyaiKeyInline(candidate),
374
+ verdict: 'verified',
375
+ endpointFingerprint,
376
+ endpointDebug: epAfter.debug,
377
+ at: new Date().toISOString(),
378
+ }
379
+ const receiptWrite = atomicWriteJsonInline(flyaiVerificationFile(), receipt)
380
+ if (!receiptWrite.ok) {
381
+ const rollback = hadConfigFile
382
+ ? atomicWriteJsonInline(flyaiConfigFile(), before.data)
383
+ : (() => { try { rmSync(flyaiConfigFile(), { force: true }); return { ok: true } } catch (e) { return { ok: false, error: e.message } } })()
384
+ say(`[gotry-setup] ✗ 验证回执写入失败:${receiptWrite.error};旧配置${rollback.ok ? '已保留' : `回滚失败:${rollback.error}`}`)
385
+ return 1
386
+ }
387
+ const after = resolveFlyaiKeyInline()
388
+ say(`[gotry-setup] ✓ 候选 key 验证通过并已保存(明文不落日志)`)
389
+ printStatus(` savedTo: ${after.configPath}`)
390
+ return 0
391
+ }
392
+
393
+ /** TTY 隐藏输入:raw mode,不回显;Enter 结束;Ctrl+C/Ctrl+D 取消 */
394
+ function hiddenInput(prompt) {
395
+ return new Promise((resolve) => {
396
+ process.stdout.write(prompt)
397
+ let buf = ''
398
+ const stdin = process.stdin
399
+ stdin.setRawMode(true)
400
+ stdin.resume()
401
+ stdin.setEncoding('utf8')
402
+ const stop = () => {
403
+ try { stdin.setRawMode(false) } catch { /* ignore */ }
404
+ stdin.pause()
405
+ process.stdout.write('\n')
406
+ }
407
+ const onData = (d) => {
408
+ for (const ch of d) {
409
+ if (ch === '\n' || ch === '\r') {
410
+ stdin.removeListener('data', onData)
411
+ stop()
412
+ resolve(buf)
413
+ return
414
+ }
415
+ if (ch === '') {
416
+ stdin.removeListener('data', onData)
417
+ stop()
418
+ resolve('')
419
+ return
420
+ }
421
+ if (ch === '') {
422
+ // Ctrl+D
423
+ stdin.removeListener('data', onData)
424
+ stop()
425
+ resolve(buf)
426
+ return
427
+ }
428
+ if (ch === '' || ch === '\x7f') {
429
+ buf = buf.slice(0, -1)
430
+ } else {
431
+ buf += ch
432
+ }
433
+ }
434
+ }
435
+ stdin.on('data', onData)
436
+ })
437
+ }
438
+
439
+ /**
440
+ * 候选 key 验证:spawn 官方 CLI(cliBin 可由测试 env 注入 fixture),
441
+ * 子进程 env scrub FLYAI_API_KEY/DEBUG_FLYAI_API_KEY 后注入候选。
442
+ * 返回 {verdict,error}。
443
+ */
444
+ function verifyCandidateKeyInline(candidate) {
445
+ const GOTRY_FLYAI_CLI = process.env.GOTRY_FLYAI_CLI_BIN ?? ''
446
+ const cliBin = GOTRY_FLYAI_CLI || 'npx'
447
+ const prefix = GOTRY_FLYAI_CLI ? [] : ['-y', '@fly-ai/flyai-cli@1.0.16']
448
+ const env = { ...process.env }
449
+ delete env.FLYAI_API_KEY
450
+ delete env.DEBUG_FLYAI_API_KEY
451
+ env.FLYAI_API_KEY = candidate
452
+ const date = new Date(Date.now() + 7 * 86_400_000).toISOString().slice(0, 10)
453
+ const args = [...prefix, 'search-flight', '--origin', '上海', '--destination', '丽江', '--dep-date', date]
454
+ const configuredTimeout = Number(process.env.GOTRY_FLYAI_VERIFY_TIMEOUT_MS ?? 20_000)
455
+ const timeoutMs = Number.isFinite(configuredTimeout) && configuredTimeout > 0 ? Math.min(configuredTimeout, 60_000) : 20_000
456
+ return new Promise((resolve) => {
457
+ let stdout = ''
458
+ let stderr = ''
459
+ let timedOut = false
460
+ let settled = false
461
+ const child = spawn(cliBin, args, {
462
+ env, stdio: ['ignore', 'pipe', 'pipe'],
463
+ detached: process.platform !== 'win32',
464
+ })
465
+ const finish = (v) => {
466
+ if (settled) return
467
+ settled = true
468
+ resolve(v)
469
+ }
470
+ const killGroup = (signal) => {
471
+ if (process.platform !== 'win32' && child.pid) {
472
+ try { process.kill(-child.pid, signal); return } catch { /* fall through */ }
473
+ }
474
+ try { child.kill(signal) } catch { /* ignore */ }
475
+ }
476
+ const timer = setTimeout(() => { timedOut = true; killGroup('SIGKILL') }, timeoutMs)
477
+ child.stdout?.on('data', d => { stdout += d.toString() })
478
+ child.stderr?.on('data', d => { stderr += d.toString() })
479
+ child.on('error', (e) => { clearTimeout(timer); finish({ verdict: 'error', error: e.message }) })
480
+ child.on('close', (code) => {
481
+ clearTimeout(timer)
482
+ const combined = `${stderr}\n${stdout}`
483
+ if (timedOut) return finish({ verdict: 'timeout', error: `验证调用超时 ${timeoutMs}ms` })
484
+ if (/Invalid API key|HTTP\s*401|\b401\b/.test(combined)) {
485
+ return finish({ verdict: 'auth-error', error: '401 Invalid API key' })
486
+ }
487
+ if (/HTTP\s*403|\b403\b/.test(combined)) {
488
+ return finish({ verdict: 'forbidden', error: 'HTTP 403:无访问权限,请核对控制台权限。' })
489
+ }
490
+ if (/Trial limit reached/.test(combined)) {
491
+ return finish({ verdict: 'needs-setup', error: 'Trial limit reached(候选 key 未被识别为正式 key?)' })
492
+ }
493
+ if (/HTTP\s*429|\b429\b/.test(combined)) {
494
+ return finish({ verdict: 'rate-limited', error: '普通限流,稍后重试' })
495
+ }
496
+ if (code !== 0) {
497
+ // Upstream diagnostics can echo credentials or a private debug URL.
498
+ return finish({ verdict: 'error', error: `验证进程失败(exit ${code});请检查网络与服务状态。` })
499
+ }
500
+ try {
501
+ const envelope = JSON.parse(stdout)
502
+ if (Array.isArray(envelope?.data?.itemList)) {
503
+ return finish({ verdict: 'hit', count: envelope.data.itemList.length })
504
+ }
505
+ return finish({ verdict: 'error', error: '响应缺 data.itemList' })
506
+ } catch {
507
+ return finish({ verdict: 'error', error: '响应不是合法 JSON' })
508
+ }
509
+ })
510
+ })
511
+ }
512
+
513
+
514
+ const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..')
515
+ const AUTO = process.argv.includes('--auto')
516
+ const CHECK_ONLY = process.argv.includes('--check-only')
517
+ const WIZARD = process.argv.includes('wizard') || process.argv.includes('--wizard')
518
+ const WIZARD_DRY_RUN = process.argv.includes('--dry-run')
519
+ // 扩展分发源(ADR-21):默认 bundled(包内副本,离线确定性);github 显式 opt-in
520
+ // (镜像 GOTRY_SESSION_TRANSPORT=cdp 的显式 opt-in 文化——不静默引入网络依赖)
521
+ const EXT_FROM_ARG = (process.argv.find((a) => a.startsWith('--extension-from=')) ?? '').split('=')[1]
522
+ const EXTENSION_FROM = EXT_FROM_ARG === 'github' || EXT_FROM_ARG === 'bundled' ? EXT_FROM_ARG : process.env.GOTRY_EXTENSION_SOURCE === 'github' ? 'github' : 'bundled'
523
+
524
+ // hbcli 安装通道:npm 优先(staicli@npmjs 双轨;显式 registry 防 bnpm 404),install.sh 兜底
525
+ const HBCLI_NPM_INSTALL_CMD = 'npm install -g staicli --registry=https://registry.npmjs.org/'
526
+ const HBCLI_INSTALL_CMD = HBCLI_NPM_INSTALL_CMD
527
+ const REACH_INSTALL_URL = 'git+https://github.com/Panniantong/Agent-Reach.git'
528
+
529
+ let activeInstallerChild = null
530
+ let activeInstallerGroupPid = null
531
+ let installerSignalHandling = false
532
+ const installerSignalListeners = new Map()
533
+ const INSTALLER_TERM_GRACE_MS = 2_000
534
+ const INSTALLER_KILL_WAIT_MS = 1_000
535
+ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
536
+ const waitChildClose = (child, ms) => new Promise((resolve) => {
537
+ if (!child) { resolve(false); return }
538
+ let done = false
539
+ const finish = (value) => {
540
+ if (done) return
541
+ done = true
542
+ clearTimeout(timer)
543
+ child.off('close', onClose)
544
+ child.off('error', onError)
545
+ resolve(value)
546
+ }
547
+ const onClose = () => finish(true)
548
+ const onError = () => finish(true)
549
+ const timer = setTimeout(() => finish(false), ms)
550
+ child.once('close', onClose)
551
+ child.once('error', onError)
552
+ })
553
+ const signalInstallerChild = (child, signal, groupPid = null) => {
554
+ if (!child) return
555
+ if (process.platform !== 'win32' && groupPid) {
556
+ try { process.kill(-groupPid, signal); return } catch { /* fallback */ }
557
+ }
558
+ try { child.kill(signal) } catch { /* ignore */ }
559
+ }
560
+ const terminateInstallerChild = async (child, signal, groupPid = null, graceMs = INSTALLER_TERM_GRACE_MS, killMs = INSTALLER_KILL_WAIT_MS) => {
561
+ if (!child) return
562
+ signalInstallerChild(child, signal, groupPid)
563
+ const closed = await waitChildClose(child, graceMs)
564
+ if (closed) return
565
+ signalInstallerChild(child, 'SIGKILL', groupPid)
566
+ await waitChildClose(child, killMs)
567
+ }
568
+ const registerInstallerSignalForwarding = () => {
569
+ if (installerSignalListeners.size) return
570
+ for (const sig of ['SIGINT', 'SIGTERM']) {
571
+ const listener = () => {
572
+ if (installerSignalHandling) return
573
+ installerSignalHandling = true
574
+ void (async () => {
575
+ await terminateInstallerChild(activeInstallerChild, sig, activeInstallerGroupPid)
576
+ for (const [s, l] of installerSignalListeners) process.off(s, l)
577
+ installerSignalListeners.clear()
578
+ process.kill(process.pid, sig)
579
+ await wait(2_000)
580
+ process.exit(128 + (sig === 'SIGINT' ? 2 : 15))
581
+ })()
582
+ }
583
+ process.on(sig, listener)
584
+ installerSignalListeners.set(sig, listener)
585
+ }
586
+ }
587
+ const unregisterInstallerSignalForwarding = () => {
588
+ for (const [s, l] of installerSignalListeners) process.off(s, l)
589
+ installerSignalListeners.clear()
590
+ installerSignalHandling = false
591
+ }
592
+
593
+ /** 带超时的子进程(inherit stdio 让用户看见上游安装进度);POSIX 下用进程组约束 installer 子树 */
594
+ function run(cmd, args, { timeoutMs, cwd, env } = {}) {
595
+ return new Promise((resolve) => {
596
+ const detachedInstaller = process.platform !== 'win32'
597
+ const child = spawn(cmd, args, { stdio: 'inherit', cwd, env: env ?? process.env, detached: detachedInstaller })
598
+ activeInstallerChild = child
599
+ activeInstallerGroupPid = detachedInstaller ? child.pid : null
600
+ registerInstallerSignalForwarding()
601
+ let done = false
602
+ let timer = null
603
+ const finish = (result) => {
604
+ if (done) return
605
+ done = true
606
+ if (timer) clearTimeout(timer)
607
+ activeInstallerChild = null
608
+ activeInstallerGroupPid = null
609
+ unregisterInstallerSignalForwarding()
610
+ resolve(result)
611
+ }
612
+ timer = setTimeout(() => {
613
+ void (async () => {
614
+ if (done) return
615
+ await terminateInstallerChild(child, 'SIGTERM', activeInstallerGroupPid)
616
+ finish({ ok: false, error: 'timeout' })
617
+ })()
618
+ }, timeoutMs)
619
+ child.on('error', (e) => finish({ ok: false, error: e.message }))
620
+ child.on('close', (code, signal) => finish({ ok: code === 0, error: code === 0 ? undefined : signal ? `signal ${signal}` : `exit ${code}` }))
621
+ })
622
+ }
623
+
624
+ /** 静默探测命令是否可执行(带超时) */
625
+ function probe(cmd, args, timeoutMs = 10_000) {
626
+ return new Promise((resolve) => {
627
+ const child = spawn(cmd, args, { stdio: 'ignore' })
628
+ let done = false
629
+ const timer = setTimeout(() => {
630
+ if (!done) { done = true; try { child.kill('SIGKILL') } catch { /* ignore */ } resolve(false) }
631
+ }, timeoutMs)
632
+ child.on('error', () => { if (!done) { done = true; clearTimeout(timer); resolve(false) } })
633
+ child.on('exit', (code) => { if (!done) { done = true; clearTimeout(timer); resolve(code === 0) } })
634
+ })
635
+ }
636
+
637
+ /**
638
+ * 探测命令 stdout,失败(spawn 错误/非零退出/超时)返回 null。
639
+ * 在 'close' 而非 'exit' 结算:'exit' 触发时 stdout 管道可能还没读完,输出会丢成空串
640
+ * (与 ts/capabilities/doctor.ts 的 probeStdout 同一处修复,#622)。
641
+ */
642
+ function probeStdout(cmd, args, timeoutMs = 10_000) {
643
+ return new Promise((resolve) => {
644
+ const child = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'ignore'] })
645
+ let out = ''
646
+ let done = false
647
+ const timer = setTimeout(() => {
648
+ if (!done) { done = true; try { child.kill('SIGKILL') } catch { /* ignore */ } resolve(null) }
649
+ }, timeoutMs)
650
+ child.on('error', () => { if (!done) { done = true; clearTimeout(timer); resolve(null) } })
651
+ child.stdout?.on('data', (d) => { out += d.toString() })
652
+ child.on('close', (code) => { if (!done) { done = true; clearTimeout(timer); resolve(code === 0 ? out.trim() : null) } })
653
+ })
654
+ }
655
+
656
+ /**
657
+ * 按 `hbcli --json auth whoami` 的输出判凭证(hotelbyte-cli 三档 api_key/portal/customer 任一
658
+ * configured 即有效),不看退出码——whoami 没有凭证时也退 0,按退出码判会把「没登录」报成「凭证有效」(#623)。
659
+ * 与工具层 ts/capabilities/doctor.ts 同语义:输出不可解析(超旧版/异常)保守视为有效;取不到输出(null)视为未确认。
660
+ * 两处实现的一致性由 bootstrap-tests 的 CLI↔工具层对拍守住。
661
+ */
662
+ function classifyHbcliWhoami(out) {
663
+ if (out === null) return { ok: false, customer: false }
664
+ try {
665
+ const w = JSON.parse(out)
666
+ const customer = !!w?.customer?.configured
667
+ return { ok: !!(w?.api_key?.configured || w?.portal?.configured || customer), customer }
668
+ } catch {
669
+ return { ok: true, customer: false }
670
+ }
671
+ }
672
+
673
+ const say = (s) => console.log(s)
674
+
675
+ /** hbcli 最低版本(可用 GOTRY_MIN_HBCLI_VERSION 覆盖)。
676
+ * 0.0.3 修了 portal-ticket fallback bug(issue #142):低于该版本的 hbcli
677
+ * 会把 literal "stored-ticket" 当 bearer 发到 hotel-be,让 trade.* /
678
+ * search/checkAvail 等 @permission: openapi 端点全部 401。 */
679
+ const MIN_HBCLI_VERSION = process.env.GOTRY_MIN_HBCLI_VERSION ?? '0.0.3'
680
+
681
+ function parseVersion(v) {
682
+ const m = String(v || '').match(/(\d+)\.(\d+)\.(\d+)/)
683
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null
684
+ }
685
+
686
+ function versionAtLeast(v, min) {
687
+ // v 可能是 hbcliVersion 已解析的数组,也可能是原始字符串——两者都接受
688
+ const a = Array.isArray(v) ? v : parseVersion(v); const b = parseVersion(min)
689
+ if (!a || !b) return false
690
+ for (let i = 0; i < 3; i++) { if (a[i] !== b[i]) return a[i] > b[i] }
691
+ return true
692
+ }
693
+
694
+ async function hbcliVersion(bin) {
695
+ const cmd = bin === 'hbcli(PATH)' ? 'hbcli' : bin
696
+ return new Promise((resolve) => {
697
+ const child = spawn(cmd, ['--version'], { stdio: ['ignore', 'pipe', 'ignore'] })
698
+ let out = ''
699
+ let done = false
700
+ const timer = setTimeout(() => { if (!done) { done = true; try { child.kill('SIGKILL') } catch { /* ignore */ } resolve(null) } }, 10_000)
701
+ child.on('error', () => { if (!done) { done = true; clearTimeout(timer); resolve(null) } })
702
+ child.stdout?.on('data', (d) => { out += d.toString() })
703
+ child.on('close', (code) => { if (!done) { done = true; clearTimeout(timer); resolve(code === 0 ? parseVersion(out.trim()) : null) } })
704
+ })
705
+ }
706
+
707
+ async function setupHbcli() {
708
+ say(`[gotry-setup] hbcli(hotelbyte-cli,可选酒店实时源;需 ≥ v${MIN_HBCLI_VERSION},低于会踩 issue #142 的 401)`)
709
+ const candidates = ['hbcli', join(homedir(), '.local/bin/hbcli'), join(homedir(), '.staicli/current/hbcli')]
710
+ const present = candidates.some((p) => existsSync(p)) && (await probe(candidates[0], ['version']) || await probe(candidates[1], ['version']) || await probe(candidates[2], ['version']))
711
+ const credFile = join(homedir(), '.staicli', 'credentials.json')
712
+ if (present) {
713
+ const hit = candidates.find((p) => existsSync(p)) || 'hbcli'
714
+ const v = await hbcliVersion(hit)
715
+ if (v && !versionAtLeast(v, MIN_HBCLI_VERSION)) {
716
+ say(` ✗ 版本过旧(v${v.join('.')} < v${MIN_HBCLI_VERSION})——@permission: openapi 端点(trade.* / search/checkAvail)会 401(issue #142)`)
717
+ say(` 升级: npm install -g staicli --registry=https://registry.npmjs.org/`)
718
+ return { ok: CHECK_ONLY ? true : false }
719
+ }
720
+ say(` ✓ 已安装(v${v ? v.join('.') : '?'})`)
721
+ if (existsSync(credFile)) say(' ✓ 凭证已配置(自检: hbcli auth whoami)')
722
+ else say(' ✗ 凭证未配置——酒店检索将用内置静态包(非实时),账号配置见下方指引')
723
+ if (CHECK_ONLY) say(' (--check-only 只报告,不安装)')
724
+ return { ok: true }
725
+ }
726
+ if (CHECK_ONLY) { say(' ✗ 未安装(--check-only 只报告)'); return { ok: true } }
727
+ say(` 安装中(npm,staicli@npmjs): ${HBCLI_INSTALL_CMD}`)
728
+ const r = await run('bash', ['-c', HBCLI_INSTALL_CMD], { timeoutMs: 120_000 })
729
+ if (!r.ok) { say(` ✗ 安装失败(${r.error})——不影响 gotry,酒店检索将用内置静态包;可稍后重试: npx @danceiny/gotry setup`); return { ok: false } }
730
+ const binDir = join(homedir(), '.local/bin')
731
+ if (!process.env.PATH.split(':').includes(binDir)) {
732
+ say(` ⚠ ${binDir} 不在当前 PATH —— gotry 工具已内建候选路径回退,无需手动处理;其他程序可用: export PATH="${binDir}:$PATH"`)
733
+ }
734
+ say(' ✓ 安装完成——账号配置(未配时酒店检索自动用静态包,非实时):')
735
+ say(' · 快速试用(官方沙箱演示账号): hbcli auth set-credentials --app-key hotelbyte_api_demo --app-secret hotelbyte_api_demo')
736
+ say(' · 正式接入: 向 HotelByte 申请专属 appKey/appSecret 后用同一命令替换')
737
+ say(' · 门户账号(权限更大,酒店搜索无必要): hbcli auth login --username <email>')
738
+ say(' 自检: hbcli auth whoami(api_key.configured=true 即就位)')
739
+ return { ok: true }
740
+ }
741
+
742
+ async function setupReach() {
743
+ say('[gotry-setup] agent-reach(可选网页/社媒读取源,pip 装入包内 .venv)')
744
+ const venvBin = join(repoRoot, '.venv/bin/agent-reach')
745
+ if (existsSync(venvBin)) { say(' ✓ 已安装(.venv)'); return { ok: true } }
746
+ if (CHECK_ONLY) { say(' ✗ 未安装(--check-only 只报告)'); return { ok: true } }
747
+ const hasPy = await probe('python3', ['--version'], 10_000)
748
+ if (!hasPy) { say(' ✗ 跳过:未找到 python3(agent-reach 需 Python 3;装好后重跑 npx @danceiny/gotry setup)'); return { ok: false } }
749
+ say(` 创建 .venv 并安装上游(${REACH_INSTALL_URL})`)
750
+ const venv = await run('python3', ['-m', 'venv', join(repoRoot, '.venv')], { timeoutMs: 120_000 })
751
+ if (!venv.ok) { say(` ✗ venv 创建失败(${venv.error})——不影响 gotry,gotry_agent_reach 将返回 not-installed 指引`); return { ok: false } }
752
+ const pip = join(repoRoot, '.venv/bin/pip')
753
+ const inst = await run(pip, ['install', '-q', REACH_INSTALL_URL], { timeoutMs: 300_000 })
754
+ if (!inst.ok) { say(` ✗ pip 安装失败(${inst.error})——可稍后重试: npx @danceiny/gotry setup`); return { ok: false } }
755
+ say(' ✓ 安装完成(渠道凭证选配见 docs/tokens.md: .venv/bin/agent-reach configure --from-browser chrome --platform <渠道>)')
756
+ return { ok: true }
757
+ }
758
+
759
+ /** sidebar 落盘状态 = 与 doctor 检查同口径的唯一事实面(profile 里的 package.json) */
760
+ function sidebarInstalled() {
761
+ return existsSync(join(homedir(), '.dsh/profiles/web/node_modules/dsh-better-sidebar/package.json'))
762
+ }
763
+
764
+ /** dsh plugin 在 profile 目录里跑 pnpm;pnpm ≥10.5 默认不执行依赖构建脚本,严格态
765
+ * (pnpm 11 默认 / strict-dep-builds)下 ERR_PNPM_IGNORED_BUILDS 直接 exit 1——
766
+ * 但包本身已完整落盘(2026-09-08 实测:exit 1 而 167 包全部就位,doctor 复检即绿)。
767
+ * 对该调用降回警告态(env 只作用于这一次 spawn,不改用户全局 pnpm 配置);
768
+ * 老版本 pnpm 不认识此配置,零影响。 */
769
+ const SIDEBAR_PNPM_ENV = { ...process.env, npm_config_strict_dep_builds: 'false' }
770
+
771
+ async function setupSidebar(attemptInstall) { say('[gotry-setup] dsh-better-sidebar(dsh web 侧栏工作台,产物查看面 issue #25)')
772
+ if (sidebarInstalled()) { say(' ✓ 已安装(~/.dsh/profiles/web)'); return { ok: true } }
773
+ if (CHECK_ONLY) { say(' ✗ 未安装(--check-only 只报告)'); return { ok: true } }
774
+ const SIDEBAR_PKG = 'dsh-better-sidebar@latest'
775
+ // dsh CLI:本包依赖里的 @deepseek-ai/dsh 优先;解析不到走 npx 自拉(官方安装途径同款)。
776
+ // attemptInstall 注入位供测试模拟「安装器失败但状态落盘」(pnpm 忽略构建脚本策略)。
777
+ const require_ = createRequire(join(repoRoot, 'package.json'))
778
+ const defaultAttempt = async () => {
779
+ try {
780
+ const dshBin = require_.resolve('@deepseek-ai/dsh/lib/bin.js')
781
+ say(` 安装中(dsh plugin → web profile): ${SIDEBAR_PKG}`)
782
+ return await run(process.execPath, [dshBin, 'plugin', '--profile', 'web', 'add', SIDEBAR_PKG], { timeoutMs: 300_000, env: SIDEBAR_PNPM_ENV })
783
+ } catch { return { ok: false, noDsh: true } }
784
+ }
785
+ const r = await (attemptInstall ?? defaultAttempt)()
786
+ if (r.ok) {
787
+ say(' ✓ 安装完成(gotry web 刷新浏览器即见右侧工作台;工作区里的产物 md 可直接预览)')
788
+ return { ok: true }
789
+ }
790
+ // exit 非 0 ≠ 没装上(pnpm 忽略构建脚本策略):先按落盘状态复核,再决定是否回退 npx 途径
791
+ if (sidebarInstalled()) {
792
+ say(' ✓ 安装完成(安装器 exit 非 0,但按落盘状态复核已就位;gotry web 刷新浏览器即见右侧工作台)')
793
+ say(' ⚠ pnpm 跳过了 node-pty(侧栏内嵌终端)的构建脚本——预览功能不受影响;如需内嵌终端:cd ~/.dsh/profiles/web && pnpm approve-builds 勾选 node-pty')
794
+ return { ok: true }
795
+ }
796
+ if (r.noDsh) say(' 本包未携带 @deepseek-ai/dsh,走 npx 途径安装')
797
+ else say(' ✗ dsh plugin 安装失败且未落盘——尝试 npx 途径')
798
+ const r2 = await run('npx', ['-y', '--package', '@deepseek-ai/dsh', 'dsh', 'plugin', '--profile', 'web', 'add', SIDEBAR_PKG], { timeoutMs: 300_000, env: SIDEBAR_PNPM_ENV })
799
+ if (r2.ok) {
800
+ say(' ✓ 安装完成(gotry web 刷新浏览器即见右侧工作台)')
801
+ return { ok: true }
802
+ }
803
+ if (sidebarInstalled()) {
804
+ say(' ✓ 安装完成(npx 途径 exit 非 0,但按落盘状态复核已就位)')
805
+ return { ok: true }
806
+ }
807
+ say(' ✗ 安装失败——不影响 gotry:产物仍可在对话里说「看看我生成的行程」经 gotry_artifacts_list/read 查看;可稍后重试: npx @danceiny/gotry doctor --fix')
808
+ return { ok: false }
809
+ }
810
+
811
+ // ---------------------------------------------------------------------------
812
+ // doctor(可选依赖体检;与 ts/capabilities/doctor.ts 同一状态面,-bootstrap 是
813
+ // CLI 执行面纯 JS、capabilities 是 MCP 工具面 TS——两边清单/口径成对修改)
814
+ // ---------------------------------------------------------------------------
815
+
816
+ const DOCTOR = process.argv.includes('doctor')
817
+ const DOCTOR_FIX = process.argv.includes('--fix')
818
+ // onboarding 子命令(issue #258):`gotry web` 启动前的显式可选能力配置 prompt——每次符合条件的
819
+ // 启动评估一次、至多问一次,无跨启动持久确认。inner 以 `onboarding --result-file=…` 同步 inherit
820
+ // 调用;`--scan` 只读查看计划(测试/调试)。
821
+ const ONBOARDING = process.argv.includes('onboarding')
822
+ // calendar 子命令(issue #106/D-9):可选日历挂载的 setup 状态管理面。
823
+ // `gotry setup calendar`=开启;`--off`=关闭(删状态文件恢复默认);`--status`=只读查看。
824
+ const CALENDAR_CMD = process.argv.includes('calendar')
825
+ const CALENDAR_OFF = process.argv.includes('--off')
826
+ const CALENDAR_STATUS = process.argv.includes('--status')
827
+
828
+ // setup flyai 子命令(issue #521):`gotry setup flyai [--stdin|--status|--clear]`。
829
+ // 与 calendar 区分:argv 同时含 'setup' 与 'flyai'。
830
+ const FLYAI_SETUP_CMD = process.argv.includes('flyai') && !DOCTOR && !process.argv.some((a) => a === 'search-flight' || a === 'search-hotel' || a === 'search-train')
831
+
832
+ // --- calendar setup 状态面(与扩展 manifest 同居 ~/.gotry;运行时 inner 与
833
+ // doctor 两端同读这一份,禁止 env 控制产品行为——founder 2026-09-03 纠偏)---
834
+ function calendarStatePath() { return join(homedir(), '.gotry', 'calendar.json') }
835
+ function readCalendarState() {
836
+ try { return JSON.parse(readFileSync(calendarStatePath(), 'utf8')) } catch { return null }
837
+ }
838
+ function calendarProfileConfigured() {
839
+ try {
840
+ const patch = readFileSync(join(homedir(), '.dsh', 'profiles', 'web', 'cordis.patch.yml'), 'utf8')
841
+ return /calendar/.test(patch) && /username\s*:/.test(patch)
842
+ } catch { return false }
843
+ }
844
+
845
+ /** 挂载/配置双态人话(bootstrap doctor 与 calendar 子命令共用) */
846
+ function calendarDetail(state) {
847
+ if (!state || state.enabled !== true) return '默认未挂载(D-9:未配置的日历工具不进工具箱;工作窗口由访谈覆盖,不影响任何检索)'
848
+ return calendarProfileConfigured()
849
+ ? `已挂载且已配置(${calendarStatePath()})`
850
+ : '已挂载但 calendar 未配置 username——日历工具会话中会报「未配置」;影响面:工作窗口只能由访谈覆盖,日历读取工具不会自动跑;机票/酒店/地图等检索不受影响'
851
+ }
852
+
853
+ async function runCalendar() {
854
+ say('[gotry-setup] dsh-calendar(可选日历,CalDAV 工作窗口读取;默认不挂载)')
855
+ if (CALENDAR_STATUS) {
856
+ const state = readCalendarState()
857
+ say(` 状态: ${calendarDetail(state)}`)
858
+ say(` 状态文件: ${calendarStatePath()}`)
859
+ say(' 说明: 挂载=`npx @danceiny/gotry setup calendar`;关闭=`npx @danceiny/gotry setup calendar --off`;配置在 dsh profile 的 cordis.patch.yml 覆盖 calendar 行 config 填 username')
860
+ return 0
861
+ }
862
+ if (CALENDAR_OFF) {
863
+ try { rmSync(calendarStatePath()) } catch { /* 本就未开启 */ }
864
+ say(' ✓ 已关闭——恢复默认不挂载(状态文件已删除;如需再开: npx @danceiny/gotry setup calendar)')
865
+ return 0
866
+ }
867
+ mkdirSync(dirname(calendarStatePath()), { recursive: true })
868
+ writeFileSync(calendarStatePath(), `${JSON.stringify({ enabled: true, updatedAt: new Date().toISOString() }, null, 2)}\n`, 'utf8')
869
+ say(` ✓ 已开启挂载(状态文件 ${calendarStatePath()};重启 gotry web/headless 生效)`)
870
+ if (calendarProfileConfigured()) {
871
+ say(' ✓ calendar 已在 dsh profile 配置 username——全部就绪')
872
+ } else {
873
+ say(' ⚠ 还差最后一步:在 dsh profile 的 cordis.patch.yml 覆盖 calendar 行的 config,填你的 CalDAV username,例如:')
874
+ say(' - id: dsh-calendar')
875
+ say(' config:')
876
+ say(' username: <你的日历账号>')
877
+ say(' 未配置时日历工具会话中会报「未配置」;不需要日历时可用 --off 恢复默认不挂载。')
878
+ }
879
+ return 0
880
+ }
881
+
882
+ /** 逐项只读检查(永不抛错;LLM key 显式让渡给 dsh 宿主,不体检)。
883
+ * level 与 ts/capabilities/doctor.ts DoctorStatus 同构:ok / missing / degraded。 */
884
+ async function doctorChecks() {
885
+ const items = []
886
+ // Node 运行时
887
+ const nv = process.versions.node
888
+ const [maj, min] = nv.split('.').map(Number)
889
+ const nodeFine = maj > 22 || (maj === 22 && min >= 15)
890
+ items.push({ label: 'Node 运行时', ok: nodeFine, level: nodeFine ? 'ok' : 'missing', detail: `Node ${nv}(需 ≥22.15)`, fix: nodeFine ? undefined : '升级 Node.js 至 22.15+(https://nodejs.org)' })
891
+ // 扩展
892
+ const extManifest = join(homedir(), '.gotry', 'extension', 'manifest.json')
893
+ const extOk = existsSync(extManifest)
894
+ items.push({ label: 'GoTry Session Bridge 扩展', ok: extOk, level: extOk ? 'ok' : 'missing', detail: extOk ? `已就位(${extManifest})` : '未安装——影响面:gotry_session_search / gotry_session_login(账号会话通道)不可用;携程机票/酒店实时检索、12306 余票、Dida 实时报价都依赖此通道。其它工具(机票 FlyAI、酒店 hbcli、地图、天气等)不受影响', fix: extOk ? undefined : '在 Chrome 应用商店一键安装(自动更新): https://chromewebstore.google.com/detail/gotry-session-bridge/oeajpiccmonococjcegddlooeeohlbgd' })
895
+ // agent-reach(.venv 装在包内)
896
+ const venvPython = join(repoRoot, '.venv/bin/python')
897
+ const reachBin = join(repoRoot, '.venv/bin/agent-reach')
898
+ const reachOk = existsSync(reachBin)
899
+ const reachLevel = reachOk ? 'ok' : existsSync(venvPython) ? 'degraded' : 'missing'
900
+ items.push({ label: 'Agent Reach(网页/社媒读取)', ok: reachOk, level: reachLevel, detail: reachOk ? `已安装(${reachBin})` : reachLevel === 'degraded' ? '.venv 在但缺 agent-reach 包——gotry_agent_reach / gotry_web_search 读页会失败' : '未装配——gotry_agent_reach / gotry_web_search(读网页)/ gotry_video_subtitle / gotry_github_search 全部不可用', fix: reachOk ? undefined : 'npx @danceiny/gotry doctor --fix' })
901
+ // hbcli(裸名靠 PATH 探测;绝对路径 existsSync;版本 < MIN_HBCLI_VERSION 视为 missing,见 issue #142)
902
+ let hbBin = ''
903
+ for (const p of ['hbcli', join(homedir(), '.local/bin/hbcli'), join(homedir(), '.staicli/current/hbcli')]) {
904
+ if (p === 'hbcli') { if (await probe(p, ['version'])) { hbBin = 'hbcli(PATH)'; break } } else if (existsSync(p)) { hbBin = p; break }
905
+ }
906
+ if (hbBin) {
907
+ const v = await hbcliVersion(hbBin)
908
+ if (v && !versionAtLeast(v, MIN_HBCLI_VERSION)) {
909
+ items.push({ label: 'hbcli(酒店实时源)', ok: false, level: 'missing', detail: `版本过旧(v${v.join('.')} < v${MIN_HBCLI_VERSION})——trade.* / search/checkAvail 会 401(issue #142)`, fix: 'npm install -g staicli --registry=https://registry.npmjs.org/' })
910
+ } else {
911
+ // 凭证按 whoami 的 JSON 三档判定,不再看退出码(#623:whoami 无凭证也退 0,旧判法恒报「凭证有效」)
912
+ const cred = classifyHbcliWhoami(await probeStdout(hbBin === 'hbcli(PATH)' ? 'hbcli' : hbBin, ['--json', 'auth', 'whoami']))
913
+ items.push({
914
+ label: 'hbcli(酒店实时源)', ok: cred.ok, level: cred.ok ? 'ok' : 'degraded',
915
+ detail: cred.ok
916
+ ? `已安装且凭证有效(${hbBin}, v${v ? v.join('.') : '?'}${cred.customer ? ',customer 档在用(客户邮箱验证码登录)' : ''})`
917
+ : '二进制在,但凭证未配置/失效——酒店检索将降级静态包(非实时)',
918
+ fix: cred.ok ? undefined : 'hbcli auth customer-send-code --email you@mail.com 收码后 hbcli auth customer-login --email you@mail.com --code <收件码>(客户邮箱验证码登录,新邮箱即注册,需 npm staicli ≥ 0.0.4);B 端租户注册走 hbcli auth register(先 auth send-code);或 hbcli auth set-credentials --app-key hotelbyte_api_demo --app-secret hotelbyte_api_demo(快速试用沙箱;正式 key 向 HotelByte 申请)',
919
+ })
920
+ }
921
+ } else {
922
+ items.push({ label: 'hbcli(酒店实时源)', ok: false, level: 'missing', detail: '未安装——酒店检索降级静态包(公开渠道估算,非实时,仅覆盖内置场景)', fix: 'npx @danceiny/gotry doctor --fix' })
923
+ }
924
+ // flyai(#521):区分「已配置」与「已验证」——非空只表示 configured,
925
+ // 不承诺正式有效/无限额;有效性必须有 key hash+endpoint 匹配的验证回执。
926
+ const flyaiResolved = resolveFlyaiKeyInline()
927
+ const flyaiEndpoint = resolveFlyaiEndpointInline()
928
+ const flyaiReceipt = readFlyaiVerificationInline()
929
+ let flyaiLevel = 'degraded'
930
+ let flyaiDetail
931
+ let flyaiFix
932
+ if (!flyaiResolved.key) {
933
+ flyaiDetail = '未配置 key——匿名试用中(共享额度易达限;达限报 Trial limit reached)。影响面:共享池额度小,频繁会话易触顶;达限本会话内 gotry_flyai_search(机票/酒店/票务/AI/万豪 8 类)失败,改走 gotry_session_search 账号会话通道。'
934
+ flyaiFix = '本机运行 `gotry setup flyai`(隐藏输入,先验证后保存 FLYAI_API_KEY);打开 https://flyai.open.fliggy.com/console,登录后复制 API Key'
935
+ } else {
936
+ const keySha = sha256Inline(flyaiResolved.key)
937
+ const receiptMatches = flyaiReceipt
938
+ && flyaiReceipt.keySha256 === keySha
939
+ && flyaiReceipt.source === flyaiResolved.source
940
+ && flyaiReceipt.verdict === 'verified'
941
+ && flyaiReceipt.endpointFingerprint === endpointFingerprintInline(flyaiEndpoint.url)
942
+ && flyaiReceipt.endpointDebug === flyaiEndpoint.debug
943
+ const sourceNote = `来源 ${flyaiResolved.source}${flyaiResolved.source === 'env-debug' ? '(DEBUG)' : ''},${maskFlyaiKeyInline(flyaiResolved.key)}`
944
+ if (receiptMatches) {
945
+ flyaiLevel = 'ok'
946
+ flyaiDetail = `已验证 ${flyaiReceipt.at}(${sourceNote};endpoint ${displayEndpointInline(flyaiEndpoint.url)}${flyaiEndpoint.debug ? '(DEBUG)' : ''})`
947
+ } else if (flyaiReceipt && flyaiReceipt.keySha256 === keySha && flyaiReceipt.verdict !== 'verified') {
948
+ flyaiDetail = `已配置未通过验证(${flyaiReceipt.verdict}@ ${flyaiReceipt.at};${sourceNote};endpoint ${displayEndpointInline(flyaiEndpoint.url)}${flyaiEndpoint.debug ? '(DEBUG)' : ''})——非空不等于鉴权通过`
949
+ flyaiFix = '本机运行 `gotry setup flyai` 重新验证;`gotry setup flyai --clear` 回退匿名'
950
+ } else {
951
+ flyaiDetail = `已配置,未验证(${sourceNote};endpoint ${displayEndpointInline(flyaiEndpoint.url)}${flyaiEndpoint.debug ? '(DEBUG)' : ''})`
952
+ flyaiFix = '本机运行 `gotry setup flyai`(候选 key scrub-env 验证后保存)'
953
+ }
954
+ }
955
+ items.push({ label: 'FlyAI(飞猪官方检索:机/火/酒/景/关键词/AI/万豪 8 类)', ok: flyaiLevel === 'ok', level: flyaiLevel, detail: flyaiDetail, fix: flyaiFix })
956
+ // sidebar(状态面与 setupSidebar 的落盘复核同一口径)
957
+ const sbOk = sidebarInstalled()
958
+ items.push({ label: 'dsh-better-sidebar(侧栏工作台)', ok: sbOk, level: sbOk ? 'ok' : 'missing', detail: sbOk ? '已安装——web UI 右侧工作台可预览产物与 doctor 报告(gotry-state/doctor-report.md)' : '未安装——dsh web 无右侧工作台,产物与 doctor 报告只能在对话里看(gotry_artifacts_list)', fix: sbOk ? undefined : 'npx @danceiny/gotry doctor --fix' })
959
+ // dsh-calendar(setup 状态面;默认不挂载=ok 是合法态,opt-in 未配置才 degraded)
960
+ const calState = readCalendarState()
961
+ const calOn = calState?.enabled === true
962
+ const calConfigured = calOn && calendarProfileConfigured()
963
+ items.push({ label: 'dsh-calendar(日历工作窗口)', ok: !calOn || calConfigured, level: !calOn ? 'ok' : calConfigured ? 'ok' : 'degraded', detail: calendarDetail(calState), fix: !calOn ? undefined : calConfigured ? undefined : '在 ~/.dsh/profiles/web/cordis.patch.yml 覆盖 calendar 行 config 填 username(或 npx @danceiny/gotry setup calendar --off 恢复默认不挂载)' })
964
+ // dsh-map-tools(patch 分发面宿主插件,issue #139):解析失败启动时整块静默剔除,
965
+ // doctor 把两态照亮。候选清单与 bin/gotry-inner.js 解析链、ts/capabilities/doctor.ts 同口径:
966
+ // tarball vendor 副本优先(随 files[] 分发,不进 npm 依赖——历史上游 peerDependencies
967
+ // 要求 dsh-settings/dsh-tools >=0.1.2-rc.1,与锁定的 0.1.2-alpha.3 家族在 npm 严格 peer
968
+ // 解析下 ERESOLVE;当前锁定 0.2.0-rc.2 家族,vendored 0.7.3 保留上游开区间 peers(>=0.1.2-rc.1)直接通过 0.2.0 起的运行时 peer 兼容闸,
969
+ // 保持 vendored 形态复用适配补丁),其余覆盖 source/提升/profile 布局。
970
+ const rootRequire = createRequire(join(repoRoot, 'package.json'))
971
+ const mapCandidates = [
972
+ join(repoRoot, 'ts/dsh-runtime/vendor/dsh-map-tools/lib/index.js'),
973
+ join(repoRoot, 'ts/dsh-runtime/node_modules/dsh-map-tools/lib/index.js'),
974
+ join(repoRoot, 'node_modules/dsh-map-tools/package.json'),
975
+ join(repoRoot, 'ts/node_modules/dsh-map-tools/package.json'),
976
+ join(homedir(), '.dsh/profiles/web/node_modules/dsh-map-tools/package.json'),
977
+ ]
978
+ let mapHit = mapCandidates.find((p) => existsSync(p))
979
+ // npm 提升布局:gotry 的依赖不在包目录 node_modules 里,而在同级提升位——
980
+ // 与 inner 的 require_.resolve 同机制(createRequire 从包位置向上走查)
981
+ if (!mapHit) { try { mapHit = rootRequire.resolve('dsh-map-tools') } catch { mapHit = undefined } }
982
+ items.push(mapHit
983
+ ? { label: 'dsh-map-tools(地图/路线/POI)', ok: true, level: 'ok', detail: `已就位(${mapHit})——地图工具可用(零 key,走 OSM/OSRM)`, fix: undefined }
984
+ : { label: 'dsh-map-tools(地图/路线/POI)', ok: false, level: 'missing', detail: '未随包解析——启动时该 patch 条目被静默剔除,地图/路线/POI 工具不会出现在模型工具箱(缺地图不挡旅行规划,只少能力)', fix: '重装 @danceiny/gotry(地图插件随包 vendor 分发,缺失多为安装不完整)' })
985
+ // dsh-tool-ask-user(结构化澄清卡,人格契约 (5) 的卡片形态载体)
986
+ const askCandidates = [
987
+ join(repoRoot, 'ts/dsh-runtime/vendor/deepseek-ai-dsh-tool-ask-user/package.json'),
988
+ join(repoRoot, 'node_modules/@deepseek-ai/dsh-tool-ask-user/package.json'),
989
+ join(homedir(), '.dsh/profiles/web/node_modules/@deepseek-ai/dsh-tool-ask-user/package.json'),
990
+ ]
991
+ let askHit = askCandidates.find((p) => existsSync(p))
992
+ // 与 inner 同口径:优先从 dsh 包上下文解析(pnpm 嵌套布局只有 dsh 看得见),再从包根上下文
993
+ // (npm/npx 提升布局),最后静态候选(source vendored / profile)
994
+ if (!askHit) {
995
+ try {
996
+ const reqFromDsh = createRequire(rootRequire.resolve('@deepseek-ai/dsh/lib/bin.js'))
997
+ askHit = reqFromDsh.resolve('@deepseek-ai/dsh-tool-ask-user')
998
+ } catch {
999
+ try { askHit = rootRequire.resolve('@deepseek-ai/dsh-tool-ask-user') } catch { askHit = undefined }
1000
+ }
1001
+ }
1002
+ items.push(askHit
1003
+ ? { label: 'dsh-tool-ask-user(结构化澄清卡)', ok: true, level: 'ok', detail: `已就位(${askHit})——ask_user_question 澄清卡可用(web 原生卡片;headless+TTY 用 stdio 提供方)`, fix: undefined }
1004
+ : { label: 'dsh-tool-ask-user(结构化澄清卡)', ok: false, level: 'missing', detail: '未解析——启动时澄清卡注入被静默剔除,模型只能散文追问(人格契约 (5) 退化文本形态)', fix: '重装 @danceiny/gotry——该依赖随 dsh 闭包自带,缺失多为安装不完整' })
1005
+ // LLM key:显式让渡(founder 2026-09-02:doctor 不管 key)
1006
+ items.push({ label: 'LLM key', ok: true, level: 'ok', detail: '由 dsh 宿主 UI 管理——不在体检范围(gotry 不接触、不回显凭证)', fix: undefined })
1007
+ return items
1008
+ }
1009
+
1010
+ const doctorIcon = { ok: '✅', missing: '❌', degraded: '⚠️' }
1011
+
1012
+ /** 修复指引入表:命令类才加反引号( prose 类如「到控制台申请 key」原样) */
1013
+ const fixCell = (fix) => (!fix ? '—' : /^(npx|hbcli|curl|pip|python|\$)/.test(fix) ? `\`${fix}\`` : fix)
1014
+
1015
+ /** 启动一次性摘要行(issue #114,design §3.1③):全 ok 返回 null(静默零输出);
1016
+ * 有待处理项给一行人话 + 指路(对话里 gotry_doctor 看详情 / 终端 npx @danceiny/gotry doctor)。
1017
+ * 纯函数,bootstrap-tests 直接断言。 */
1018
+ function startupDoctorLine(items) {
1019
+ const broken = (items ?? []).filter((i) => i.level && i.level !== 'ok')
1020
+ if (broken.length === 0) return null
1021
+ const human = (lv) => (lv === 'missing' ? '缺' : '半可用')
1022
+ return `[gotry] doctor: ${broken.length} 项待处理(${broken.map((i) => `${i.label}=${human(i.level)}`).join('、')})——对话里让助手调 gotry_doctor 看详情与指引,或终端跑 npx @danceiny/gotry doctor`
1023
+ }
1024
+
1025
+ /** 体检报告 markdown(与 ts/capabilities/doctor.ts renderDoctorReportMd 同形) */
1026
+ function renderDoctorReportMd(items) {
1027
+ const broken = items.filter((i) => i.level !== 'ok' && i.label !== 'LLM key')
1028
+ const lines = [
1029
+ '# GoTry 依赖体检报告(doctor)',
1030
+ '',
1031
+ `> 生成于 ${new Date().toISOString()};重新生成:终端 \`npx @danceiny/gotry doctor\`,或在对话里让助手调 gotry_doctor。`,
1032
+ '',
1033
+ '| 状态 | 依赖 | 现状 | 修复指引 |',
1034
+ '|---|---|---|---|',
1035
+ ...items.map((i) => `| ${doctorIcon[i.level]} | ${i.label} | ${i.detail} | ${fixCell(i.fix)} |`),
1036
+ '',
1037
+ `**结论**:${broken.length === 0 ? '全部就绪(可选依赖齐,LLM key 归 dsh 宿主管)。' : `${broken.length} 项待处理:${broken.map((i) => i.label).join('、')}。补装: npx @danceiny/gotry doctor --fix`}`,
1038
+ '',
1039
+ '---',
1040
+ '',
1041
+ '- `npx @danceiny/gotry doctor` 随时可重跑(只读,不改任何东西);',
1042
+ '- `npx @danceiny/gotry doctor --fix` 按上表补装(hbcli 官方脚本 / agent-reach pip / dsh-better-sidebar 插件);',
1043
+ '- LLM key 由 dsh 宿主 UI 管理,gotry 永不体检、不回显。',
1044
+ '',
1045
+ ]
1046
+ return lines.join('\n')
1047
+ }
1048
+
1049
+ /** doctor 主流程:体检 → 打印 →(可选)fix → 报告落盘。
1050
+ * exit 0=就绪(或仅剩有自动回退的降级项);exit 1=仍有缺失类问题。 */
1051
+ async function runDoctor() {
1052
+ let items = await doctorChecks()
1053
+ // 启动一次性摘要(issue #114):inner 分离子进程带 --summary 调用——只读体检,
1054
+ // 有待处理项打一行 stderr,零写盘零 header;全 ok 静默。与完整体检面(逐项/报告落盘/exit 语义)分离。
1055
+ if (process.argv.includes('--summary')) {
1056
+ const line = startupDoctorLine(items)
1057
+ if (line) console.error(line)
1058
+ return 0
1059
+ }
1060
+ say('[gotry-doctor] GoTry 可选依赖体检(只读;LLM key 归 dsh 宿主管,不在范围)')
1061
+ for (const i of items) {
1062
+ say(` ${doctorIcon[i.level]} ${i.label}:${i.detail}`)
1063
+ if (i.level !== 'ok' && i.fix) say(` ↳ 修复: ${i.fix}`)
1064
+ }
1065
+ if (DOCTOR_FIX) {
1066
+ if (!doctorFixAutoSupported()) {
1067
+ say('[gotry-doctor] --fix 在 Windows 暂不支持自动安装(hbcli/agent-reach/sidebar 上游无 win 安装面);请按各项修复指引手动处理')
1068
+ } else {
1069
+ say('[gotry-doctor] 开始补装缺失项(--fix)…')
1070
+ const results = []
1071
+ if (process.env.GOTRY_SETUP_HBCLI !== '0') results.push(await setupHbcli())
1072
+ if (process.env.GOTRY_SETUP_REACH !== '0') results.push(await setupReach())
1073
+ if (process.env.GOTRY_SETUP_SIDEBAR !== '0') results.push(await setupSidebar())
1074
+ const failedFix = results.filter((r) => !r.ok).length
1075
+ say(failedFix === 0 ? '[gotry-doctor] 补装完成;下面是补装后复检。' : `[gotry-doctor] ${failedFix} 项补装失败——见上方安装器输出;可重跑 npx @danceiny/gotry doctor --fix`)
1076
+ items = await doctorChecks()
1077
+ for (const i of items) {
1078
+ say(` ${doctorIcon[i.level]} ${i.label}:${i.detail}`)
1079
+ }
1080
+ }
1081
+ }
1082
+ // 报告落盘(侧栏工作台预览面;写失败不挡体检结论)
1083
+ try {
1084
+ const stateDir = join(repoRoot, 'gotry-state')
1085
+ mkdirSync(stateDir, { recursive: true })
1086
+ writeFileSync(join(stateDir, 'doctor-report.md'), renderDoctorReportMd(items), 'utf8')
1087
+ say(`[gotry-doctor] 报告已写: ${join(stateDir, 'doctor-report.md')}(dsh 侧栏工作台可直接预览)`)
1088
+ } catch (e) {
1089
+ say(`[gotry-doctor] 报告写盘失败(${e.message})——体检结论仍有效`)
1090
+ }
1091
+ // 降级类(凭证未配/flyai 试用)不挡 exit 0——降级有自动回退路径,缺失类才挡
1092
+ const stillMissing = items.filter((i) => i.level === 'missing').length
1093
+ return stillMissing === 0 ? 0 : 1
1094
+ }
1095
+
1096
+ // ---------------------------------------------------------------------------
1097
+ // onboarding(issue #258):`npx @danceiny/gotry web` 启动前的显式可选能力配置 prompt——每次符合条件的启动
1098
+ // 评估一次、至多问一次,无跨启动持久确认(不写「已问过」标记,每次 web 启动独立判定)。
1099
+ // 契约:仅交互式 TTY + 有「可自动安装」缺项时问一次;y → 复用 doctor --fix 的幂等
1100
+ // 安装器(setupHbcli/setupReach/setupSidebar,不建第二套);n → 立即继续 web。
1101
+ // CI/benchmark/非 TTY/全健康/GOTRY_SETUP_SKIP=1/GOTRY_ONBOARDING_SKIP=1/--no-onboarding
1102
+ // 均零 prompt 零安装,仍启 web;永不 postinstall 或后台任务里安装。结果三态:
1103
+ // installed / needs-user-action / unavailable,各带具体原因;浏览器商店/凭证/账号/
1104
+ // key/上游授权永不冒充自动完成。部分失败不挡 web 且给重试命令;再跑不重装已健康项
1105
+ // (安装器自带存在性短路 + doctor 复检)。纯函数(classify/plan/skipReason)与
1106
+ // runOnboardingFix(注入安装器)均导出供 bootstrap-tests 隔离单测,不跑真安装器。
1107
+ // ---------------------------------------------------------------------------
1108
+
1109
+ /** 安装器 id 映射:doctor item label → 安装器 id(与 GOTRY_SETUP_* env 同名)。纯函数。 */
1110
+ function installerIdFor(label) {
1111
+ if (typeof label !== 'string') return null
1112
+ if (label.startsWith('Agent Reach')) return 'reach'
1113
+ if (label.startsWith('hbcli')) return 'hbcli'
1114
+ if (label.startsWith('dsh-better-sidebar')) return 'sidebar'
1115
+ return null
1116
+ }
1117
+
1118
+ /** 该安装器是否被 GOTRY_SETUP_*=0 显式 opt-out(与 runDoctor 同口径)。纯函数。
1119
+ * env 可注入:测试与可复用逻辑不依赖 ambient GOTRY_SETUP_*,生产 CLI 路径仍取 process.env。 */
1120
+ function installerEnabled(id, env = process.env) {
1121
+ if (id === 'hbcli') return env.GOTRY_SETUP_HBCLI !== '0'
1122
+ if (id === 'reach') return env.GOTRY_SETUP_REACH !== '0'
1123
+ if (id === 'sidebar') return env.GOTRY_SETUP_SIDEBAR !== '0'
1124
+ return true
1125
+ }
1126
+
1127
+ /** doctor --fix 自动安装的平台支持面(共享契约):win32 上 hbcli(staicli)/agent-reach(pip venv)
1128
+ * 上游无 win 安装面,dsh-better-sidebar(dsh plugin → pnpm)在 win32 同样不自动跑——runDoctor
1129
+ * 的 --fix 守卫与 onboarding 的 auto 分桶复用同一条边界,不在 win32 把这些缺项分到 auto
1130
+ * (否则 prompt 了却装不上,不诚实)。纯函数,可注入 platform 供确定性单测。 */
1131
+ function doctorFixAutoSupported(platform = process.platform) {
1132
+ return platform !== 'win32'
1133
+ }
1134
+
1135
+ /** win32 上本可 auto 的缺项降为 unavailable 时的具体原因(诚实,不冒充 auto / 不给无效的
1136
+ * `npx @danceiny/gotry doctor --fix` 指引)。纯函数。 */
1137
+ function platformAutoUnavailableReason(id) {
1138
+ if (id === 'hbcli') return 'Windows 暂不支持 hbcli 自动安装(staicli 上游无 win 安装面);请按 doctor 指引手动配置或切到 macOS/Linux'
1139
+ if (id === 'reach') return 'Windows 暂不支持 agent-reach 自动安装(pip venv 上游无 win 安装面);请按 doctor 指引手动配置或切到 macOS/Linux'
1140
+ if (id === 'sidebar') return 'Windows 暂不支持 dsh-better-sidebar 自动安装(dsh plugin → pnpm 在 win32 不自动跑);请按 doctor 指引手动配置或切到 macOS/Linux'
1141
+ return 'Windows 暂不支持自动安装;请按 doctor 指引手动配置'
1142
+ }
1143
+
1144
+ /**
1145
+ * 把单个 doctor item 分到 onboarding 桶(纯函数,与 ts/capabilities/doctor.ts 同口径)。
1146
+ * auto = 本机可自动安装(setupHbcli/setupReach/setupSidebar 且未被 env opt-out);
1147
+ * user-action = 浏览器商店/凭证/账号/key/上游授权/手动配置——必须用户本人操作;
1148
+ * unavailable = 本仓无自动安装面(重装 gotry / Node 升级 / env opt-out / 安装器不自动升级项)。
1149
+ * LLM key 永远 ok,不进任何桶。
1150
+ */
1151
+ function classifyDoctorGap(item, opts = {}) {
1152
+ const platform = opts.platform ?? process.platform
1153
+ const env = opts.env ?? process.env
1154
+ if (!item || !item.level || item.level === 'ok') return 'ok'
1155
+ const label = item.label ?? ''
1156
+ if (label === 'LLM key') return 'ok'
1157
+ const autoSupported = doctorFixAutoSupported(platform)
1158
+ if (label.startsWith('Agent Reach')) return installerEnabled('reach', env) && autoSupported ? 'auto' : 'unavailable'
1159
+ // hbcli:二进制缺失(missing)= auto(setupHbcli 装;win32 → unavailable,上游无 win 安装面);
1160
+ // 凭证未配(degraded)= user-action(平台无关);旧版本(missing,安装器不自动升级)= auto 分桶
1161
+ // 但安装会失败 → 结果兜底 unavailable,诚实不冒充。
1162
+ if (label.startsWith('hbcli')) {
1163
+ if (item.level === 'degraded') return 'user-action'
1164
+ return installerEnabled('hbcli', env) && autoSupported ? 'auto' : 'unavailable'
1165
+ }
1166
+ if (label.startsWith('dsh-better-sidebar')) return installerEnabled('sidebar', env) && autoSupported ? 'auto' : 'unavailable'
1167
+ if (label.startsWith('GoTry Session Bridge')) return 'user-action' // 浏览器商店一键装
1168
+ if (label.startsWith('FlyAI')) return 'user-action' // 上游控制台申请 key
1169
+ if (label.startsWith('dsh-calendar')) return 'user-action' // profile cordis.patch.yml 配置
1170
+ if (label.startsWith('dsh-map-tools')) return 'unavailable' // 随包 vendor,缺失即重装 gotry
1171
+ if (label.startsWith('dsh-tool-ask-user')) return 'unavailable' // 随 dsh 闭包,缺失即重装 gotry
1172
+ if (label.startsWith('Node')) return 'unavailable' // 手动升级运行时
1173
+ return 'unavailable'
1174
+ }
1175
+
1176
+ /**
1177
+ * 由 doctor items 构建 onboarding 计划(纯函数)。
1178
+ * promptable = 存在可自动安装缺项(决定是否问那一次)。
1179
+ */
1180
+ function buildOnboardingPlan(items, opts = {}) {
1181
+ const platform = opts.platform ?? process.platform
1182
+ const env = opts.env ?? process.env
1183
+ const auto = []
1184
+ const userAction = []
1185
+ const unavailable = []
1186
+ for (const item of items ?? []) {
1187
+ const bucket = classifyDoctorGap(item, { platform, env })
1188
+ if (bucket === 'ok') continue
1189
+ const entry = { label: item.label, level: item.level, detail: item.detail, fix: item.fix }
1190
+ if (bucket === 'auto') auto.push(entry)
1191
+ else if (bucket === 'user-action') userAction.push(entry)
1192
+ else {
1193
+ // win32 平台把本可 auto 的缺项降为 unavailable 时,给具体平台原因(诚实,不冒充 auto,
1194
+ // 也不给 win32 上无效的 `npx @danceiny/gotry doctor --fix` 指引);env opt-out / 随包缺失等保持原 detail。
1195
+ const id = installerIdFor(item.label)
1196
+ if (id && !doctorFixAutoSupported(platform) && installerEnabled(id, env)) {
1197
+ entry.detail = platformAutoUnavailableReason(id)
1198
+ }
1199
+ unavailable.push(entry)
1200
+ }
1201
+ }
1202
+ return { promptable: auto.length > 0, auto, userAction, unavailable }
1203
+ }
1204
+
1205
+ /**
1206
+ * onboarding 跳过原因(纯函数;inner 与 bootstrap 共用,确保两边判定一致且可单测)。
1207
+ * 返回原因字符串则跳过(零 prompt 零安装),null 则可继续。mode/benchmark 仅 inner
1208
+ * 提供;bootstrap 直接调用时 mode 缺省(不检 non-web),只检 env/argv/isTTY。
1209
+ */
1210
+ function onboardingSkipReason({ mode, benchmark, env = process.env, argv = process.argv, isTTY = process.stdin?.isTTY } = {}) {
1211
+ if (mode !== undefined && mode !== 'web') return 'non-web-mode'
1212
+ if (benchmark) return 'benchmark'
1213
+ if (env.GOTRY_SETUP_SKIP === '1') return 'GOTRY_SETUP_SKIP=1'
1214
+ if (env.GOTRY_ONBOARDING_SKIP === '1') return 'GOTRY_ONBOARDING_SKIP=1'
1215
+ if (env.CI) return 'CI'
1216
+ if (Array.isArray(argv) && argv.includes('--no-onboarding')) return '--no-onboarding'
1217
+ if (!isTTY) return 'non-tty'
1218
+ return null
1219
+ }
1220
+
1221
+ /** 读取 stdin 一行(对 TTY 与注入流均工作;finish 后 pause,不干扰后续 dsh web 的 stdin)。 */
1222
+ function readOneLine(input) {
1223
+ return new Promise((resolve) => {
1224
+ let buf = ''
1225
+ let done = false
1226
+ const finish = (line) => {
1227
+ if (done) return
1228
+ done = true
1229
+ input.removeListener('data', onData)
1230
+ input.removeListener('end', onEnd)
1231
+ try { input.pause?.() } catch { /* ignore */ }
1232
+ resolve(line ?? '')
1233
+ }
1234
+ const onData = (chunk) => {
1235
+ buf += Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk)
1236
+ const nl = buf.indexOf('\n')
1237
+ if (nl >= 0) finish(buf.slice(0, nl).replace(/\r$/, ''))
1238
+ }
1239
+ const onEnd = () => finish(buf)
1240
+ input.on('data', onData)
1241
+ input.on('end', onEnd)
1242
+ try { input.resume?.() } catch { /* ignore */ }
1243
+ })
1244
+ }
1245
+
1246
+ /**
1247
+ * 交互 prompt:打印缺项概览 + 一次询问,读一行返回 'yes' | 'no'。
1248
+ * input/output 可注入(测试);默认 process.stdin/process.stdout。
1249
+ */
1250
+ async function promptOnboarding(plan, opts = {}) {
1251
+ const input = opts.input ?? process.stdin
1252
+ const output = opts.output ?? process.stdout
1253
+ output.write('\n[gotry] 启动前检测到可选能力缺项,其中可自动配置:\n')
1254
+ for (const g of plan.auto) output.write(` • ${g.label} —— ${g.detail}\n`)
1255
+ if (plan.userAction.length || plan.unavailable.length) {
1256
+ output.write('需你本人操作(浏览器商店 / 凭证 / key / 重装),gotry 不自动处理:\n')
1257
+ for (const g of plan.userAction) output.write(` • ${g.label} —— ${g.detail}\n`)
1258
+ for (const g of plan.unavailable) output.write(` • ${g.label} —— ${g.detail}\n`)
1259
+ }
1260
+ output.write('\n现在自动配置可安装项吗?(y/N,随后继续启动 web) ')
1261
+ const answer = await readOneLine(input)
1262
+ return /^\s*y(es)?\s*$/i.test(answer) ? 'yes' : 'no'
1263
+ }
1264
+
1265
+ /**
1266
+ * 渲染分类计划(无可自动安装项时):把 needs-user-action / unavailable 逐项带具体原因打印,
1267
+ * 不 prompt 不安装。用于 win32 等无 auto 安装面、或仅凭证/key/重装缺口的交互式启动——
1268
+ * 让用户看见分类与原因,而非只剩后台一行摘要。output 可注入(测试)。
1269
+ */
1270
+ function renderClassifiedPlan(plan, output) {
1271
+ output.write('\n[gotry] 启动前检测到可选能力缺项(无可自动安装项,以下需你本人操作或本机暂不支持):\n')
1272
+ if (plan.userAction.length) {
1273
+ output.write('需你本人操作(浏览器商店 / 凭证 / key / 配置),gotry 不自动处理:\n')
1274
+ for (const g of plan.userAction) output.write(` • ${g.label} —— ${g.detail}\n`)
1275
+ }
1276
+ if (plan.unavailable.length) {
1277
+ output.write('本机暂不支持自动安装(平台限制 / 重装 gotry / Node 升级):\n')
1278
+ for (const g of plan.unavailable) output.write(` • ${g.label} —— ${g.detail}\n`)
1279
+ }
1280
+ output.write('详情与指引:对话里让助手调 gotry_doctor,或终端跑 npx @danceiny/gotry doctor\n')
1281
+ }
1282
+
1283
+ /**
1284
+ * 执行安装(复用 setupHbcli/setupReach/setupSidebar,不建第二套)。
1285
+ * installers/recheck 可注入(测试用 fakes,永不跑真安装器)。返回逐项结果:
1286
+ * installed / needs-user-action / unavailable(失败带重试命令)。部分失败不抛、不挡 web。
1287
+ */
1288
+ const ONBOARDING_DEFAULT_INSTALLERS = { hbcli: setupHbcli, reach: setupReach, sidebar: setupSidebar }
1289
+
1290
+ async function runOnboardingFix(plan, opts = {}) {
1291
+ const installers = { ...ONBOARDING_DEFAULT_INSTALLERS, ...(opts.installers ?? {}) }
1292
+ const recheck = opts.recheck ?? doctorChecks
1293
+ const attempts = {}
1294
+ for (const gap of plan.auto) {
1295
+ const id = installerIdFor(gap.label)
1296
+ const installer = id ? installers[id] : null
1297
+ if (!installer) { attempts[gap.label] = { ok: false, error: 'no installer mapped for this gap' }; continue }
1298
+ try {
1299
+ const r = await installer()
1300
+ attempts[gap.label] = { ok: r?.ok === true, error: r?.error }
1301
+ } catch (e) {
1302
+ attempts[gap.label] = { ok: false, error: e?.message ?? String(e) }
1303
+ }
1304
+ }
1305
+ // 复检决定 auto 项最终状态(安装器自带存在性短路 → 已健康项不重装;幂等)
1306
+ const rechecked = await recheck()
1307
+ const byLabel = new Map(rechecked.map((i) => [i.label, i]))
1308
+ const results = []
1309
+ for (const gap of plan.auto) {
1310
+ const item = byLabel.get(gap.label)
1311
+ if (item && item.level === 'ok') {
1312
+ results.push({ label: gap.label, status: 'installed', reason: item.detail ?? '已就位' })
1313
+ } else {
1314
+ const a = attempts[gap.label]
1315
+ results.push({ label: gap.label, status: 'unavailable', reason: a?.error ?? item?.detail ?? 'install failed', retry: 'npx @danceiny/gotry doctor --fix' })
1316
+ }
1317
+ }
1318
+ for (const gap of plan.userAction) {
1319
+ results.push({ label: gap.label, status: 'needs-user-action', reason: gap.fix ?? gap.detail ?? '需用户本人操作' })
1320
+ }
1321
+ for (const gap of plan.unavailable) {
1322
+ results.push({ label: gap.label, status: 'unavailable', reason: gap.detail ?? '本仓无自动安装面' })
1323
+ }
1324
+ return results
1325
+ }
1326
+
1327
+ /**
1328
+ * `onboarding` 子命令(inner 以 `onboarding --result-file=…` 同步 inherit 调用;
1329
+ * 亦可直接 `node gotry-bootstrap.js onboarding --scan` 只读查看计划)。
1330
+ * 恒 exit 0(永不挡 web);result JSON 写到 --result-file 告知 inner 是否 prompt 过。
1331
+ *
1332
+ * opts(注入位,供 bootstrap-tests 在进程内跨 inner→onboarding→web 边界跑真实 onboarding 逻辑,
1333
+ * 永不跑真安装器/开浏览器):input/output(注入流替 stdin/stdout)、isTTY/env/argv(覆盖跳过判定)、
1334
+ * scan(注入 doctor items 替真机探测)、installers/recheck(注入假安装器/复检)、platform(注入平台)。
1335
+ * 返回 result 对象({ prompted, answered?, results?, skipped?, plan? });CLI 仍恒 exit 0。
1336
+ */
1337
+ async function runOnboarding(args, opts = {}) {
1338
+ const resultFile = (args.find((a) => a.startsWith('--result-file=')) ?? '').slice('--result-file='.length) || null
1339
+ const scanOnly = args.includes('--scan')
1340
+ const input = opts.input ?? process.stdin
1341
+ const output = opts.output ?? process.stdout
1342
+ const isTTY = opts.isTTY ?? process.stdin?.isTTY
1343
+ const env = opts.env ?? process.env
1344
+ const argv = opts.argv ?? process.argv
1345
+ const scan = opts.scan ?? doctorChecks
1346
+ const platform = opts.platform ?? process.platform
1347
+ // result 通道硬化:mode 0600 + flag wx——存在路径/符号链接永不覆盖(inner 在 0700
1348
+ // mkdtemp 私有目录下传 result.json,这里 wx 防该确切路径被预置/符号链接互换攻击)。
1349
+ const writeResult = (r) => { if (resultFile) { try { writeFileSync(resultFile, JSON.stringify(r), { mode: 0o600, flag: 'wx' }) } catch { /* 坏 result 不挡 web */ } } }
1350
+
1351
+ if (scanOnly) {
1352
+ const plan = buildOnboardingPlan(await scan(), { platform, env })
1353
+ say(JSON.stringify(plan))
1354
+ return { prompted: false, scan: true, plan }
1355
+ }
1356
+
1357
+ const skipReason = onboardingSkipReason({ env, argv, isTTY })
1358
+ if (skipReason) {
1359
+ const result = { prompted: false, skipped: skipReason }
1360
+ writeResult(result)
1361
+ return result
1362
+ }
1363
+
1364
+ const plan = buildOnboardingPlan(await scan(), { platform, env })
1365
+ if (!plan.promptable) {
1366
+ // 无可自动安装缺项。两类:
1367
+ // (1) 有需用户操作/不可用项(如 win32 平台不支持自动安装,或仅凭证/key/重装缺口):
1368
+ // 渲染分类计划与具体原因,不 prompt 不安装;标记 reported 让 inner 抑制重复的
1369
+ // detached doctor 摘要(design §3.1③,#114)——用户已看见分类,不再叠一行摘要。
1370
+ // (2) 全健康:静默(后台摘要亦静默),不渲染。
1371
+ const reportable = plan.userAction.length + plan.unavailable.length
1372
+ if (reportable > 0) {
1373
+ renderClassifiedPlan(plan, output)
1374
+ const result = { prompted: false, reported: true, plan }
1375
+ writeResult(result)
1376
+ return result
1377
+ }
1378
+ const result = { prompted: false, plan }
1379
+ writeResult(result)
1380
+ return result
1381
+ }
1382
+
1383
+ const answer = await promptOnboarding(plan, { input, output })
1384
+ if (answer === 'yes') {
1385
+ say('[gotry] 开始自动配置可安装项(复用 doctor --fix 幂等安装器)…')
1386
+ const results = await runOnboardingFix(plan, { installers: opts.installers, recheck: opts.recheck })
1387
+ say('')
1388
+ say('[gotry] 配置结果:')
1389
+ for (const r of results) {
1390
+ const tag = r.status === 'installed' ? '✅ installed' : r.status === 'needs-user-action' ? '↪ needs-user-action' : '✗ unavailable'
1391
+ say(` ${tag} ${r.label} —— ${r.reason}${r.retry ? `(重试: ${r.retry})` : ''}`)
1392
+ }
1393
+ const result = { prompted: true, answered: 'yes', results }
1394
+ writeResult(result)
1395
+ return result
1396
+ }
1397
+ say(' 跳过——随时可跑: npx @danceiny/gotry doctor --fix(随后继续启动 web)')
1398
+ const result = { prompted: true, answered: 'no' }
1399
+ writeResult(result)
1400
+ return result
1401
+ }
1402
+
1403
+ /**
1404
+ * web 模式的 onboarding + 启动编排(issue #258 E2E 边界缝):
1405
+ * 跳过判定 →(可选)onboarding →(可选)后台 doctor 摘要 → dsh web 启动。抽成显式注入依赖的
1406
+ * 函数,使 bootstrap-tests 能在不 spawn 真 dsh / 不跑真安装器的前提下,断言 inner 的真实编排跨过
1407
+ * inner→onboarding→web 启动边界(yes/no/非 TTY/部分失败/summary 抑制均到达 web 启动标记)。
1408
+ * 生产侧 inner 传入真实依赖(spawnSync 跑 bootstrap onboarding 子进程、spawn detached 跑 doctor
1409
+ * --summary、spawn 跑 dsh web);launchWeb 即 web 启动边界标记。
1410
+ * 摘要抑制契约:onboarding 已 prompt(用户交互过)或已 reported(渲染过分类计划,如 win32 无 auto
1411
+ * 安装面)时,抑制重复的 detached doctor 摘要;跳过态(非 TTY/CI/opt-out)与全健康态仍走摘要
1412
+ * (全健康时摘要本身静默)。返回 onboardingPrompted 供 inner 复用(本函数内已处理摘要抑制,返回值仅信息性)。
1413
+ */
1414
+ async function orchestrateWebLaunch({ onboardingSkip, runOnboarding, runSummary, launchWeb }) {
1415
+ let onboardingPrompted = false
1416
+ let onboardingResult = null
1417
+ if (!onboardingSkip) {
1418
+ try { onboardingResult = await runOnboarding() } catch { /* onboarding 不可用不挡 web */ }
1419
+ onboardingPrompted = onboardingResult?.prompted === true
1420
+ }
1421
+ // onboarding 已 prompt 或已 reported(渲染过分类计划)时抑制重复后台摘要(design §3.1③,#114)
1422
+ if (!onboardingPrompted && !onboardingResult?.reported) {
1423
+ try { runSummary() } catch { /* 体检不可用不挡启动 */ }
1424
+ }
1425
+ await launchWeb()
1426
+ return { launched: true, onboardingPrompted, onboardingResult }
1427
+ }
1428
+
1429
+ const EXTENSION_FILES = ['manifest.json', 'background.js', 'content-main.js', 'content-bridge.js', 'README.md']
1430
+
1431
+ function readManifestVersion(manifestPath) {
1432
+ try { return JSON.parse(readFileSync(manifestPath, 'utf8')).version ?? null } catch { return null }
1433
+ }
1434
+
1435
+ function copyExtensionDir(srcDir, dstDir) {
1436
+ mkdirSync(dstDir, { recursive: true })
1437
+ for (const f of EXTENSION_FILES) {
1438
+ const s = join(srcDir, f)
1439
+ if (!existsSync(s)) continue
1440
+ copyFileSync(s, join(dstDir, f))
1441
+ }
1442
+ }
1443
+
1444
+ /** 会话检索扩展(issue #21 方案 C):包内 extension/ → ~/.gotry/extension 幂等落位 + 一次性加载指引 */
1445
+ async function setupExtension() {
1446
+ say('[gotry-setup] GoTry Session Bridge 扩展(会话检索数据面 issue #21;一次性安装,替代逐连接弹窗)')
1447
+ const srcDir = join(repoRoot, 'extension')
1448
+ const srcManifest = join(srcDir, 'manifest.json')
1449
+ if (!existsSync(srcManifest)) {
1450
+ say(` ✗ 包内未找到扩展文件(${srcDir})——不影响 gotry:会话检索降级,重装 npx @danceiny/gotry 可恢复`)
1451
+ return { ok: false }
1452
+ }
1453
+ const srcVersion = readManifestVersion(srcManifest)
1454
+ const dstDir = join(homedir(), '.gotry', 'extension')
1455
+ const dstVersion = readManifestVersion(join(dstDir, 'manifest.json'))
1456
+ if (dstVersion != null && dstVersion === srcVersion) {
1457
+ say(` ✓ 已就位(~/.gotry/extension,v${dstVersion};若浏览器里尚未加载:chrome://extensions → 开发者模式 → 加载已解压的扩展程序 → 选 ~/.gotry/extension)`)
1458
+ return { ok: true }
1459
+ }
1460
+ if (CHECK_ONLY) { say(` ✗ ${dstVersion == null ? `未安装(落位 ${dstDir})` : `待更新 v${dstVersion} → v${srcVersion},落位 ${dstDir}`}(--check-only 只报告)`); return { ok: true } }
1461
+ try {
1462
+ copyExtensionDir(srcDir, dstDir)
1463
+ } catch (e) {
1464
+ say(` ✗ 落位失败(${e.message})——不影响 gotry:会话检索降级;可手动拷贝 ${srcDir} → ${dstDir}`)
1465
+ return { ok: false }
1466
+ }
1467
+ say(` ✓ 已落位 ${dstDir}(v${srcVersion};manifest 带固定 key,unpacked 扩展 ID 恒为 olpgkofjhhiiiahdkkbcninhjmegghfe)`)
1468
+ say(' 推荐(免下面三步):Chrome 应用商店一键安装 GoTry Session Bridge(自动更新): https://chromewebstore.google.com/detail/gotry-session-bridge/oeajpiccmonococjcegddlooeeohlbgd')
1469
+ say(' 本地加载(每台浏览器一次,约 30 秒):Chrome 打开 chrome://extensions → 右上角开启「开发者模式」→「加载已解压的扩展程序」→ 选择 ~/.gotry/extension')
1470
+ say(' 获取/更新本地通道扩展可走 GitHub Releases:npx @danceiny/gotry setup --extension-from=github(自动下载校验落位;手动下载: github.com/Danceiny/gotry/releases 标签 ext-*)')
1471
+ say(' 装好即生效,零系统弹窗;扩展卡片开关=总闸(与 gotry 授权闸 sessionAccess 双重控制)')
1472
+ return { ok: true }
1473
+ }
1474
+
1475
+ /**
1476
+ * GitHub Releases 下载通道(ADR-21 分发 A,--extension-from=github 显式 opt-in):
1477
+ * repo 态 spawn tsx CLI(与 wizard/health-watch 同款单行 JSON 回传),npm 态 import dist JS;
1478
+ * 任何失败显式降级 bundled 包内副本——安装外部产物永远不挡 gotry 本体。
1479
+ */
1480
+ async function setupExtensionFromGithub() {
1481
+ say('[gotry-setup] GoTry Session Bridge 扩展 · GitHub Releases 下载通道(dist-manifest → tar.gz → SHA256 → key 钉扎)')
1482
+ const destDir = join(homedir(), '.gotry', 'extension')
1483
+ const cliScript = join(repoRoot, 'ts', 'scripts', 'extension-distribution-cli.ts')
1484
+ const releaseBase = process.env.GOTRY_EXTENSION_RELEASE_BASE // 镜像/测试基址覆盖;缺省 GitHub 官方
1485
+ let r = null
1486
+ if (existsSync(cliScript)) {
1487
+ r = await new Promise((resolve) => {
1488
+ const child = spawn('npx', ['--yes', 'tsx', cliScript, '--dest', destDir, '--source-dir', join(repoRoot, 'extension'), ...(releaseBase ? ['--release-base', releaseBase] : []), ...(CHECK_ONLY ? ['--check-only'] : [])], {
1489
+ cwd: join(repoRoot, 'ts'),
1490
+ stdio: ['ignore', 'pipe', 'inherit'],
1491
+ })
1492
+ let buf = ''
1493
+ child.stdout.on('data', (c) => { buf += c.toString('utf8') })
1494
+ child.on('close', () => {
1495
+ const line = (buf.trim().split('\n').pop() ?? '')
1496
+ if (line.startsWith('{')) { try { resolve(JSON.parse(line)); return } catch { /* 落到下一行 */ } }
1497
+ resolve({ ok: false, action: 'fallback-bundled', error: `CLI 无 JSON 输出(末行:${line.slice(0, 120)})` })
1498
+ })
1499
+ child.on('error', (e) => resolve({ ok: false, action: 'fallback-bundled', error: `CLI 启动失败 ${e.message}` }))
1500
+ })
1501
+ } else {
1502
+ try {
1503
+ const mod = await import('../dist/capabilities/session/extension-distribution.js')
1504
+ r = await mod.installExtensionFromGithub({ destDir, pinnedSourceDir: join(repoRoot, 'extension'), ...(releaseBase ? { releaseBase } : {}), checkOnly: CHECK_ONLY })
1505
+ } catch (e) {
1506
+ r = { ok: false, action: 'fallback-bundled', error: `dist 模块不可用:${e.message}` }
1507
+ }
1508
+ }
1509
+ if (r && r.ok) {
1510
+ if (r.action === 'installed') {
1511
+ say(` ✓ 已从 GitHub Releases 落位 ${destDir}(v${r.version}${r.previousVersion ? `,旧 v${r.previousVersion}` : ''})`)
1512
+ say(' 已装过旧版的浏览器:chrome://extensions → GoTry Session Bridge 卡片 → 「重新加载」一次即生效(新装跳过)。')
1513
+ return { ok: true }
1514
+ }
1515
+ // up-to-date(check-only 下远端更新也会走到这里,只报告不落盘)
1516
+ if (r.version != null && r.previousVersion != null && r.version !== r.previousVersion) {
1517
+ say(` ✓ 远端有新版 v${r.version}(本地 v${r.previousVersion})${CHECK_ONLY ? '(--check-only 只报告)' : ''}`)
1518
+ return { ok: true }
1519
+ }
1520
+ say(` ✓ 已是最新(v${r.version ?? '?'});若浏览器里尚未加载:chrome://extensions → 开发者模式 → 加载已解压 → 选 ${destDir}`)
1521
+ return { ok: true }
1522
+ }
1523
+ say(` ✗ GitHub 通道失败(${r && r.error ? r.error : '未知'})——显式降级包内副本:`)
1524
+ return setupExtension()
1525
+ }
1526
+
1527
+ /**
1528
+ * 调 (npm 安装态降级)只确保扩展落位后跑扩展分支 hydration。wizard.ts 现在
1529
+ * 只做 ensure + watch precheck(Node 端职责),不 spawn 任何 GUI;真正的浏览器
1530
+ * 商店页安装走 dsh UI 渲染的 verdict.installUrl,健康探活走 runHealthWatch。
1531
+ *
1532
+ * 历史:wizard-bootstrap.ts(此文件之前 spawn 的 tsx 子进程)已删除;
1533
+ * run-all §40 onboarding-tests 直接 import wizard.ts 的 runOnboardingWizard。
1534
+ */
1535
+ async function runWizardBootstrap() {
1536
+ // wizard.ts 已退化为纯 Node 2 步(ensure + watch precheck),不 spawn 任何东西;
1537
+ // tsx 子进程路径 = <repoRoot>/ts;npm 安装态(wizard-bootstrap.ts 已删除)走包内 setupExtension。
1538
+ const wizardScript = join(repoRoot, 'ts', 'scripts', 'wizard-bootstrap.ts')
1539
+ if (!existsSync(wizardScript)) {
1540
+ say('[gotry-wizard] 内置 wizard 脚本缺失(已装 npm 包态;npm 安装态应在浏览器商店一键装,不需要 wizard 跑落位)——只跑包内扩展落位:')
1541
+ await setupExtension()
1542
+ return { ok: true }
1543
+ }
1544
+ return new Promise((resolve) => {
1545
+ const child = spawn('npx', ['--yes', 'tsx', wizardScript, '--extension-dir', join(homedir(), '.gotry', 'extension'), '--source-dir', join(repoRoot, 'extension')], {
1546
+ cwd: join(repoRoot, 'ts'),
1547
+ stdio: ['ignore', 'pipe', 'inherit'],
1548
+ })
1549
+ let stdoutBuf = ''
1550
+ let resolved = false
1551
+ const finish = (r) => {
1552
+ if (resolved) return
1553
+ resolved = true
1554
+ resolve(r)
1555
+ }
1556
+ child.stdout.on('data', (c) => {
1557
+ stdoutBuf += c.toString('utf8')
1558
+ let nl = stdoutBuf.indexOf('\n')
1559
+ while (nl >= 0) {
1560
+ const line = stdoutBuf.slice(0, nl).trim()
1561
+ stdoutBuf = stdoutBuf.slice(nl + 1)
1562
+ if (line.startsWith('{')) {
1563
+ try {
1564
+ const o = JSON.parse(line)
1565
+ // 打印 2 步 status(wizard 自己的 print 已在子进程跑了;此处父进程再 human-friendly 一次)
1566
+ if (Array.isArray(o.steps)) {
1567
+ for (const s of o.steps) {
1568
+ const tag = s.status === 'ok' ? '✅' : s.status === 'skip' ? '⏭ ' : '❌'
1569
+ say(`[gotry-wizard] ${tag} ${s.step}: ${s.summary}`)
1570
+ }
1571
+ }
1572
+ say(`[gotry-wizard] extensionDir=${o.extensionDir}; ok=${o.ok}`)
1573
+ finish({ ok: o.ok === true })
1574
+ } catch {
1575
+ say('[gotry-wizard] ✗ wizard JSON 解析失败')
1576
+ finish({ ok: false })
1577
+ }
1578
+ try { child.kill('SIGTERM') } catch { /* ignore */ }
1579
+ return
1580
+ }
1581
+ nl = stdoutBuf.indexOf('\n')
1582
+ }
1583
+ })
1584
+ child.on('exit', (code) => {
1585
+ if (!resolved) {
1586
+ say(`[gotry-wizard] ✗ wizard 子进程 exit ${code}(stdout 无 JSON)`)
1587
+ finish({ ok: code === 0 })
1588
+ }
1589
+ })
1590
+ child.on('error', (e) => {
1591
+ say(`[gotry-wizard] ✗ wizard 子进程启动失败: ${e.message}`)
1592
+ finish({ ok: false })
1593
+ })
1594
+ })
1595
+ }
1596
+
1597
+ /**
1598
+ * 后台探活扩展心跳(wizard 闭环「装完零重跑」,§3.3):
1599
+ * spawn tsx 跑 health-watch.ts(纯 TS 探活,run-all §40 同一份代码),接 stdout JSON 结果行。
1600
+ * 探活期间每 5s stdout 一行 `.`,用户看见 stdout 持续推进,不卡死假象。
1601
+ * 默认 120s 超时;若用户提前 Ctrl+C,timeoutMs 走 0 立即返回 cancelled。
1602
+ */
1603
+ async function runHealthWatch() {
1604
+ // 跨语言:bootstrap 是纯 JS,health-watch 是 TS;走 spawn npx tsx 子进程,stdout 解析。
1605
+ // tsx 子进程路径 = <repoRoot>/ts;不在 npm 包里跑(发布面只走 src/bin,不走 wizard 自动化)
1606
+ const watcherScript = join(repoRoot, 'ts', 'scripts', 'health-watch-cli.ts')
1607
+ // 默认 120s / 5s,§3.3 设计值;env 可覆盖(bootstrap-tests 跑真实路径用)
1608
+ const timeoutMs = parseInt(process.env.GOTRY_ONBOARDING_TIMEOUT_MS ?? '120000', 10) || 120_000
1609
+ const probeIntervalMs = parseInt(process.env.GOTRY_ONBOARDING_INTERVAL_MS ?? '5000', 10) || 5_000
1610
+ return new Promise((resolve) => {
1611
+ const args = [watcherScript, '--timeout', String(timeoutMs), '--interval', String(probeIntervalMs), '--json']
1612
+ // 先确认 watcher 脚本存在(npm 安装态可能未带 ts/目录——降级:走内联 node:http 短探活,避免挡用户)
1613
+ if (!existsSync(watcherScript)) {
1614
+ say(`[gotry-wizard] 内置探活脚本缺失(${watcherScript})——改用内置 5s 轮询`)
1615
+ resolve(runInlineHealthWatch(timeoutMs))
1616
+ return
1617
+ }
1618
+ const child = spawn('npx', ['--yes', 'tsx', ...args], {
1619
+ cwd: join(repoRoot, 'ts'),
1620
+ stdio: ['ignore', 'pipe', 'inherit'],
1621
+ env: { ...process.env, GOTRY_ONBOARDING_HEADLESS: process.env.GOTRY_ONBOARDING_HEADLESS ?? '1' },
1622
+ })
1623
+ let stdoutBuf = ''
1624
+ let heartbeatTicker = 0
1625
+ const hbInterval = setInterval(() => {
1626
+ heartbeatTicker += 1
1627
+ process.stdout.write('.')
1628
+ if (heartbeatTicker % 12 === 0) process.stdout.write(`(${Math.round(heartbeatTicker * probeIntervalMs / 1000)}s)\n`)
1629
+ }, probeIntervalMs)
1630
+ child.stdout.on('data', (c) => {
1631
+ const s = c.toString('utf8')
1632
+ stdoutBuf += s
1633
+ // JSON 结果行以 \n 分隔;watcher 一次性 stdout 一行 JSON 退出
1634
+ let nl = stdoutBuf.indexOf('\n')
1635
+ while (nl >= 0) {
1636
+ const line = stdoutBuf.slice(0, nl).trim()
1637
+ stdoutBuf = stdoutBuf.slice(nl + 1)
1638
+ if (line.startsWith('{')) {
1639
+ clearInterval(hbInterval)
1640
+ try {
1641
+ const o = JSON.parse(line)
1642
+ resolve({ ready: o.ready === true, attempts: o.attempts ?? 0, waitedMs: o.waitedMs ?? 0, reason: o.reason ?? 'timeout', timeoutMs })
1643
+ } catch {
1644
+ resolve({ ready: false, attempts: 0, waitedMs: 0, reason: 'parse-error', timeoutMs })
1645
+ }
1646
+ try { child.kill('SIGTERM') } catch { /* ignore */ }
1647
+ return
1648
+ }
1649
+ nl = stdoutBuf.indexOf('\n')
1650
+ }
1651
+ })
1652
+ child.on('exit', (code) => {
1653
+ clearInterval(hbInterval)
1654
+ process.stdout.write('\n')
1655
+ if (code !== 0 && stdoutBuf.trim() === '') {
1656
+ resolve({ ready: false, attempts: 0, waitedMs: 0, reason: `watcher-exit-${code}`, timeoutMs })
1657
+ }
1658
+ })
1659
+ child.on('error', () => {
1660
+ clearInterval(hbInterval)
1661
+ resolve({ ready: false, attempts: 0, waitedMs: 0, reason: 'watcher-spawn-error', timeoutMs })
1662
+ })
1663
+ })
1664
+ }
1665
+
1666
+ /** npm 安装态(无 ts/目录)降级:内置 5s 轮询 node:http,与 health-watch 同节奏 */
1667
+ async function runInlineHealthWatch(timeoutMs) {
1668
+ const intervalMs = parseInt(process.env.GOTRY_ONBOARDING_INTERVAL_MS ?? '5000', 10) || 5_000
1669
+ const ports = [8791, 8792, 8793, 8794, 8795]
1670
+ const http = await import('node:http')
1671
+ const startedAt = Date.now()
1672
+ let attempts = 0
1673
+ while (Date.now() - startedAt < timeoutMs) {
1674
+ attempts += 1
1675
+ for (const port of ports) {
1676
+ const ok = await new Promise((resolve) => {
1677
+ const req = http.request({ host: '127.0.0.1', port, path: '/status', method: 'GET', timeout: 2_000 }, (res) => {
1678
+ if (!res.statusCode || res.statusCode >= 400) { resolve(false); return }
1679
+ let buf = ''
1680
+ res.setEncoding('utf8')
1681
+ res.on('data', (c) => { buf += c })
1682
+ res.on('end', () => {
1683
+ try { resolve(JSON.parse(buf).extensionConnected === true) } catch { resolve(false) }
1684
+ })
1685
+ res.on('error', () => resolve(false))
1686
+ })
1687
+ req.on('error', () => resolve(false))
1688
+ req.on('timeout', () => { req.destroy(); resolve(false) })
1689
+ req.end()
1690
+ })
1691
+ if (ok) {
1692
+ process.stdout.write('\n')
1693
+ return { ready: true, attempts, waitedMs: Date.now() - startedAt, reason: 'ready', timeoutMs }
1694
+ }
1695
+ }
1696
+ process.stdout.write('.')
1697
+ await new Promise((r) => setTimeout(r, intervalMs))
1698
+ }
1699
+ process.stdout.write('\n')
1700
+ return { ready: false, attempts, waitedMs: Date.now() - startedAt, reason: 'timeout', timeoutMs }
1701
+ }
1702
+
1703
+ async function main() {
1704
+ // setup flyai 子命令(issue #521):凭据唯一写面(隐藏/stdin/status/clear),
1705
+ // 必须先于 CALENDAR_CMD 判定(命令形如 `setup flyai`)。
1706
+ if (FLYAI_SETUP_CMD) process.exit(await runSetupFlyai())
1707
+
1708
+ // calendar 子命令(issue #106/D-9):可选日历挂载的 setup 状态管理(on/off/status)
1709
+ if (CALENDAR_CMD) process.exit(await runCalendar())
1710
+
1711
+ // doctor 子命令(2026-09-02 迪拜 session 复盘):可选依赖体检 + 补装指引/补装执行;
1712
+ // 只读体检零副作用(--fix 才装),win32 也可跑体检(fix 面另有提示)。
1713
+ if (DOCTOR) process.exit(await runDoctor())
1714
+
1715
+ // onboarding 子命令(issue #258):web 启动前的交互式可选能力配置(恒 exit 0,不挡 web)。
1716
+ if (ONBOARDING) { await runOnboarding(process.argv.slice(2)); process.exit(0) }
1717
+
1718
+ // wizard 子命令(2026-09-02 商店上架后退化):**只走 stdout 提示 + 健康探活等待**;
1719
+ // 不 spawn 任何 GUI 工具(不动 pbcopy / osascript / open / xdg-open / zenity),
1720
+ // 不打开 chrome://extensions,不动扩展路径——浏览器自己当安装器,gotry 不越界。
1721
+ if (WIZARD) {
1722
+ if (WIZARD_DRY_RUN) {
1723
+ say('[gotry-wizard] dry-run(零网络零浏览器零剪贴板;run-all §40 走这条)')
1724
+ say(' 步骤: ensure-extension-files → watch-extension-ready(2 步纯 Node 端)')
1725
+ say(' 平台: 不依赖 darwin/linux/win32(只 stdout)')
1726
+ say(' ✅ exit 0')
1727
+ process.exit(0)
1728
+ }
1729
+ say('')
1730
+ say('[gotry-wizard] ────────────────────────────────')
1731
+ say('[gotry-wizard] GoTry Session Bridge 是浏览器的事——在 Chrome 应用商店一键装:')
1732
+ say('[gotry-wizard] https://chromewebstore.google.com/detail/gotry-session-bridge/oeajpiccmonococjcegddlooeeohlbgd')
1733
+ say('[gotry-wizard] 装好即生效;装完后我会自动检测到(下方探活等待,最长 120s)。')
1734
+ say('[gotry-wizard] 全过程 gotry 不动你的剪贴板、不开你的 Chrome、不弹任何面板。')
1735
+ say('[gotry-wizard] ────────────────────────────────')
1736
+ say('')
1737
+ say('[gotry-wizard] 探活中(Ctrl+C 立即退出)…')
1738
+
1739
+ // 健康探活等待:扩展一就位即返回;无 GUI/剪贴板副作用
1740
+ const watchResult = await runHealthWatch()
1741
+ if (watchResult.ready) {
1742
+ say(`[gotry-wizard] ✅ 扩展就绪(${watchResult.attempts} 次探活,等待 ${watchResult.waitedMs}ms)`)
1743
+ process.exit(0)
1744
+ } else {
1745
+ say(`[gotry-wizard] ✗ ${watchResult.reason}——未在 ${watchResult.timeoutMs}ms 内就绪`)
1746
+ say('[gotry-wizard] 不需重跑;扩展未装时 dsh UI 中 gotry_session_search 的 needs-extension 会带商店链接,wizard 完全幂等。')
1747
+ process.exit(1)
1748
+ }
1749
+ }
1750
+
1751
+ if (process.platform === 'win32') {
1752
+ say('[gotry-setup] GoTry Session Bridge 扩展:推荐 Chrome 应用商店一键安装 https://chromewebstore.google.com/detail/gotry-session-bridge/oeajpiccmonococjcegddlooeeohlbgd ;本地通道:手动把包内 extension/ 目录拷到 %USERPROFILE%\\.gotry\\extension,再在 chrome://extensions 开发者模式「加载已解压的扩展程序」')
1753
+ process.exit(AUTO ? 0 : 1)
1754
+ }
1755
+ if (AUTO && (process.env.CI || process.env.GOTRY_SETUP_SKIP === '1')) {
1756
+ say('[gotry-setup] CI/GOTRY_SETUP_SKIP 检测——跳过')
1757
+ process.exit(0)
1758
+ }
1759
+ if (!AUTO && process.env.GOTRY_SETUP_SKIP === '1') { say('[gotry-setup] GOTRY_SETUP_SKIP=1——跳过'); process.exit(0) }
1760
+ const results = []
1761
+ // gotry 自留面只剩扩展是否就位;hbcli/agent-reach/dsh-better-sidebar 由各自宿主生态自管。
1762
+ if (process.env.GOTRY_SETUP_EXTENSION !== '0') results.push(await (EXTENSION_FROM === 'github' ? setupExtensionFromGithub() : setupExtension()))
1763
+ else say('[gotry-setup] GoTry Session Bridge 扩展:GOTRY_SETUP_EXTENSION=0 跳过')
1764
+ const failed = results.filter((r) => !r.ok).length
1765
+ if (failed > 0) {
1766
+ say(`[gotry-setup] ${failed} 项未就绪——gotry 本体不受影响;可稍后重跑: npx @danceiny/gotry setup`)
1767
+ process.exit(AUTO ? 0 : 1)
1768
+ }
1769
+ say('[gotry-setup] 扩展就绪检查完成。')
1770
+ process.exit(0)
1771
+ }
1772
+
1773
+ // 直接执行才跑 main(bootstrap-tests import 本模块做 setupSidebar 单测时不触发安装面;
1774
+ // bin/gotry-inner.js 始终以 `node …/gotry-bootstrap.js` 直接 spawn,不受影响)
1775
+ if (process.argv[1] && process.argv[1].endsWith('gotry-bootstrap.js')) {
1776
+ main().catch((e) => {
1777
+ say(`[gotry-setup] 异常:${e.message}(不影响 gotry 本体;可重试 npx @danceiny/gotry setup)`)
1778
+ process.exit(AUTO ? 0 : 1)
1779
+ })
1780
+ }
1781
+
1782
+ export {
1783
+ setupSidebar, sidebarInstalled,
1784
+ classifyDoctorGap, buildOnboardingPlan, onboardingSkipReason,
1785
+ runOnboardingFix, promptOnboarding, renderClassifiedPlan, runOnboarding, orchestrateWebLaunch,
1786
+ doctorFixAutoSupported, platformAutoUnavailableReason, installerEnabled,
1787
+ classifyHbcliWhoami,
1788
+ }