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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (507) hide show
  1. package/README.md +172 -219
  2. package/README.zh-CN.md +222 -0
  3. package/bin/gotry-backend.js +53 -0
  4. package/bin/gotry-booking-copilot.js +53 -0
  5. package/bin/gotry-bootstrap.js +1744 -0
  6. package/bin/gotry-inner.js +733 -83
  7. package/bin/gotry-process-liveness.d.ts +27 -0
  8. package/bin/gotry-process-liveness.js +97 -0
  9. package/bin/gotry-runtime-resolution.d.ts +27 -0
  10. package/bin/gotry-runtime-resolution.js +49 -0
  11. package/bin/gotry.js +1 -1
  12. package/client/client.js +203 -0
  13. package/cordis.gotry-patch.yml +80 -56
  14. package/data/airline-airports.json +39 -0
  15. package/data/stations-12306-verify.json +524 -0
  16. package/dist/capabilities/agent-reach-deep.js +45 -122
  17. package/dist/capabilities/agent-reach.js +101 -146
  18. package/dist/capabilities/anything.js +135 -94
  19. package/dist/capabilities/artifacts.js +370 -0
  20. package/dist/capabilities/channel-health.js +131 -0
  21. package/dist/capabilities/channel-registry.js +256 -0
  22. package/dist/capabilities/doctor.js +546 -0
  23. package/dist/capabilities/effect.js +394 -0
  24. package/dist/capabilities/external-event.js +149 -0
  25. package/dist/capabilities/fact-log.js +55 -0
  26. package/dist/capabilities/flyai-config.js +337 -0
  27. package/dist/capabilities/flyai.js +947 -110
  28. package/dist/capabilities/fx-contract.js +293 -0
  29. package/dist/capabilities/geo-atlas.js +246 -0
  30. package/dist/capabilities/ground-transfer.js +509 -0
  31. package/dist/capabilities/hbcli.js +260 -123
  32. package/dist/capabilities/hotelbyte-transaction.js +194 -0
  33. package/dist/capabilities/incident-log.js +131 -61
  34. package/dist/capabilities/itinerary-artifact.js +190 -0
  35. package/dist/capabilities/itinerary-deck-artifact.js +194 -0
  36. package/dist/capabilities/itinerary-deck-export.js +339 -0
  37. package/dist/capabilities/lavish-local.js +924 -0
  38. package/dist/capabilities/model-override.js +38 -0
  39. package/dist/capabilities/opensky.js +54 -43
  40. package/dist/capabilities/resilience.js +191 -0
  41. package/dist/capabilities/session/action-cache.js +50 -44
  42. package/dist/capabilities/session/adapters/ctrip-flight.js +120 -62
  43. package/dist/capabilities/session/adapters/ctrip-hotel.js +222 -0
  44. package/dist/capabilities/session/adapters/dida-portal.js +274 -0
  45. package/dist/capabilities/session/adapters/meituan-local.js +35 -54
  46. package/dist/capabilities/session/adapters/rail-12306.js +342 -0
  47. package/dist/capabilities/session/benchmark.js +189 -0
  48. package/dist/capabilities/session/bridge-ledger.js +224 -0
  49. package/dist/capabilities/session/extension-bridge.js +636 -0
  50. package/dist/capabilities/session/extension-channel.js +109 -0
  51. package/dist/capabilities/session/extension-distribution.js +230 -0
  52. package/dist/capabilities/session/extract.js +31 -21
  53. package/dist/capabilities/session/golden-score.js +114 -0
  54. package/dist/capabilities/session/health-watch.js +215 -0
  55. package/dist/capabilities/session/read-guard.js +75 -24
  56. package/dist/capabilities/session/static-flight-golden.js +150 -0
  57. package/dist/capabilities/session/transport.js +97 -83
  58. package/dist/capabilities/session/wizard.js +117 -0
  59. package/dist/capabilities/session-consent.js +178 -0
  60. package/dist/capabilities/session-login.js +170 -0
  61. package/dist/capabilities/session-search.js +635 -44
  62. package/dist/capabilities/spawn-bounded.js +203 -0
  63. package/dist/capabilities/sponsor-plugin.js +341 -0
  64. package/dist/capabilities/visa-policy.js +140 -0
  65. package/dist/capabilities/weather.js +243 -138
  66. package/dist/data/session-golden-20.json +25 -0
  67. package/dist/data/sf-golden-manifest.json +103 -0
  68. package/dist/data/sf-static-routes.json +91 -0
  69. package/dist/scripts/action-cache-tests.js +24 -59
  70. package/dist/scripts/agent-planning-turn-deadline-e2e.js +242 -0
  71. package/dist/scripts/agent-planning-turn-deadline-tests.js +185 -0
  72. package/dist/scripts/agent-reach-deep-tests.js +30 -40
  73. package/dist/scripts/agent-reach-error-tests.js +354 -0
  74. package/dist/scripts/agent-reach-tests.js +26 -18
  75. package/dist/scripts/agent-reach-wrapper-tests.js +50 -50
  76. package/dist/scripts/anything-tests.js +116 -60
  77. package/dist/scripts/artifact-client-contract-tests.js +220 -0
  78. package/dist/scripts/artifacts-capability-tests.js +727 -0
  79. package/dist/scripts/async-collect.js +50 -11
  80. package/dist/scripts/benchmark-environment-bridge-e2e.js +741 -0
  81. package/dist/scripts/benchmark-environment-bridge-tests.js +1452 -0
  82. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +126 -0
  83. package/dist/scripts/booking-copilot-availability-policy-tests.js +265 -0
  84. package/dist/scripts/booking-copilot-bin-proof-tests.js +66 -0
  85. package/dist/scripts/booking-copilot-cleanup-proof-helper.js +76 -0
  86. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +144 -0
  87. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +424 -0
  88. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +2839 -0
  89. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +309 -0
  90. package/dist/scripts/booking-copilot-dsh-readiness-proof.js +238 -0
  91. package/dist/scripts/booking-copilot-dsh-warmer-proof.js +123 -0
  92. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +156 -0
  93. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +70 -0
  94. package/dist/scripts/booking-copilot-intent-proof-tests.js +422 -0
  95. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +184 -0
  96. package/dist/scripts/booking-copilot-planner-live-probe.js +95 -0
  97. package/dist/scripts/booking-copilot-proof-runtime.js +42 -0
  98. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +210 -0
  99. package/dist/scripts/booking-copilot-runtime-proof-tests.js +2349 -0
  100. package/dist/scripts/booking-copilot-server-proof-tests.js +784 -0
  101. package/dist/scripts/booking-copilot-startup-proof-tests.js +94 -0
  102. package/dist/scripts/booking-executor-tests.js +80 -0
  103. package/dist/scripts/booking-recovery-chain-tests.js +182 -0
  104. package/dist/scripts/booking-saga-tests.js +125 -0
  105. package/dist/scripts/booking-surface-contract-proof-tests.js +212 -0
  106. package/dist/scripts/bootstrap-tests.js +1479 -0
  107. package/dist/scripts/bridge-events-tests.js +239 -0
  108. package/dist/scripts/bridge-ledger-tests.js +428 -0
  109. package/dist/scripts/build-changelog.js +238 -0
  110. package/dist/scripts/build-metrics-report.js +331 -0
  111. package/dist/scripts/changelog-tests.js +118 -0
  112. package/dist/scripts/channel-probe-tests.js +103 -0
  113. package/dist/scripts/channel-probe.js +169 -0
  114. package/dist/scripts/channel-registry-tests.js +208 -0
  115. package/dist/scripts/companion-tests.js +25 -70
  116. package/dist/scripts/dida-cookie-probe.js +87 -0
  117. package/dist/scripts/dida-runner-stop-tests.js +311 -0
  118. package/dist/scripts/dida-sw-probe.js +114 -0
  119. package/dist/scripts/diff-test.js +17 -12
  120. package/dist/scripts/doctor-tests.js +564 -0
  121. package/dist/scripts/dsh-artifact-e2e.js +279 -0
  122. package/dist/scripts/dsh-artifact-web-e2e.js +675 -0
  123. package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
  124. package/dist/scripts/dsh-runtime-closure-tests.js +182 -0
  125. package/dist/scripts/dsh-runtime-closure.js +162 -0
  126. package/dist/scripts/dsh-session-v3-migration-proof.js +109 -0
  127. package/dist/scripts/dsh-subprocess-local-proof.js +150 -0
  128. package/dist/scripts/dsh-target-closure-proof.js +178 -0
  129. package/dist/scripts/effect-tests.js +539 -0
  130. package/dist/scripts/engine-run.js +1 -3
  131. package/dist/scripts/engine-tests.js +16 -10
  132. package/dist/scripts/evaluation-cadence-tests.js +174 -0
  133. package/dist/scripts/evaluation-contract-tests.js +268 -0
  134. package/dist/scripts/extension-distribution-cli.js +39 -0
  135. package/dist/scripts/extension-distribution-tests.js +443 -0
  136. package/dist/scripts/extension-tests.js +988 -0
  137. package/dist/scripts/external-event-tests.js +350 -0
  138. package/dist/scripts/fact-gate-tests.js +869 -0
  139. package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
  140. package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
  141. package/dist/scripts/flight-malformed-tests.js +159 -0
  142. package/dist/scripts/flyai-setup-tests.js +228 -0
  143. package/dist/scripts/flyai-setup-tool-tests.js +54 -0
  144. package/dist/scripts/flyai-tests.js +692 -0
  145. package/dist/scripts/flyai-tool-registration-contract-tests.js +91 -0
  146. package/dist/scripts/fx-contract-tests.js +201 -0
  147. package/dist/scripts/g5-guard-tests.js +131 -0
  148. package/dist/scripts/g5-guard.js +189 -0
  149. package/dist/scripts/gen-lunar-anchors.js +106 -0
  150. package/dist/scripts/geo-atlas-tests.js +283 -0
  151. package/dist/scripts/gotry-backend-kernel-tests.js +206 -0
  152. package/dist/scripts/gotry-try-demo-tests.js +79 -0
  153. package/dist/scripts/gotry-try-demo.js +49 -0
  154. package/dist/scripts/ground-transfer-tests.js +956 -0
  155. package/dist/scripts/hbcli-e2e-tests.js +159 -0
  156. package/dist/scripts/hbcli-live-optin-tests.js +96 -0
  157. package/dist/scripts/hbcli-release-contract-tests.js +122 -0
  158. package/dist/scripts/hbcli-tests.js +121 -69
  159. package/dist/scripts/health-watch-cli.js +73 -0
  160. package/dist/scripts/hotel-date-gate-tests.js +496 -0
  161. package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
  162. package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
  163. package/dist/scripts/i18n-tests.js +41 -0
  164. package/dist/scripts/incident-tests.js +281 -87
  165. package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
  166. package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
  167. package/dist/scripts/issue-271-liveness-tests.js +569 -0
  168. package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
  169. package/dist/scripts/issue-289-model-retry-tests.js +441 -0
  170. package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
  171. package/dist/scripts/issue-338-home-city-e2e.js +577 -0
  172. package/dist/scripts/issue343-real-entry-e2e.js +160 -0
  173. package/dist/scripts/issue436-persisted-routing-e2e.js +247 -0
  174. package/dist/scripts/itinerary-artifact-tests.js +368 -0
  175. package/dist/scripts/itinerary-deck-artifact-tests.js +416 -0
  176. package/dist/scripts/itinerary-deck-export-tests.js +552 -0
  177. package/dist/scripts/itinerary-deck-tests.js +435 -0
  178. package/dist/scripts/itinerary-html-tests.js +426 -0
  179. package/dist/scripts/journey-tests.js +22 -27
  180. package/dist/scripts/kernel-manifest-evidence.js +109 -0
  181. package/dist/scripts/kernel-manifest-gate.js +126 -0
  182. package/dist/scripts/kernel-manifest-tests.js +212 -0
  183. package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
  184. package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
  185. package/dist/scripts/kernel-manifest.js +369 -0
  186. package/dist/scripts/lavish-local-tests.js +1226 -0
  187. package/dist/scripts/lavish-product-tools-tests.js +537 -0
  188. package/dist/scripts/ledger-repair-apply-tests.js +316 -0
  189. package/dist/scripts/ledger-repair-plan-tests.js +375 -0
  190. package/dist/scripts/ledger-tests.js +382 -246
  191. package/dist/scripts/ledger-workflow-crash.js +10 -20
  192. package/dist/scripts/managed-dsh-cleanup-diagnostic-proof.js +184 -0
  193. package/dist/scripts/managed-dsh-close-failure-proof.js +172 -0
  194. package/dist/scripts/managed-dsh-run-port-proof.js +153 -0
  195. package/dist/scripts/managed-dsh-terminal-proof.js +252 -0
  196. package/dist/scripts/map-tools-vendor-package-proof.js +365 -0
  197. package/dist/scripts/memory-capture-tests.js +242 -61
  198. package/dist/scripts/memory-decay-tests.js +18 -52
  199. package/dist/scripts/memory-lifecycle-tests.js +657 -0
  200. package/dist/scripts/memory-lifecycle.js +196 -0
  201. package/dist/scripts/memory-metrics.js +24 -12
  202. package/dist/scripts/memory-value-report.js +631 -0
  203. package/dist/scripts/memory-value-tests.js +328 -0
  204. package/dist/scripts/metrics-report-tests.js +151 -0
  205. package/dist/scripts/model-override-e2e.js +137 -0
  206. package/dist/scripts/nightly-evidence-tests.js +161 -0
  207. package/dist/scripts/nightly-evidence.js +286 -0
  208. package/dist/scripts/nudge-digest.js +39 -28
  209. package/dist/scripts/observability-tests.js +381 -0
  210. package/dist/scripts/onboarding-tests.js +161 -0
  211. package/dist/scripts/opensky-check.js +18 -13
  212. package/dist/scripts/opensky-tests.js +28 -28
  213. package/dist/scripts/persona-surface-guard-tests.js +49 -0
  214. package/dist/scripts/pnpm-dsh-closure-proof.js +17 -0
  215. package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
  216. package/dist/scripts/price-drift-tests.js +340 -0
  217. package/dist/scripts/price-drift-watch.js +447 -0
  218. package/dist/scripts/probe-poi-tests.js +53 -45
  219. package/dist/scripts/product-metrics.js +483 -0
  220. package/dist/scripts/publish-preverify.js +110 -0
  221. package/dist/scripts/realtime-pricing-tests.js +76 -0
  222. package/dist/scripts/recall-tests.js +513 -0
  223. package/dist/scripts/replay-async.js +20 -18
  224. package/dist/scripts/replay-real.js +18 -18
  225. package/dist/scripts/replay.js +52 -82
  226. package/dist/scripts/session-attach-diagnose.js +20 -20
  227. package/dist/scripts/session-attach-poc.js +34 -37
  228. package/dist/scripts/session-benchmark.js +180 -0
  229. package/dist/scripts/session-dida-live-e2e.js +199 -0
  230. package/dist/scripts/session-extract-tests.js +14 -35
  231. package/dist/scripts/session-link-tests.js +478 -0
  232. package/dist/scripts/session-login.js +27 -46
  233. package/dist/scripts/session-tests.js +796 -96
  234. package/dist/scripts/session-zones-tests.js +358 -0
  235. package/dist/scripts/sf-live-benchmark.js +367 -0
  236. package/dist/scripts/sf-live-challenge-stop-tests.js +453 -0
  237. package/dist/scripts/sf-live-cli-tests.js +92 -0
  238. package/dist/scripts/sf-live-options.js +46 -0
  239. package/dist/scripts/sf-manifest-tests.js +30 -0
  240. package/dist/scripts/sf-manifest.js +33 -0
  241. package/dist/scripts/sf-soft-score-tests.js +67 -0
  242. package/dist/scripts/sf-summary-tests.js +324 -0
  243. package/dist/scripts/sf-summary.js +525 -0
  244. package/dist/scripts/share-tests.js +432 -0
  245. package/dist/scripts/skeleton-check.js +16 -14
  246. package/dist/scripts/skeleton-integration-test.js +28 -11
  247. package/dist/scripts/skills-contract-tests.js +51 -46
  248. package/dist/scripts/smoke-session-gate-tests.js +21 -0
  249. package/dist/scripts/smoke.js +690 -246
  250. package/dist/scripts/spawn-bounded-abort-tests.js +241 -0
  251. package/dist/scripts/sponsor-reuse-tests.js +262 -0
  252. package/dist/scripts/state-cli-tests.js +226 -101
  253. package/dist/scripts/state-cli.js +550 -199
  254. package/dist/scripts/state-repair-cli.js +66 -0
  255. package/dist/scripts/state-repair-tests.js +140 -0
  256. package/dist/scripts/static-golden-tests.js +165 -0
  257. package/dist/scripts/time-eval-tests.js +210 -192
  258. package/dist/scripts/travel-timeline-tests.js +29 -93
  259. package/dist/scripts/turn-handoff-collect-tests.js +169 -0
  260. package/dist/scripts/turn-handoff-collect.js +161 -0
  261. package/dist/scripts/turn-policy-tests.js +55 -0
  262. package/dist/scripts/typed-contract-canary.js +176 -0
  263. package/dist/scripts/tz-resolver-sanity.js +50 -0
  264. package/dist/scripts/tz-resolver-tests.js +584 -0
  265. package/dist/scripts/unified-tests.js +23 -19
  266. package/dist/scripts/visa-policy-tests.js +62 -0
  267. package/dist/scripts/weather-tests.js +276 -41
  268. package/dist/scripts/wish-channel-gate-tests.js +54 -0
  269. package/dist/scripts/write-gate-tests.js +522 -0
  270. package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
  271. package/dist/scripts/z3-lifecycle-tests.js +170 -0
  272. package/dist/scripts/z3-race-repeat-tests.js +39 -0
  273. package/dist/scripts/z3-race-tests.js +41 -0
  274. package/dist/src/artifact-gate.js +829 -0
  275. package/dist/src/backend/kernel.js +91 -0
  276. package/dist/src/backend/modules/booking-copilot.js +92 -0
  277. package/dist/src/backend/modules/booking-executor.js +124 -0
  278. package/dist/src/backend/modules/session-search.js +551 -0
  279. package/dist/src/benchmark-agent-conformance.js +578 -0
  280. package/dist/src/benchmark-environment-bridge.js +515 -0
  281. package/dist/src/benchmark-headless-child-diagnostics.js +166 -0
  282. package/dist/src/benchmark-tool-isolation.js +151 -0
  283. package/dist/src/bookable-facts.js +533 -0
  284. package/dist/src/booking-saga.js +97 -0
  285. package/dist/src/booking-surface/availability-policy.js +697 -0
  286. package/dist/src/booking-surface/booking-intent.js +43 -0
  287. package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
  288. package/dist/src/booking-surface/canonical-schema.js +116 -0
  289. package/dist/src/booking-surface/contracts.js +75 -0
  290. package/dist/src/booking-surface/dsh-planner.js +1854 -0
  291. package/dist/src/booking-surface/dsh-plugin.js +378 -0
  292. package/dist/src/booking-surface/dsh-worker.js +98 -0
  293. package/dist/src/booking-surface/error-codes.js +231 -0
  294. package/dist/src/booking-surface/index.js +9 -0
  295. package/dist/src/booking-surface/managed-dsh-cleanup-diagnostic.js +97 -0
  296. package/dist/src/booking-surface/managed-dsh-run-port.js +244 -0
  297. package/dist/src/booking-surface/profile.js +24 -0
  298. package/dist/src/booking-surface/recovery-chain.js +367 -0
  299. package/dist/src/booking-surface/runtime.js +1968 -0
  300. package/dist/src/booking-surface/server.js +674 -0
  301. package/dist/src/booking-surface/startup.js +168 -0
  302. package/dist/src/booking-surface/validation.js +261 -0
  303. package/dist/src/bridge.js +20 -13
  304. package/dist/src/companions.js +48 -74
  305. package/dist/src/contracts.js +21 -43
  306. package/dist/src/dsh-llm.js +97 -111
  307. package/dist/src/engine.js +276 -193
  308. package/dist/src/evaluation-cadence.js +97 -0
  309. package/dist/src/evaluation-contracts.js +283 -0
  310. package/dist/src/flight-pack-adapter.js +54 -0
  311. package/dist/src/flight-pack-contract.js +2 -0
  312. package/dist/src/flyai-setup-tool.js +70 -0
  313. package/dist/src/gotry-backend.js +125 -0
  314. package/dist/src/hotel-date-gate.js +118 -0
  315. package/dist/src/i18n.js +88 -0
  316. package/dist/src/index.js +2227 -882
  317. package/dist/src/itinerary-deck.js +139 -0
  318. package/dist/src/itinerary-doc-shared.js +661 -0
  319. package/dist/src/itinerary-html.js +131 -0
  320. package/dist/src/journey.js +155 -108
  321. package/dist/src/lavish-tools.js +400 -0
  322. package/dist/src/ledger-repair-apply.js +538 -0
  323. package/dist/src/ledger-repair-plan.js +463 -0
  324. package/dist/src/loop.js +319 -160
  325. package/dist/src/memory-capture.js +180 -32
  326. package/dist/src/memory-decay.js +30 -14
  327. package/dist/src/memory-lifecycle.js +1310 -0
  328. package/dist/src/memory-utility.js +27 -40
  329. package/dist/src/mock-llm.js +36 -61
  330. package/dist/src/model.js +81 -36
  331. package/dist/src/realtime-pricing.js +103 -0
  332. package/dist/src/recall/card.js +74 -0
  333. package/dist/src/recall/evaluator.js +140 -0
  334. package/dist/src/recall/integration.js +17 -0
  335. package/dist/src/recall/tick.js +166 -0
  336. package/dist/src/session-link/plan-it-action.js +48 -0
  337. package/dist/src/session-link/session-link.js +248 -0
  338. package/dist/src/session-zones.js +507 -0
  339. package/dist/src/share/adapters.js +51 -0
  340. package/dist/src/share/share-consent.js +76 -0
  341. package/dist/src/share/share-deck.js +58 -0
  342. package/dist/src/share/share-token.js +146 -0
  343. package/dist/src/slot-spec.js +84 -106
  344. package/dist/src/state-ledger.js +435 -516
  345. package/dist/src/state-repair.js +299 -0
  346. package/dist/src/subagent-job-id-guard.js +58 -0
  347. package/dist/src/time-anchor.js +231 -33
  348. package/dist/src/tool-packet.js +20 -8
  349. package/dist/src/travel-slots.js +58 -35
  350. package/dist/src/travel-timeline.js +44 -63
  351. package/dist/src/turn-deadline.js +287 -0
  352. package/dist/src/turn-policy.js +118 -0
  353. package/dist/src/tz-resolver.js +143 -0
  354. package/dist/src/unified.js +535 -359
  355. package/dist/src/wish-pool.js +42 -12
  356. package/dist/src/write-gate.js +495 -0
  357. package/dist/src/z3-shared.js +284 -0
  358. package/extension/README.md +21 -0
  359. package/extension/README.zh-CN.md +21 -0
  360. package/extension/background.js +463 -0
  361. package/extension/content-bridge.js +270 -0
  362. package/extension/content-main.js +111 -0
  363. package/extension/manifest.json +60 -0
  364. package/package.json +407 -14
  365. package/schemas/booking.intent.schema.json +193 -0
  366. package/schemas/booking.surface.schema.json +2622 -0
  367. package/ts/capabilities/agent-reach-deep.ts +114 -0
  368. package/ts/capabilities/agent-reach.ts +198 -0
  369. package/ts/capabilities/anything.ts +255 -0
  370. package/ts/capabilities/artifacts.ts +428 -0
  371. package/ts/capabilities/fact-log.ts +54 -0
  372. package/ts/capabilities/flyai-config.ts +388 -0
  373. package/ts/capabilities/flyai.ts +1274 -0
  374. package/ts/capabilities/hbcli.ts +253 -71
  375. package/ts/capabilities/incident-log.ts +88 -59
  376. package/ts/capabilities/itinerary-artifact.ts +214 -0
  377. package/ts/capabilities/itinerary-deck-artifact.ts +218 -0
  378. package/ts/capabilities/itinerary-deck-export.ts +378 -0
  379. package/ts/capabilities/opensky.ts +121 -0
  380. package/ts/capabilities/session/action-cache.ts +148 -0
  381. package/ts/capabilities/session/adapters/ctrip-flight.ts +181 -0
  382. package/ts/capabilities/session/adapters/ctrip-hotel.ts +238 -0
  383. package/ts/capabilities/session/adapters/dida-portal.ts +338 -0
  384. package/ts/capabilities/session/adapters/meituan-local.ts +70 -0
  385. package/ts/capabilities/session/adapters/rail-12306.ts +392 -0
  386. package/ts/capabilities/session/benchmark.ts +276 -0
  387. package/ts/capabilities/session/bridge-ledger.ts +333 -0
  388. package/ts/capabilities/session/extension-bridge.ts +794 -0
  389. package/ts/capabilities/session/extension-channel.ts +144 -0
  390. package/ts/capabilities/session/extension-distribution.ts +266 -0
  391. package/ts/capabilities/session/extract.ts +54 -0
  392. package/ts/capabilities/session/golden-score.ts +139 -0
  393. package/ts/capabilities/session/health-watch.ts +264 -0
  394. package/ts/capabilities/session/read-guard.ts +141 -0
  395. package/ts/capabilities/session/static-flight-golden.ts +209 -0
  396. package/ts/capabilities/session/transport.ts +146 -0
  397. package/ts/capabilities/session/wizard.ts +140 -0
  398. package/ts/capabilities/session-consent.ts +211 -0
  399. package/ts/capabilities/session-login.ts +208 -0
  400. package/ts/capabilities/session-search.ts +893 -0
  401. package/ts/capabilities/weather.ts +330 -0
  402. package/ts/dsh-runtime/vendor/README.md +68 -0
  403. package/ts/dsh-runtime/vendor/dsh-map-tools/LICENSE +21 -0
  404. package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +405 -0
  405. package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +404 -0
  406. package/ts/dsh-runtime/vendor/dsh-map-tools/client/client.js +1584 -0
  407. package/ts/dsh-runtime/vendor/dsh-map-tools/cordis.patch.yml +5 -0
  408. package/ts/dsh-runtime/vendor/dsh-map-tools/icon.svg +5 -0
  409. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/amap.js +595 -0
  410. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/nominatim.js +63 -0
  411. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/osrm.js +58 -0
  412. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/photon.js +55 -0
  413. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-file.js +87 -0
  414. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-route.js +131 -0
  415. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config.js +16 -0
  416. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/geo.js +232 -0
  417. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/index.js +120 -0
  418. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +40 -0
  419. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/staticmap-route.js +115 -0
  420. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/geocode.js +127 -0
  421. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/poi.js +84 -0
  422. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/routes.js +267 -0
  423. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/amap.d.ts +90 -0
  424. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/nominatim.d.ts +17 -0
  425. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/osrm.d.ts +15 -0
  426. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/photon.d.ts +22 -0
  427. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-file.d.ts +33 -0
  428. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-route.d.ts +23 -0
  429. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config.d.ts +23 -0
  430. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/geo.d.ts +61 -0
  431. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/index.d.ts +18 -0
  432. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +14 -0
  433. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/staticmap-route.d.ts +31 -0
  434. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/geocode.d.ts +11 -0
  435. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/poi.d.ts +9 -0
  436. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/routes.d.ts +75 -0
  437. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/types.d.ts +68 -0
  438. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types.js +16 -0
  439. package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +97 -0
  440. package/ts/package.json +293 -3
  441. package/ts/scripts/async-collect.ts +91 -0
  442. package/ts/scripts/map-tools-vendor-package-proof.ts +404 -0
  443. package/ts/scripts/skeleton-check.ts +1 -1
  444. package/ts/scripts/skeleton-integration-test.ts +23 -1
  445. package/ts/scripts/state-cli.ts +568 -0
  446. package/ts/scripts/turn-handoff-collect.ts +178 -0
  447. package/ts/src/artifact-gate.ts +991 -0
  448. package/ts/src/benchmark-agent-conformance.ts +619 -0
  449. package/ts/src/benchmark-environment-bridge.ts +538 -0
  450. package/ts/src/benchmark-headless-child-diagnostics.ts +184 -0
  451. package/ts/src/benchmark-tool-isolation.ts +166 -0
  452. package/ts/src/bookable-facts.ts +837 -0
  453. package/ts/src/booking-saga.ts +133 -0
  454. package/ts/src/booking-surface/availability-policy.ts +522 -0
  455. package/ts/src/booking-surface/booking-intent.ts +86 -0
  456. package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
  457. package/ts/src/booking-surface/canonical-schema.js +116 -0
  458. package/ts/src/booking-surface/contracts.ts +475 -0
  459. package/ts/src/booking-surface/dsh-planner.ts +1949 -0
  460. package/ts/src/booking-surface/dsh-plugin.js +378 -0
  461. package/ts/src/booking-surface/dsh-worker.ts +110 -0
  462. package/ts/src/booking-surface/error-codes.ts +241 -0
  463. package/ts/src/booking-surface/index.ts +9 -0
  464. package/ts/src/booking-surface/managed-dsh-cleanup-diagnostic.ts +151 -0
  465. package/ts/src/booking-surface/managed-dsh-run-port.ts +273 -0
  466. package/ts/src/booking-surface/profile.ts +42 -0
  467. package/ts/src/booking-surface/recovery-chain.ts +409 -0
  468. package/ts/src/booking-surface/runtime.ts +1793 -0
  469. package/ts/src/booking-surface/server.ts +733 -0
  470. package/ts/src/booking-surface/startup.ts +201 -0
  471. package/ts/src/booking-surface/validation.ts +217 -0
  472. package/ts/src/companions.ts +113 -0
  473. package/ts/src/contracts.ts +13 -3
  474. package/ts/src/dsh-llm.ts +44 -11
  475. package/ts/src/flyai-setup-tool.ts +77 -0
  476. package/ts/src/index.ts +1948 -198
  477. package/ts/src/itinerary-deck.ts +189 -0
  478. package/ts/src/itinerary-doc-shared.ts +741 -0
  479. package/ts/src/itinerary-html.ts +171 -0
  480. package/ts/src/loop.ts +223 -25
  481. package/ts/src/memory-capture.ts +283 -0
  482. package/ts/src/memory-decay.ts +55 -0
  483. package/ts/src/memory-utility.ts +70 -0
  484. package/ts/src/mock-llm.ts +4 -8
  485. package/ts/src/model.ts +88 -2
  486. package/ts/src/recall/card.ts +106 -0
  487. package/ts/src/recall/evaluator.ts +170 -0
  488. package/ts/src/recall/integration.ts +35 -0
  489. package/ts/src/recall/tick.ts +213 -0
  490. package/ts/src/session-link/plan-it-action.ts +78 -0
  491. package/ts/src/session-link/session-link.ts +275 -0
  492. package/ts/src/share/adapters.ts +93 -0
  493. package/ts/src/share/share-consent.ts +105 -0
  494. package/ts/src/share/share-deck.ts +85 -0
  495. package/ts/src/share/share-token.ts +166 -0
  496. package/ts/src/slot-spec.ts +172 -0
  497. package/ts/src/state-ledger.ts +912 -0
  498. package/ts/src/subagent-job-id-guard.ts +72 -0
  499. package/ts/src/time-anchor.ts +338 -0
  500. package/ts/src/tool-packet.ts +32 -0
  501. package/ts/src/travel-slots.ts +219 -0
  502. package/ts/src/travel-timeline.ts +83 -0
  503. package/ts/src/turn-deadline.ts +366 -0
  504. package/ts/src/turn-policy.ts +154 -0
  505. package/ts/src/unified.ts +354 -88
  506. package/ts/src/wish-pool.ts +79 -0
  507. package/dist/scripts/session-attach-wait.js +0 -41
@@ -0,0 +1,794 @@
1
+ /**
2
+ * 会话扩展桥(extension-bridge,RFC docs/rfc/user-session-data-rfc.md §2.2 通道 C——2026-08-29 founder 定案为 PRIMARY 传输)。
3
+ *
4
+ * 形态:Node http 服务,懒启动单例,只绑 127.0.0.1,端口池 8791-8795(与 extension/manifest.json
5
+ * host_permissions 一一对应,防漂移测试守住);**零新依赖**(node:http 手写,对齐已退役 shell 薄壳
6
+ * 的回环纪律;不引 ws——MV3 Service Worker 靠 ≤20s 的长轮询节奏维持 30s 存活窗口,HTTP 够用)。
7
+ *
8
+ * 授权模型:扩展经「一次性安装」获得信任,每请求校验 Origin ∈ 双通道扩展源白名单
9
+ * (网页发起的跨域请求必带邪恶 Origin,直接 403;POST 端点强制;GET /health /status 是本机
10
+ * 诊断面放行)。不做 CORS——有 host_permissions 的扩展 fetch 天然免预检,网页过不来。
11
+ * 双通道 ID 实录(2026-09-02):unpacked 通道(bundled/GitHub Releases)由 manifest 固定 key
12
+ * 派生 EXTENSION_ID;Chrome Web Store 用商店自己的签名 key 重签(不认 manifest key),
13
+ * 商店版 ID = EXTENSION_ID_STORE。两个 ID 都是 founder 控制的同一扩展,白名单双收。
14
+ *
15
+ * 协议(长轮询):
16
+ * GET /health → lastSeen 刷新(扩展每 ≤30s 一次 + 每次断线重连)
17
+ * GET /status → 诊断面 {port, extensionConnected, lastSeenMsAgo, queued, inFlight, parked}
18
+ * POST /jobs → 长轮询取活(hold ≤20s 返 {job:null});扩展单客户端
19
+ * POST /results/:jobId → 善果/失败回传(≤8MB;batchSearch ~550KB)
20
+ * 语义:桥不存在即整个扩展车道不存在——fail-closed 不变量在调用方(session-search 的 needs-extension)。
21
+ * 审计:job 提交/回传写 GuardAuditEntry 同款 JSONL(kind:'extension-session-job',channel 层负责)。
22
+ */
23
+
24
+ import { randomUUID } from 'node:crypto'
25
+ import { existsSync } from 'node:fs'
26
+ import { homedir } from 'node:os'
27
+ import { join } from 'node:path'
28
+ import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http'
29
+
30
+ /** 桥端口池(extension/manifest.json host_permissions 必须同面;§38 断言) */
31
+ export const BRIDGE_PORTS = [8791, 8792, 8793, 8794, 8795] as const
32
+ export const BRIDGE_PROTOCOL = 'session-bridge.v1'
33
+ /** 扩展固定 ID(unpacked 通道:manifest key 派生;bundled/GitHub Releases 均此 ID。变更即失配,Node 侧拒绝) */
34
+ export const EXTENSION_ID = 'olpgkofjhhiiiahdkkbcninhjmegghfe'
35
+ export const EXTENSION_ORIGIN = `chrome-extension://${EXTENSION_ID}`
36
+ /**
37
+ * Chrome Web Store 版扩展 ID(2026-09-02 上架):商店用自己生成的签名 key 重签,
38
+ * **不认 manifest 里的固定 key**——商店版 ID 与 unpacked 版不同(上架材料文档的
39
+ * 「若商店不认 key」预案坐实)。item ID 即商店详情页 URL 末段。
40
+ */
41
+ export const EXTENSION_ID_STORE = 'oeajpiccmonococjcegddlooeeohlbgd'
42
+ export const EXTENSION_ORIGIN_STORE = `chrome-extension://${EXTENSION_ID_STORE}`
43
+ /** 商店详情页(一键安装 + 自动更新通道;推荐安装方式) */
44
+ export const EXTENSION_STORE_URL = `https://chromewebstore.google.com/detail/gotry-session-bridge/${EXTENSION_ID_STORE}`
45
+ /** 桥信任的扩展 Origin 白名单:unpacked(固定 key)+ 商店版,双通道同一扩展同源信任 */
46
+ export const EXTENSION_ORIGINS: readonly string[] = [EXTENSION_ORIGIN, EXTENSION_ORIGIN_STORE]
47
+
48
+ /**
49
+ * 本地 unpacked 通道是否已落位(~/.gotry/extension/manifest.json)。
50
+ * 商店版没有本地文件面(装在 Chrome 里),离线不可探测——本函数只回答「本地通道在不在」,
51
+ * 商店版用户在这里恒 false,这正是 D-24 自适应文案的判定输入。
52
+ */
53
+ export function localExtensionInstalled(home = homedir()): boolean {
54
+ return existsSync(join(home, '.gotry', 'extension', 'manifest.json'))
55
+ }
56
+
57
+ /**
58
+ * needs-extension 自适应摘要(issue #117,D-24 残量):
59
+ * - 本地 unpacked 已落位 → 完整双通道指引(商店 + GitHub/开发者模式本地通道);
60
+ * - 未落位(商店版用户,或完全未装)→ 只推商店一键装,并给「已装商店版?打开 Chrome 即可」
61
+ * 提示——**不再出现开发者模式/本地通道文案**(已装商店版的用户不该再被指导 load unpacked)。
62
+ * 影响面统一行:账号会话通道断 → 列表底部工具不提供;其余工具不受影响。
63
+ * 纯函数(默认 HOME 可注入)。
64
+ *
65
+ * 双语对照:CN=本函数返回值;EN 权威见 docs/capability-onboarding.md「Session Bridge 安装」段。
66
+ */
67
+ const EXTENSION_IMPACT_LINE = '影响面:gotry_session_search / gotry_session_login(账号会话通道)不可用;携程机票/酒店实时检索、12306 余票、Dida 实时报价都依赖此通道。其它工具(机票 FlyAI、酒店 hbcli、地图、天气等)不受影响。'
68
+ export function needsExtensionSummary(opts: { localInstalled?: boolean; home?: string } = {}): string {
69
+ const local = opts.localInstalled ?? localExtensionInstalled(opts.home)
70
+ if (local) {
71
+ return 'GoTry Session Bridge 扩展未连接(未安装或已停用)。推荐 Chrome 应用商店一键装(自动更新) '
72
+ + `${EXTENSION_STORE_URL} ;或 npx @danceiny/gotry setup 落位后 chrome://extensions 开发者模式「加载已解压的扩展程序」指向 ~/.gotry/extension。`
73
+ + EXTENSION_IMPACT_LINE
74
+ }
75
+ return 'GoTry Session Bridge 扩展未连接。若你已从 Chrome 商店安装(自动更新):打开 Chrome 并确认扩展已启用即可自动连接,无需再装。'
76
+ + `若尚未安装:应用商店一键装 ${EXTENSION_STORE_URL} (装完零弹窗,浏览器自己当安装器)。`
77
+ + EXTENSION_IMPACT_LINE
78
+ }
79
+ /** 扩展在线判定:/health 心跳 ≤30s 一次 + 轮询重连,1.5 倍容差 */
80
+ export const EXTENSION_CONNECTED_WINDOW_MS = 45_000
81
+ /** /jobs 长轮询 hold 上限(必须 < MV3 SW 30s 存活窗口) */
82
+ export const JOBS_LONG_POLL_MS = 20_000
83
+ /** 提交 job 时扩展尚未上线,等它连上桥的宽限(首次调用后桥才拉起,扩展 5s 内即回连) */
84
+ export const DEFAULT_EXTENSION_WAIT_MS = 6_000
85
+ /** /results 回包上限(batchSearch ~550KB,余量给未来酒店/多航段) */
86
+ export const MAX_RESULT_BODY_BYTES = 8 * 1024 * 1024
87
+
88
+ export type ExtensionJobKind = 'search' | 'open-login' | 'cookie-names'
89
+
90
+ /** search + multiCollect(dida 推荐流):按接口类别分桶的嗅探回包体 */
91
+ export interface SniffBodies {
92
+ hotels?: string
93
+ recommendPrices?: string
94
+ realtime?: string
95
+ /** /hotel/list 查询态价格面(2026-09-21): 按城市+日期的酒店报价列表 */
96
+ searchCache?: string
97
+ }
98
+
99
+ export interface ExtensionJob {
100
+ jobId: string
101
+ kind: ExtensionJobKind
102
+ site: string
103
+ /** search / open-login:置顶导航地址(白名单域在扩展侧再校验一道) */
104
+ url?: string
105
+ /** search:等嗅探回包上限 */
106
+ timeoutMs?: number
107
+ /**
108
+ * search:dida 推荐流多回包收集(2026-09-11,执行环境迁浏览器客户端)——
109
+ * find 页加载自发 hotels + recommendPrices 两个接口,单首包语义会丢一半数据;
110
+ * true 时扩展按类别收齐(或超时结算)后经 bodies 回传
111
+ */
112
+ multiCollect?: boolean
113
+ }
114
+
115
+ export interface ExtensionJobResult {
116
+ ok: boolean
117
+ kind?: ExtensionJobKind
118
+ /** cookie-names:命中票据名(名字级——协议面不存在 cookie 值字段) */
119
+ names?: string[]
120
+ /** search:NETWORK_HINTS 命中的响应原文 */
121
+ body?: string
122
+ /** search:the exact NETWORK_HINTS response URL selected for body;未命中/超时为空 */
123
+ url?: string
124
+ /** search + multiCollect:分桶回包体(见 SniffBodies) */
125
+ bodies?: SniffBodies
126
+ title?: string
127
+ /** search:页面级挑战标记(content-bridge 按 DOM 判定) */
128
+ challenge?: boolean
129
+ opened?: boolean
130
+ timeout?: boolean
131
+ error?: string
132
+ }
133
+
134
+ export type SubmitOutcome =
135
+ | { ok: true; result: ExtensionJobResult; origin?: string; clientId?: string }
136
+ | { ok: false; reason: 'bridge-unavailable' | 'extension-not-connected' | 'timeout'; summary: string }
137
+
138
+ export interface SessionJobHandle {
139
+ submit(job: Omit<ExtensionJob, 'jobId'>, opts?: { timeoutMs?: number; extensionWaitMs?: number; preferredOrigin?: string; preferredClientId?: string }): Promise<SubmitOutcome>
140
+ extensionConnected(): boolean
141
+ port: number
142
+ close(): Promise<void>
143
+ }
144
+
145
+ export interface SessionBridgeOptions {
146
+ /** 测试用 [0] 让 OS 分配;缺省 8791..8795 端口池 */
147
+ ports?: number[]
148
+ /** 测试覆盖信任的扩展 Origin 白名单;缺省 EXTENSION_ORIGINS(unpacked + 商店版双通道) */
149
+ extensionOrigins?: readonly string[]
150
+ now?: () => number
151
+ /**
152
+ * **保持桥进程钉住事件循环**(不 unref)。wizard 期间需要:扩展 SW 周期心跳
153
+ * ≤30s,unref 后 wizard main 一退桥就退,扩展再发心跳连不上 → 误判扩展未就绪。
154
+ * 真实 session 检索走单 job 流程,默认 unref=true(任何跑完主流程的宿主进程能退出)。
155
+ * run-all §40 onboarding-tests 用 keepBridge=true 验证 wizard 探活;
156
+ * §38 session 套件走默认 unref,语义不变。
157
+ */
158
+ keepBridge?: boolean
159
+ }
160
+
161
+ interface QueuedJob {
162
+ job: ExtensionJob
163
+ resolve: (outcome: SubmitOutcome) => void
164
+ timer: ReturnType<typeof setTimeout> | null
165
+ preferredOrigin?: string
166
+ claimedOrigin?: string
167
+ preferredClientId?: string
168
+ claimedClientId?: string
169
+ }
170
+
171
+ /**
172
+ * capability 路由(2026-09-09):/jobs 轮询体里的 capabilities(扩展 SITES 键清单)
173
+ * 从「上报即弃」升级为**路由依据**——扩展只领自己声明的站点的 job。
174
+ * 背景:同机多 Chrome(日常 + 测试/多 profile)各自装着同一扩展的不同版本时,
175
+ * 旧版扩展会领走新站点 job 并答 ok:true 空 names(cookie-names 语义里 conf 缺失
176
+ * 返 []),把「未登录」伪造成普查结论。按 site ∈ capabilities 匹配后,新站点 job
177
+ * 只会落到认识它的扩展手里;capabilities 缺省(理论上不存在,当前扩展全部上报)
178
+ * 视为 match-all,向前兼容。
179
+ */
180
+ interface ParkedPoller {
181
+ res: ServerResponse
182
+ timer: ReturnType<typeof setTimeout>
183
+ capabilities: Set<string> | null
184
+ origin: string
185
+ clientId?: string
186
+ }
187
+
188
+ /** 扩展客户端标识形状(每 SW 生命周期一个 crypto.randomUUID);导出供事件上行端点(批次 B)同链校验 */
189
+ export const CLIENT_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
190
+
191
+ function jobMatches(entry: QueuedJob, capabilities: Set<string> | null, origin: string, clientId?: string): boolean {
192
+ if (entry.preferredOrigin && entry.preferredOrigin !== origin) return false
193
+ if (entry.preferredClientId && entry.preferredClientId !== clientId) return false
194
+ return capabilities == null || capabilities.size === 0 || capabilities.has(entry.job.site)
195
+ }
196
+
197
+ function readBody(req: IncomingMessage, cap: number): Promise<string | { err: string }> {
198
+ return new Promise((resolve) => {
199
+ const chunks: Buffer[] = []
200
+ let size = 0
201
+ let done = false
202
+ req.on('data', (c: Buffer) => {
203
+ if (done) return
204
+ size += c.length
205
+ if (size > cap) {
206
+ done = true
207
+ req.removeAllListeners('data')
208
+ resolve({ err: `body 超 ${cap} 字节上限(防失控回包)` })
209
+ return
210
+ }
211
+ chunks.push(c)
212
+ })
213
+ req.on('end', () => { if (!done) { done = true; resolve(Buffer.concat(chunks).toString('utf8')) } })
214
+ req.on('error', () => { if (!done) { done = true; resolve({ err: 'body 读取失败' }) } })
215
+ })
216
+ }
217
+
218
+ export interface BridgeQueueStats {
219
+ queued: number
220
+ inFlight: number
221
+ parked: number
222
+ lastSeenMsAgo: number | null
223
+ }
224
+
225
+ /**
226
+ * 传输无关的桥作业队列(2026-09-11 抽出,founder 定案:执行环境=浏览器客户端,
227
+ * 服务端零 Chrome)——queue/inFlight/parked 三态 + 长轮询协议处理器,
228
+ * 可挂两种形态:
229
+ * - loopback 独立桥(createSessionBridge;gotry 桌面形态,扩展与本机进程同机)
230
+ * - gotry-backend 鉴权面挂载(handleMountedRequest;远程扩展经 bearer 鉴权连入,
231
+ * 浏览器客户端即执行环境,服务器只做编排/队列)
232
+ */
233
+ export interface BridgeJobQueue extends SessionJobHandle {
234
+ stats(): BridgeQueueStats
235
+ /** 完整桥协议(loopback 独立桥形态;/results/:jobId 路径式) */
236
+ handleRequest(req: IncomingMessage, res: ServerResponse): void
237
+ /** 挂载形态(/v1/session/bridge 前缀;/results?jobId= 查询式;bearer 鉴权由宿主模块前置) */
238
+ handleMountedRequest(req: IncomingMessage, res: ServerResponse): void
239
+ /**
240
+ * 开机恢复(批次 A;仅 store 注入形态存在,纯内存无此方法):
241
+ * queued→重排 / claimed 已超时→unresolved+节律记账 / claimed 未超时→复活 inFlight。
242
+ * gotry-backend 会话模块启动路径调用(见 backend/modules/session-search.ts)。
243
+ */
244
+ recoverOnBoot?(): BridgeRecoverySummary
245
+ }
246
+
247
+ export interface BridgeJobQueueOptions {
248
+ /** 信任的扩展 Origin 白名单;缺省 EXTENSION_ORIGINS(unpacked + 商店版双通道) */
249
+ extensionOrigins?: readonly string[]
250
+ now?: () => number
251
+ /** 长轮询 parked 定时器是否 unref(独立桥惰性形态用;gotry-backend 常驻服务无需) */
252
+ keepBridge?: boolean
253
+ /**
254
+ * 可选持久账本(DI,批次 A「桥作业账本化」2026-10-01):缺省 undefined = 现状纯内存
255
+ * (桌面 loopback 形态零变化);gotry-backend 挂载路径传 bridge-ledger 的 SQLite 实现,
256
+ * 队列/在飞/节律/客户端注册落 <stateRoot>/gotry-state/bridge.db,重启经 recoverOnBoot 收敛。
257
+ */
258
+ store?: BridgeJobStore
259
+ }
260
+
261
+ /**
262
+ * 桥作业持久账本契约(实现方:capabilities/session/bridge-ledger.ts)。
263
+ * 只承载**控制状态**——job 元数据、回包、节律、客户端能力清单;协议面不存在
264
+ * cookie 值字段(§38 红线),账本零凭据。时间戳一律 epoch 毫秒。
265
+ */
266
+ export interface BridgeJobStore {
267
+ /** write-before-submit:入队前先落 queued;**抛错 = 提交失败,调用方不得入队派发** */
268
+ insertQueuedJob(job: ExtensionJob, timeoutAt: number): void
269
+ /** 领取(/jobs 派发与 dispatchToParked 两处):claimed + 领取者,attempts 递增 */
270
+ markClaimed(jobId: string, claim: { origin: string; clientId?: string }): void
271
+ markResolved(jobId: string, result: ExtensionJobResult): void
272
+ /** 超时/对账结算:曾被领取却未拿到善果终态(带回包则由 recordOrphanResult 落 result) */
273
+ markUnresolved(jobId: string, note?: { reason: string }): void
274
+ /** 从未被领取的终态作废(零站点花费) */
275
+ markVoid(jobId: string): void
276
+ /** 孤儿回包落库(未知 jobId 的回包内容以 unresolved 保留,供对账) */
277
+ recordOrphanResult(jobId: string, result: ExtensionJobResult): void
278
+ /** bridge_clients upsert(/jobs 轮询体 clientId + capabilities 到达即刷新) */
279
+ upsertClient(clientId: string, origin: string, capabilities: readonly string[]): void
280
+ /** cookie-names 命中票据名(名字级)→ 记客户端在该站点持有登录态 */
281
+ recordClientLoginSite(clientId: string, site: string): void
282
+ /** 站点导航类作业提交/领取:记命中时刻并推冷却 */
283
+ noteSiteHit(site: string): void
284
+ /** 超时结算冷却(claimed 超时:站点可能已被打) */
285
+ applyCooldown(site: string): void
286
+ pacingOf(site: string): { lastHitAt: number; cooldownUntil: number } | null
287
+ /** recoverOnBoot 输入:未终态(queued/claimed)作业投影 */
288
+ listPendingJobs(): Array<{
289
+ id: string
290
+ kind: string
291
+ site: string
292
+ payload_json: string
293
+ status: 'queued' | 'claimed'
294
+ claimed_by_client: string | null
295
+ claimed_origin: string | null
296
+ timeout_at: number | null
297
+ attempts: number
298
+ }>
299
+ }
300
+
301
+ /** recoverOnBoot 结算摘要(挂载路径启动日志/测试断言面) */
302
+ export interface BridgeRecoverySummary {
303
+ /** queued 且未过期 → 重新入队(迟到回包仍可 resolved 落账) */
304
+ requeued: number
305
+ /** claimed 且未超时 → 复活进 inFlight(扩展若已领取,回包直达 resolved) */
306
+ rehydratedInFlight: number
307
+ /** claimed 且 timeout_at 已过 → unresolved + 节律记账 */
308
+ settledUnresolved: number
309
+ /** queued 且 timeout_at 已过(未被领取即过期)→ void,零花费不再派发 */
310
+ voidedStale: number
311
+ }
312
+
313
+ /** 站点导航类作业(会真实打开供应商页面的 kind;cookie-names 只读不导航) */
314
+ const NAVIGATION_JOB_KINDS: ReadonlySet<ExtensionJobKind> = new Set(['search', 'open-login'])
315
+ export function isNavigationJobKind(kind: ExtensionJobKind): boolean {
316
+ return NAVIGATION_JOB_KINDS.has(kind)
317
+ }
318
+
319
+ export function createBridgeJobQueue(opts: BridgeJobQueueOptions = {}): BridgeJobQueue {
320
+ const extensionOrigins = new Set(opts.extensionOrigins ?? EXTENSION_ORIGINS)
321
+ const now = opts.now ?? Date.now
322
+ const store = opts.store
323
+ const queue: QueuedJob[] = []
324
+ const inFlight = new Map<string, QueuedJob>()
325
+ const parked: ParkedPoller[] = []
326
+ let lastSeenAt = 0
327
+ let closed = false
328
+ const extensionConnected = (): boolean => lastSeenAt > 0 && now() - lastSeenAt < EXTENSION_CONNECTED_WINDOW_MS
329
+
330
+ /**
331
+ * 账本写兜底:持久面故障不得打断桥协议主路径(write-before-submit 的 INSERT 除外,
332
+ * 它在 submit 里直接决定提交成败)。stderr 留痕,作业照常流转。
333
+ */
334
+ const record = (label: string, write: () => void): void => {
335
+ if (!store) return
336
+ try {
337
+ write()
338
+ } catch (e) {
339
+ process.stderr.write(`[bridge] 账本写入失败(${label}): ${e instanceof Error ? e.message : String(e)}\n`)
340
+ }
341
+ }
342
+
343
+ /** 领走首个 capability 匹配的 job(移入 inFlight——回包按 jobId 路由,两处都可找到) */
344
+ function takeMatching(capabilities: Set<string> | null, origin: string, clientId?: string): QueuedJob | null {
345
+ const idx = queue.findIndex((q) => jobMatches(q, capabilities, origin, clientId))
346
+ if (idx < 0) return null
347
+ const [next] = queue.splice(idx, 1)
348
+ next.claimedOrigin = origin
349
+ next.claimedClientId = clientId
350
+ inFlight.set(next.job.jobId, next)
351
+ record('markClaimed', () => store!.markClaimed(next.job.jobId, { origin, clientId }))
352
+ if (isNavigationJobKind(next.job.kind)) record('noteSiteHit', () => store!.noteSiteHit(next.job.site))
353
+ return next
354
+ }
355
+
356
+ /** 扩展回包:queued 与 inFlight 两处都可寻址(job 执行失败 = result.ok:false,桥自身不吞) */
357
+ function resolveJob(jobId: string, parsed: ExtensionJobResult): void {
358
+ const inFlightHit = inFlight.get(jobId)
359
+ if (inFlightHit) {
360
+ inFlight.delete(jobId)
361
+ if (inFlightHit.timer) clearTimeout(inFlightHit.timer)
362
+ record('markResolved', () => store!.markResolved(jobId, parsed))
363
+ record('client-login-site', () => {
364
+ const names = Array.isArray(parsed.names) ? parsed.names : []
365
+ if (parsed.ok === true && inFlightHit.job.kind === 'cookie-names' && names.length > 0 && inFlightHit.claimedClientId) {
366
+ store!.recordClientLoginSite(inFlightHit.claimedClientId, inFlightHit.job.site)
367
+ }
368
+ })
369
+ inFlightHit.resolve({ ok: true, result: parsed, origin: inFlightHit.claimedOrigin, clientId: inFlightHit.claimedClientId })
370
+ return
371
+ }
372
+ const idx = queue.findIndex((q) => q.job.jobId === jobId)
373
+ if (idx < 0) return
374
+ const [entry] = queue.splice(idx, 1)
375
+ if (entry.timer) clearTimeout(entry.timer)
376
+ record('markResolved', () => store!.markResolved(jobId, parsed))
377
+ entry.resolve({ ok: true, result: parsed })
378
+ }
379
+
380
+ /** 新 job 入队时即时派发给 capability 匹配的 parked 取活者(否则要白等一个 20s 长轮询周期) */
381
+ function dispatchToParked(entry: QueuedJob): boolean {
382
+ const i = parked.findIndex((p) => jobMatches(entry, p.capabilities, p.origin, p.clientId))
383
+ if (i < 0) return false
384
+ const [p] = parked.splice(i, 1)
385
+ clearTimeout(p.timer)
386
+ entry.claimedOrigin = p.origin
387
+ entry.claimedClientId = p.clientId
388
+ inFlight.set(entry.job.jobId, entry)
389
+ record('markClaimed', () => store!.markClaimed(entry.job.jobId, { origin: p.origin, clientId: p.clientId }))
390
+ if (isNavigationJobKind(entry.job.kind)) record('noteSiteHit', () => store!.noteSiteHit(entry.job.site))
391
+ p.res.statusCode = 200
392
+ p.res.setHeader('content-type', 'application/json')
393
+ p.res.end(JSON.stringify({ job: entry.job }))
394
+ return true
395
+ }
396
+
397
+ const stats = (): BridgeQueueStats => ({
398
+ queued: queue.length,
399
+ inFlight: inFlight.size,
400
+ parked: parked.length,
401
+ lastSeenMsAgo: lastSeenAt > 0 ? now() - lastSeenAt : null,
402
+ })
403
+
404
+ /**
405
+ * 结算器(submit 与 recoverOnBoot 复活条目共用):内存三态清理 + 账本终态落笔。
406
+ * 幂等护栏(settled 标记)防 waitTimer 兜底在 resolved 之后二次结算把账本写花。
407
+ * 超时结算规则(批次 A):曾被领取→unresolved+导航类记节律冷却(站点可能已被打);
408
+ * 从未被领取→void(零花费)。extension-not-connected 同为零花费 void;
409
+ * close 作废时已领取条目**留在 claimed**——持久记录交给下一轮 recoverOnBoot 收敛
410
+ * (扩展迟到回包仍可经 recordOrphanResult 对账,不虚构终态)。
411
+ */
412
+ function armSettle(entry: QueuedJob, deliver?: (outcome: SubmitOutcome) => void): (outcome: SubmitOutcome) => void {
413
+ let settled = false
414
+ return (outcome: SubmitOutcome): void => {
415
+ if (settled) return
416
+ settled = true
417
+ if (entry.timer) clearTimeout(entry.timer)
418
+ const i = queue.findIndex((q) => q.job.jobId === entry.job.jobId)
419
+ if (i >= 0) queue.splice(i, 1)
420
+ const wasClaimed = inFlight.delete(entry.job.jobId) || entry.claimedOrigin !== undefined
421
+ // outcome.ok 的 resolved 已在 resolveJob 落账;这里只结算失败终态
422
+ if (store && !outcome.ok) {
423
+ if (outcome.reason === 'timeout') {
424
+ if (wasClaimed) {
425
+ record('markUnresolved', () => store!.markUnresolved(entry.job.jobId, { reason: 'timeout' }))
426
+ if (isNavigationJobKind(entry.job.kind)) record('applyCooldown', () => store!.applyCooldown(entry.job.site))
427
+ } else {
428
+ record('markVoid', () => store!.markVoid(entry.job.jobId))
429
+ }
430
+ } else if (outcome.reason === 'extension-not-connected') {
431
+ record('markVoid', () => store!.markVoid(entry.job.jobId))
432
+ } else if (!wasClaimed) {
433
+ record('markVoid', () => store!.markVoid(entry.job.jobId))
434
+ }
435
+ }
436
+ deliver?.(outcome)
437
+ }
438
+ }
439
+
440
+ /**
441
+ * 开机恢复(仅 store 形态;纯内存 no-op):
442
+ * queued 未过期 → 重新入队(可被派发,迟到善果仍 resolved 落账);
443
+ * queued 已过期 → void(未被领取即过期,零花费不再派发);
444
+ * claimed 未超时 → 复活进 inFlight(重启丢在飞作业的修复面:扩展若已领取,回包直达);
445
+ * claimed 已超时 → unresolved + 导航类记节律冷却(该站点可能已被打)。
446
+ * payload 损坏(id 不匹配/非 JSON)按不可对账处理:queued→void / claimed→unresolved。
447
+ */
448
+ function recoverOnBoot(): BridgeRecoverySummary {
449
+ const summary: BridgeRecoverySummary = { requeued: 0, rehydratedInFlight: 0, settledUnresolved: 0, voidedStale: 0 }
450
+ if (!store || closed) return summary
451
+ let pending: ReturnType<BridgeJobStore['listPendingJobs']> = []
452
+ try {
453
+ pending = store.listPendingJobs()
454
+ } catch (e) {
455
+ process.stderr.write(`[bridge] 账本恢复读取失败:${e instanceof Error ? e.message : String(e)}\n`)
456
+ return summary
457
+ }
458
+ for (const row of pending) {
459
+ const expired = row.timeout_at !== null && row.timeout_at <= now()
460
+ let job: ExtensionJob | null = null
461
+ try {
462
+ const parsed = JSON.parse(row.payload_json) as ExtensionJob
463
+ if (parsed && typeof parsed.jobId === 'string' && parsed.jobId === row.id) job = parsed
464
+ } catch { /* 损坏 payload 按不可对账处理 */ }
465
+ if (row.status === 'claimed') {
466
+ if (!expired && job) {
467
+ const entry: QueuedJob = {
468
+ job,
469
+ resolve: null as unknown as (o: SubmitOutcome) => void,
470
+ timer: null,
471
+ claimedOrigin: row.claimed_origin ?? undefined,
472
+ claimedClientId: row.claimed_by_client ?? undefined,
473
+ }
474
+ const settle = armSettle(entry)
475
+ entry.resolve = settle
476
+ const remaining = row.timeout_at !== null ? Math.max(row.timeout_at - now(), 1) : 30_000
477
+ entry.timer = setTimeout(() => settle({ ok: false, reason: 'timeout', summary: `恢复作业 ${row.id} 超时未回包` }), remaining)
478
+ inFlight.set(row.id, entry)
479
+ summary.rehydratedInFlight++
480
+ } else {
481
+ record('markUnresolved', () => store!.markUnresolved(row.id, { reason: 'timeout' }))
482
+ if (isNavigationJobKind(row.kind as ExtensionJobKind)) record('applyCooldown', () => store!.applyCooldown(row.site))
483
+ summary.settledUnresolved++
484
+ }
485
+ } else {
486
+ if (!expired && job) {
487
+ const entry: QueuedJob = {
488
+ job,
489
+ resolve: null as unknown as (o: SubmitOutcome) => void,
490
+ timer: null,
491
+ }
492
+ const settle = armSettle(entry)
493
+ entry.resolve = settle
494
+ const remaining = row.timeout_at !== null ? Math.max(row.timeout_at - now(), 1) : 30_000
495
+ entry.timer = setTimeout(() => settle({ ok: false, reason: 'timeout', summary: `恢复作业 ${row.id} 超时未回包` }), remaining)
496
+ if (!dispatchToParked(entry)) queue.push(entry)
497
+ summary.requeued++
498
+ } else {
499
+ record('markVoid', () => store!.markVoid(row.id))
500
+ summary.voidedStale++
501
+ }
502
+ }
503
+ }
504
+ return summary
505
+ }
506
+
507
+ /**
508
+ * 协议处理器工厂:prefix='' 为独立桥形态(路径式 results);prefix='/v1/session/bridge'
509
+ * 为挂载形态(查询式 results——gotry-backend 内核只支持精确路由,jobId 走 query)。
510
+ * Origin 白名单校验内置于两种形态(远程形态同样强制:网页跨域请求必带邪恶 Origin)。
511
+ */
512
+ function makeHandler(prefix: string, resultsViaQuery: boolean, statusExtra?: () => Record<string, unknown>) {
513
+ return (req: IncomingMessage, res: ServerResponse): void => {
514
+ if (closed) { res.statusCode = 503; res.end(); return }
515
+ const urlPath = (req.url ?? '').split('?')[0]
516
+ const path = prefix && urlPath.startsWith(prefix) ? urlPath.slice(prefix.length) : urlPath
517
+ const origin = req.headers.origin
518
+ const isPost = req.method === 'POST'
519
+ const originTrusted = typeof origin === 'string' && extensionOrigins.has(origin)
520
+ // 网页侧请求必带 Origin;桥端点只信任白名单内的 chrome-extension:// 源(诊断 GET 面放行)
521
+ if (isPost && !originTrusted) {
522
+ res.statusCode = 403
523
+ res.setHeader('content-type', 'application/json')
524
+ res.end(JSON.stringify({ ok: false, error: 'origin 不在桥白名单' }))
525
+ return
526
+ }
527
+ // 心跳:扩展平时只走 /jobs 长轮询(健康只在启动探测时 ping 一次)——
528
+ // 一切携带白名单扩展 Origin 的请求都刷新 lastSeen,否则 45s 后误判扩展掉线;
529
+ // 无 Origin 的诊断 GET(/health /status)不记心跳,curl 探活不能伪造「扩展在线」
530
+ if (originTrusted) {
531
+ lastSeenAt = now()
532
+ }
533
+ const finish = (code: number, body: unknown): void => {
534
+ res.statusCode = code
535
+ res.setHeader('content-type', 'application/json')
536
+ res.end(JSON.stringify(body))
537
+ }
538
+ if (req.method === 'GET' && path === '/health') {
539
+ finish(200, { ok: true, service: 'gotry-session-bridge', protocol: BRIDGE_PROTOCOL })
540
+ return
541
+ }
542
+ if (req.method === 'GET' && path === '/status') {
543
+ finish(200, {
544
+ ok: true, protocol: BRIDGE_PROTOCOL, ...stats(),
545
+ extensionConnected: extensionConnected(),
546
+ ...(statusExtra ? statusExtra() : {}),
547
+ })
548
+ return
549
+ }
550
+ if (isPost && path === '/jobs') {
551
+ // 轮询体携带 {extensionVersion, capabilities: SITES 键清单}——按站点 capability
552
+ // 路由(见 ParkedPoller 注);体缺失/非 JSON 视为 match-all,向前兼容旧扩展。
553
+ void readBody(req, 64 * 1024).then((body) => {
554
+ const pollerInfo = ((): { capabilities: Set<string> | null; clientId?: string } => {
555
+ if (typeof body !== 'string') return { capabilities: null }
556
+ try {
557
+ const parsed = JSON.parse(body) as { capabilities?: unknown; clientId?: unknown }
558
+ return {
559
+ capabilities: Array.isArray(parsed.capabilities) ? new Set(parsed.capabilities.filter((c): c is string => typeof c === 'string')) : null,
560
+ ...(typeof parsed.clientId === 'string' && CLIENT_ID_RE.test(parsed.clientId) ? { clientId: parsed.clientId } : {}),
561
+ }
562
+ } catch {
563
+ return { capabilities: null }
564
+ }
565
+ })()
566
+ // 客户端注册落账(store 形态):轮询即刷新 origin/capabilities/last_seen(bridge_clients upsert)
567
+ if (pollerInfo.clientId) {
568
+ record('upsertClient', () => store!.upsertClient(pollerInfo.clientId!, origin as string, [...(pollerInfo.capabilities ?? [])]))
569
+ }
570
+ const next = takeMatching(pollerInfo.capabilities, origin as string, pollerInfo.clientId)
571
+ if (next) { finish(200, { job: next.job }); return }
572
+ // 长轮询:hold ≤ JOBS_LONG_POLL_MS(必须 < MV3 SW 30s 存活窗口,每次响应都续命);
573
+ // 新 job 提交时即时唤醒 parked 取活者(见 dispatchToParked)
574
+ const poller: ParkedPoller = { res, timer: null as unknown as ReturnType<typeof setTimeout>, capabilities: pollerInfo.capabilities, origin: origin as string, clientId: pollerInfo.clientId }
575
+ const parkTimer = setTimeout(() => {
576
+ const i = parked.findIndex((p) => p.res === res)
577
+ if (i >= 0) parked.splice(i, 1)
578
+ finish(200, { job: null })
579
+ }, JOBS_LONG_POLL_MS)
580
+ poller.timer = parkTimer
581
+ // 默认桥是惰性能力:外部扩展的 parked 轮询不得在主流程结束后钉住 CLI。
582
+ // keepBridge wizard 反过来要靠它守住进程,因此只对默认形态 unref。
583
+ if (!opts.keepBridge) parkTimer.unref()
584
+ parked.push(poller)
585
+ res.on('close', () => {
586
+ const i = parked.findIndex((p) => p.res === res)
587
+ if (i >= 0) parked.splice(i, 1)
588
+ clearTimeout(parkTimer)
589
+ })
590
+ })
591
+ return
592
+ }
593
+ const isResults = isPost && (resultsViaQuery ? path === '/results' : path.startsWith('/results/'))
594
+ if (isResults) {
595
+ const jobId = resultsViaQuery
596
+ ? new URL(req.url ?? '', 'http://bridge.local').searchParams.get('jobId') ?? ''
597
+ : decodeURIComponent(path.slice('/results/'.length))
598
+ if (!jobId) { finish(400, { ok: false, error: '缺 jobId' }); return }
599
+ void readBody(req, MAX_RESULT_BODY_BYTES).then((body) => {
600
+ if (typeof body !== 'string') { finish(400, { ok: false, error: body.err }); return }
601
+ let parsed: ExtensionJobResult
602
+ try {
603
+ parsed = JSON.parse(body) as ExtensionJobResult
604
+ } catch {
605
+ finish(400, { ok: false, error: '回包不是 JSON' })
606
+ return
607
+ }
608
+ const assigned = inFlight.get(jobId)
609
+ if (assigned?.claimedOrigin && assigned.claimedOrigin !== origin) {
610
+ finish(403, { ok: false, error: '作业由另一扩展 Origin 领取' })
611
+ return
612
+ }
613
+ if (assigned?.claimedClientId && assigned.claimedClientId !== req.headers['x-gotry-client-id']) {
614
+ finish(403, { ok: false, error: '作业由另一扩展客户端领取' })
615
+ return
616
+ }
617
+ // 未知 jobId(批次 A):不再 200 静默丢弃——404 明示,孤儿回包内容落账
618
+ // (status='unresolved',result_json 保留)供对账。扩展 postResult 本就有
619
+ // catch 且不检查状态码(extension/background.js),协议兼容零改动。
620
+ const known = assigned !== undefined || queue.some((q) => q.job.jobId === jobId)
621
+ if (!known) {
622
+ record('orphan-result', () => store!.recordOrphanResult(jobId, parsed))
623
+ finish(404, { ok: false, error: 'unknown-job' })
624
+ return
625
+ }
626
+ finish(200, { ok: true })
627
+ resolveJob(jobId, parsed)
628
+ })
629
+ return
630
+ }
631
+ finish(404, { ok: false, error: 'unknown endpoint' })
632
+ }
633
+ }
634
+
635
+ const queueHandle: BridgeJobQueue = {
636
+ port: 0,
637
+ extensionConnected,
638
+ stats,
639
+ handleRequest: makeHandler('', false),
640
+ handleMountedRequest: makeHandler('/v1/session/bridge', true),
641
+ ...(store ? { recoverOnBoot } : {}),
642
+ submit(job, submitOpts = {}) {
643
+ const extensionWaitMs = submitOpts.extensionWaitMs ?? DEFAULT_EXTENSION_WAIT_MS
644
+ const timeoutMs = submitOpts.timeoutMs ?? job.timeoutMs ?? 30_000
645
+ const full: ExtensionJob = { jobId: randomUUID(), ...job }
646
+ return new Promise<SubmitOutcome>((resolve) => {
647
+ if (closed) { resolve({ ok: false, reason: 'bridge-unavailable', summary: '扩展桥已关闭' }); return }
648
+ if (submitOpts.preferredOrigin && !extensionOrigins.has(submitOpts.preferredOrigin)) {
649
+ resolve({ ok: false, reason: 'bridge-unavailable', summary: '指定扩展 Origin 不在桥白名单' })
650
+ return
651
+ }
652
+ if (submitOpts.preferredClientId && !CLIENT_ID_RE.test(submitOpts.preferredClientId)) {
653
+ resolve({ ok: false, reason: 'bridge-unavailable', summary: '指定扩展客户端标识无效' })
654
+ return
655
+ }
656
+ // write-before-submit(store 形态):先落 queued 再入队/派发——持久面失败即提交失败,
657
+ // 绝不派发无账作业。桌面纯内存形态此步整体跳过,行为零变化。
658
+ if (store) {
659
+ try {
660
+ store.insertQueuedJob(full, now() + timeoutMs)
661
+ } catch (e) {
662
+ resolve({ ok: false, reason: 'bridge-unavailable', summary: `桥账本写入失败,作业不入队:${e instanceof Error ? e.message : String(e)}` })
663
+ return
664
+ }
665
+ if (isNavigationJobKind(full.kind)) record('noteSiteHit', () => store!.noteSiteHit(full.site))
666
+ }
667
+ const entry: QueuedJob = { job: full, resolve: null as unknown as (o: SubmitOutcome) => void, timer: null, preferredOrigin: submitOpts.preferredOrigin, preferredClientId: submitOpts.preferredClientId }
668
+ const settle = armSettle(entry, resolve)
669
+ entry.resolve = settle
670
+ entry.timer = setTimeout(() => {
671
+ settle({ ok: false, reason: 'timeout', summary: `扩展未在 ${timeoutMs}ms 内回包(标签页无嗅探命中或扩展已停用)` })
672
+ }, timeoutMs)
673
+ if (!dispatchToParked(entry)) queue.push(entry)
674
+ // 扩展未上线:有界等待(桥刚拉起时扩展 ≤5s 即连上);宽限过仍无 → 立即 no-spend 失败,不空耗
675
+ let waited = 0
676
+ const waitTimer = setInterval(() => {
677
+ waited += 250
678
+ if (queueHandle.extensionConnected()) { clearInterval(waitTimer); return }
679
+ if (waited >= extensionWaitMs) {
680
+ clearInterval(waitTimer)
681
+ settle({
682
+ ok: false,
683
+ reason: 'extension-not-connected',
684
+ // D-24 自适应文案(issue #117):按本地通道是否落位决定是否提及开发者模式
685
+ summary: needsExtensionSummary(),
686
+ })
687
+ }
688
+ }, 250)
689
+ })
690
+ },
691
+ close: () =>
692
+ new Promise<void>((resolve) => {
693
+ closed = true
694
+ const voided: SubmitOutcome = { ok: false, reason: 'bridge-unavailable', summary: '扩展桥关闭,任务作废(零花费)' }
695
+ for (const q of queue.splice(0)) {
696
+ if (q.timer) clearTimeout(q.timer)
697
+ q.resolve(voided)
698
+ }
699
+ for (const [, q] of inFlight) {
700
+ if (q.timer) clearTimeout(q.timer)
701
+ q.resolve(voided)
702
+ }
703
+ inFlight.clear()
704
+ for (const p of parked.splice(0)) {
705
+ clearTimeout(p.timer)
706
+ p.res.destroy()
707
+ }
708
+ resolve()
709
+ }),
710
+ }
711
+ return queueHandle
712
+ }
713
+
714
+ /** 创 Bridge 服务(测试可直接调;生产走 getOrCreateSessionBridge 懒单例) */
715
+ export async function createSessionBridge(opts: SessionBridgeOptions = {}): Promise<{ ok: true; bridge: SessionJobHandle } | { ok: false; summary: string }> {
716
+ const ports = opts.ports ?? [...BRIDGE_PORTS]
717
+ const queueHandle = createBridgeJobQueue(opts)
718
+ let port = 0
719
+
720
+ const server = createServer((req, res) => queueHandle.handleRequest(req, res))
721
+ server.on('connection', (socket) => {
722
+ // server.unref() 只解开监听器;已接受的扩展长轮询 socket 仍会引用事件循环。
723
+ if (!opts.keepBridge) socket.unref()
724
+ })
725
+
726
+ const bridge: SessionJobHandle = {
727
+ port: 0,
728
+ extensionConnected: queueHandle.extensionConnected,
729
+ submit: (job, submitOpts) => queueHandle.submit(job, submitOpts),
730
+ close: () =>
731
+ new Promise<void>((resolve) => {
732
+ void queueHandle.close().then(() => {
733
+ server.close(() => resolve())
734
+ server.closeAllConnections?.()
735
+ })
736
+ }),
737
+ }
738
+
739
+ const listenAt = (p: number): Promise<void> =>
740
+ new Promise((resolve, reject) => {
741
+ const onError = (e: Error): void => { server.off('listening', onListening); reject(e) }
742
+ const onListening = (): void => { server.off('error', onError); resolve() }
743
+ server.once('error', onError)
744
+ server.once('listening', onListening)
745
+ server.listen(p, '127.0.0.1')
746
+ })
747
+
748
+ for (const p of ports) {
749
+ try {
750
+ await listenAt(p)
751
+ if (!opts.keepBridge) {
752
+ // 桥是后台服务:不得钉住事件循环——否则任何跑完主流程的宿主进程(如 smoke/CI)无法退出
753
+ server.unref()
754
+ }
755
+ // keepBridge 模式下:wizard 期间要持续监听扩展心跳(30s 周期),
756
+ // 不 unref 让 server 钉事件循环,wizard main 退时再显式 close 释放。
757
+ port = (server.address() as { port: number }).port
758
+ bridge.port = port
759
+ return { ok: true, bridge }
760
+ } catch {
761
+ // 端口被占(如另一个 gotry 实例)——试端口池下一个
762
+ }
763
+ }
764
+ await queueHandle.close().catch(() => { /* ignore */ })
765
+ return {
766
+ ok: false,
767
+ summary: `扩展桥端口池 ${ports.join('/')} 全部被占(多为并行 gotry 实例);不影响官方通道 gotry_flyai_search。关闭多余实例后重试。`,
768
+ }
769
+ }
770
+
771
+ /* ---------- 生产懒单例(进程内随 dsh 存活;失败可重试) ---------- */
772
+
773
+ let singleton: { ok: true; bridge: SessionJobHandle } | { ok: false; summary: string } | null = null
774
+
775
+ export async function getOrCreateSessionBridge(opts: SessionBridgeOptions = {}): Promise<{ ok: true; bridge: SessionJobHandle } | { ok: false; summary: string }> {
776
+ if (singleton) return singleton
777
+ const created = await createSessionBridge(opts)
778
+ const result = created.ok
779
+ ? ({ ok: true, bridge: created.bridge } as { ok: true; bridge: SessionJobHandle })
780
+ : ({ ok: false, summary: created.summary } as { ok: false; summary: string })
781
+ singleton = result
782
+ return result
783
+ }
784
+
785
+ /** 测试隔离:丢弃单例(端口是进程内资源,close 后端口池立即可复用) */
786
+ export async function __resetSessionBridgeForTest(): Promise<void> {
787
+ if (singleton?.ok) await singleton.bridge.close().catch(() => { /* ignore */ })
788
+ singleton = null
789
+ }
790
+
791
+ /** 测试注入:以临时端口的桥替换单例(channel 层测试不碰真实端口池) */
792
+ export function __setSessionBridgeForTest(bridge: SessionJobHandle | null): void {
793
+ singleton = bridge ? { ok: true, bridge } : null
794
+ }