@danceiny/gotry 0.0.1-rc.21 → 0.0.1-rc.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (327) hide show
  1. package/README.md +102 -194
  2. package/README.zh-CN.md +115 -210
  3. package/bin/gotry-backend.js +53 -0
  4. package/bin/gotry-bootstrap.js +466 -35
  5. package/bin/gotry-inner.js +255 -36
  6. package/bin/gotry-process-liveness.d.ts +27 -0
  7. package/bin/gotry-process-liveness.js +97 -0
  8. package/bin/gotry-runtime-resolution.d.ts +1 -1
  9. package/bin/gotry-runtime-resolution.js +2 -2
  10. package/client/client.js +174 -0
  11. package/cordis.gotry-patch.yml +70 -93
  12. package/dist/capabilities/agent-reach-deep.js +45 -122
  13. package/dist/capabilities/agent-reach.js +63 -148
  14. package/dist/capabilities/anything.js +89 -75
  15. package/dist/capabilities/artifacts.js +123 -104
  16. package/dist/capabilities/channel-health.js +65 -45
  17. package/dist/capabilities/channel-registry.js +108 -87
  18. package/dist/capabilities/doctor.js +357 -235
  19. package/dist/capabilities/effect.js +223 -309
  20. package/dist/capabilities/fact-log.js +28 -14
  21. package/dist/capabilities/flyai.js +245 -156
  22. package/dist/capabilities/ground-transfer.js +509 -0
  23. package/dist/capabilities/hbcli.js +144 -148
  24. package/dist/capabilities/hotelbyte-transaction.js +194 -0
  25. package/dist/capabilities/incident-log.js +131 -61
  26. package/dist/capabilities/model-override.js +30 -10
  27. package/dist/capabilities/opensky.js +54 -43
  28. package/dist/capabilities/resilience.js +50 -32
  29. package/dist/capabilities/session/action-cache.js +50 -44
  30. package/dist/capabilities/session/adapters/ctrip-flight.js +119 -62
  31. package/dist/capabilities/session/adapters/ctrip-hotel.js +125 -110
  32. package/dist/capabilities/session/adapters/dida-portal.js +206 -0
  33. package/dist/capabilities/session/adapters/meituan-local.js +35 -54
  34. package/dist/capabilities/session/adapters/rail-12306.js +169 -112
  35. package/dist/capabilities/session/benchmark.js +85 -149
  36. package/dist/capabilities/session/extension-bridge.js +320 -259
  37. package/dist/capabilities/session/extension-channel.js +57 -76
  38. package/dist/capabilities/session/extension-distribution.js +115 -119
  39. package/dist/capabilities/session/extract.js +31 -21
  40. package/dist/capabilities/session/golden-score.js +55 -33
  41. package/dist/capabilities/session/health-watch.js +146 -154
  42. package/dist/capabilities/session/read-guard.js +55 -47
  43. package/dist/capabilities/session/static-flight-golden.js +49 -36
  44. package/dist/capabilities/session/transport.js +54 -61
  45. package/dist/capabilities/session/wizard.js +63 -93
  46. package/dist/capabilities/session-consent.js +143 -47
  47. package/dist/capabilities/session-login.js +95 -126
  48. package/dist/capabilities/session-search.js +391 -253
  49. package/dist/capabilities/sponsor-plugin.js +341 -0
  50. package/dist/capabilities/visa-policy.js +87 -73
  51. package/dist/capabilities/weather.js +163 -235
  52. package/dist/scripts/action-cache-tests.js +24 -59
  53. package/dist/scripts/agent-planning-turn-deadline-e2e.js +91 -142
  54. package/dist/scripts/agent-planning-turn-deadline-tests.js +80 -176
  55. package/dist/scripts/agent-reach-deep-tests.js +30 -40
  56. package/dist/scripts/agent-reach-tests.js +26 -18
  57. package/dist/scripts/agent-reach-wrapper-tests.js +46 -50
  58. package/dist/scripts/anything-tests.js +58 -62
  59. package/dist/scripts/artifact-client-contract-tests.js +69 -0
  60. package/dist/scripts/artifacts-capability-tests.js +235 -0
  61. package/dist/scripts/async-collect.js +26 -15
  62. package/dist/scripts/benchmark-environment-bridge-e2e.js +287 -684
  63. package/dist/scripts/benchmark-environment-bridge-tests.js +818 -3123
  64. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +66 -279
  65. package/dist/scripts/booking-copilot-availability-policy-tests.js +153 -1257
  66. package/dist/scripts/booking-copilot-bin-proof-tests.js +22 -45
  67. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +64 -76
  68. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +69 -199
  69. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +393 -759
  70. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +33 -75
  71. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +60 -110
  72. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +28 -66
  73. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +72 -271
  74. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +73 -275
  75. package/dist/scripts/booking-copilot-runtime-proof-tests.js +784 -3398
  76. package/dist/scripts/booking-copilot-server-proof-tests.js +63 -168
  77. package/dist/scripts/booking-copilot-startup-proof-tests.js +38 -124
  78. package/dist/scripts/booking-executor-tests.js +80 -0
  79. package/dist/scripts/booking-recovery-chain-tests.js +182 -0
  80. package/dist/scripts/booking-saga-tests.js +61 -123
  81. package/dist/scripts/booking-surface-contract-proof-tests.js +152 -1167
  82. package/dist/scripts/bootstrap-tests.js +1383 -128
  83. package/dist/scripts/build-changelog.js +89 -130
  84. package/dist/scripts/build-metrics-report.js +135 -143
  85. package/dist/scripts/changelog-tests.js +35 -82
  86. package/dist/scripts/channel-probe-tests.js +44 -116
  87. package/dist/scripts/channel-probe.js +84 -167
  88. package/dist/scripts/channel-registry-tests.js +97 -185
  89. package/dist/scripts/companion-tests.js +25 -70
  90. package/dist/scripts/dida-cookie-probe.js +87 -0
  91. package/dist/scripts/dida-sw-probe.js +114 -0
  92. package/dist/scripts/diff-test.js +17 -12
  93. package/dist/scripts/doctor-tests.js +428 -120
  94. package/dist/scripts/dsh-artifact-e2e.js +150 -0
  95. package/dist/scripts/dsh-artifact-web-e2e.js +443 -0
  96. package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
  97. package/dist/scripts/dsh-runtime-closure-tests.js +110 -153
  98. package/dist/scripts/dsh-runtime-closure.js +75 -63
  99. package/dist/scripts/dsh-session-v3-migration-proof.js +106 -0
  100. package/dist/scripts/dsh-subprocess-local-proof.js +149 -0
  101. package/dist/scripts/dsh-target-closure-proof.js +178 -0
  102. package/dist/scripts/effect-tests.js +165 -552
  103. package/dist/scripts/engine-run.js +1 -3
  104. package/dist/scripts/engine-tests.js +16 -10
  105. package/dist/scripts/evaluation-cadence-tests.js +69 -242
  106. package/dist/scripts/evaluation-contract-tests.js +193 -499
  107. package/dist/scripts/extension-distribution-cli.js +19 -15
  108. package/dist/scripts/extension-distribution-tests.js +98 -211
  109. package/dist/scripts/extension-tests.js +366 -389
  110. package/dist/scripts/fact-gate-tests.js +725 -263
  111. package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
  112. package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
  113. package/dist/scripts/flight-malformed-tests.js +159 -0
  114. package/dist/scripts/flyai-tests.js +325 -68
  115. package/dist/scripts/g5-guard-tests.js +131 -0
  116. package/dist/scripts/g5-guard.js +189 -0
  117. package/dist/scripts/gen-lunar-anchors.js +106 -0
  118. package/dist/scripts/gotry-backend-kernel-tests.js +79 -0
  119. package/dist/scripts/ground-transfer-tests.js +956 -0
  120. package/dist/scripts/hbcli-e2e-tests.js +71 -90
  121. package/dist/scripts/hbcli-live-optin-tests.js +96 -0
  122. package/dist/scripts/hbcli-release-contract-tests.js +122 -0
  123. package/dist/scripts/hbcli-tests.js +41 -115
  124. package/dist/scripts/health-watch-cli.js +42 -28
  125. package/dist/scripts/hotel-date-gate-tests.js +496 -0
  126. package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
  127. package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
  128. package/dist/scripts/i18n-tests.js +16 -9
  129. package/dist/scripts/incident-tests.js +281 -87
  130. package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
  131. package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
  132. package/dist/scripts/issue-271-liveness-tests.js +547 -0
  133. package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
  134. package/dist/scripts/issue-289-model-retry-tests.js +441 -0
  135. package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
  136. package/dist/scripts/issue-338-home-city-e2e.js +577 -0
  137. package/dist/scripts/issue343-real-entry-e2e.js +160 -0
  138. package/dist/scripts/journey-tests.js +22 -27
  139. package/dist/scripts/kernel-manifest-evidence.js +109 -0
  140. package/dist/scripts/kernel-manifest-gate.js +126 -0
  141. package/dist/scripts/kernel-manifest-tests.js +212 -0
  142. package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
  143. package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
  144. package/dist/scripts/kernel-manifest.js +369 -0
  145. package/dist/scripts/ledger-repair-apply-tests.js +316 -0
  146. package/dist/scripts/ledger-repair-plan-tests.js +375 -0
  147. package/dist/scripts/ledger-tests.js +325 -340
  148. package/dist/scripts/ledger-workflow-crash.js +10 -20
  149. package/dist/scripts/map-tools-vendor-package-proof.js +206 -134
  150. package/dist/scripts/memory-capture-tests.js +242 -61
  151. package/dist/scripts/memory-decay-tests.js +18 -52
  152. package/dist/scripts/memory-lifecycle-tests.js +657 -0
  153. package/dist/scripts/memory-lifecycle.js +196 -0
  154. package/dist/scripts/memory-metrics.js +24 -12
  155. package/dist/scripts/memory-value-report.js +474 -222
  156. package/dist/scripts/memory-value-tests.js +328 -0
  157. package/dist/scripts/metrics-report-tests.js +62 -199
  158. package/dist/scripts/model-override-e2e.js +66 -105
  159. package/dist/scripts/nightly-evidence-tests.js +64 -89
  160. package/dist/scripts/nightly-evidence.js +108 -111
  161. package/dist/scripts/nudge-digest.js +39 -28
  162. package/dist/scripts/onboarding-tests.js +63 -88
  163. package/dist/scripts/opensky-check.js +18 -13
  164. package/dist/scripts/opensky-tests.js +28 -28
  165. package/dist/scripts/persona-surface-guard-tests.js +37 -12
  166. package/dist/scripts/pnpm-dsh-closure-proof.js +2 -5
  167. package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
  168. package/dist/scripts/price-drift-tests.js +126 -227
  169. package/dist/scripts/price-drift-watch.js +209 -253
  170. package/dist/scripts/probe-poi-tests.js +52 -45
  171. package/dist/scripts/product-metrics.js +187 -273
  172. package/dist/scripts/publish-preverify.js +47 -41
  173. package/dist/scripts/realtime-pricing-tests.js +37 -76
  174. package/dist/scripts/replay-async.js +20 -18
  175. package/dist/scripts/replay-real.js +16 -18
  176. package/dist/scripts/replay.js +52 -82
  177. package/dist/scripts/session-attach-diagnose.js +16 -12
  178. package/dist/scripts/session-attach-poc.js +34 -37
  179. package/dist/scripts/session-benchmark.js +66 -147
  180. package/dist/scripts/session-dida-live-e2e.js +151 -0
  181. package/dist/scripts/session-extract-tests.js +14 -35
  182. package/dist/scripts/session-login.js +14 -13
  183. package/dist/scripts/session-tests.js +545 -583
  184. package/dist/scripts/sf-live-benchmark.js +92 -110
  185. package/dist/scripts/sf-live-cli-tests.js +2 -9
  186. package/dist/scripts/sf-soft-score-tests.js +19 -60
  187. package/dist/scripts/sf-summary-tests.js +149 -0
  188. package/dist/scripts/sf-summary.js +466 -84
  189. package/dist/scripts/skeleton-check.js +16 -14
  190. package/dist/scripts/skeleton-integration-test.js +11 -10
  191. package/dist/scripts/skills-contract-tests.js +51 -46
  192. package/dist/scripts/smoke-session-gate-tests.js +12 -20
  193. package/dist/scripts/smoke.js +541 -422
  194. package/dist/scripts/sponsor-reuse-tests.js +262 -0
  195. package/dist/scripts/state-cli-tests.js +226 -101
  196. package/dist/scripts/state-cli.js +549 -212
  197. package/dist/scripts/static-golden-tests.js +46 -180
  198. package/dist/scripts/time-eval-tests.js +210 -192
  199. package/dist/scripts/travel-timeline-tests.js +29 -93
  200. package/dist/scripts/turn-handoff-collect-tests.js +46 -93
  201. package/dist/scripts/turn-handoff-collect.js +71 -99
  202. package/dist/scripts/turn-policy-tests.js +28 -29
  203. package/dist/scripts/typed-contract-canary.js +77 -162
  204. package/dist/scripts/tz-resolver-sanity.js +50 -0
  205. package/dist/scripts/tz-resolver-tests.js +584 -0
  206. package/dist/scripts/unified-tests.js +23 -19
  207. package/dist/scripts/visa-policy-tests.js +31 -44
  208. package/dist/scripts/weather-tests.js +121 -543
  209. package/dist/scripts/wish-channel-gate-tests.js +29 -86
  210. package/dist/scripts/write-gate-tests.js +522 -0
  211. package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
  212. package/dist/scripts/z3-lifecycle-tests.js +170 -0
  213. package/dist/scripts/z3-race-repeat-tests.js +39 -0
  214. package/dist/scripts/z3-race-tests.js +18 -12
  215. package/dist/src/artifact-gate.js +652 -221
  216. package/dist/src/backend/kernel.js +91 -0
  217. package/dist/src/backend/modules/booking-copilot.js +55 -0
  218. package/dist/src/backend/modules/booking-executor.js +124 -0
  219. package/dist/src/backend/modules/session-search.js +194 -0
  220. package/dist/src/benchmark-agent-conformance.js +296 -262
  221. package/dist/src/benchmark-environment-bridge.js +298 -413
  222. package/dist/src/benchmark-headless-child-diagnostics.js +93 -100
  223. package/dist/src/benchmark-tool-isolation.js +94 -67
  224. package/dist/src/bookable-facts.js +299 -174
  225. package/dist/src/booking-saga.js +64 -120
  226. package/dist/src/booking-surface/availability-policy.js +443 -576
  227. package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
  228. package/dist/src/booking-surface/contracts.js +24 -80
  229. package/dist/src/booking-surface/dsh-planner.js +510 -281
  230. package/dist/src/booking-surface/dsh-plugin.js +17 -1
  231. package/dist/src/booking-surface/error-codes.js +82 -10
  232. package/dist/src/booking-surface/index.js +0 -3
  233. package/dist/src/booking-surface/profile.js +10 -54
  234. package/dist/src/booking-surface/recovery-chain.js +367 -0
  235. package/dist/src/booking-surface/runtime.js +765 -891
  236. package/dist/src/booking-surface/server.js +200 -251
  237. package/dist/src/booking-surface/startup.js +58 -54
  238. package/dist/src/booking-surface/validation.js +150 -218
  239. package/dist/src/bridge.js +20 -13
  240. package/dist/src/companions.js +48 -74
  241. package/dist/src/contracts.js +21 -43
  242. package/dist/src/dsh-llm.js +82 -120
  243. package/dist/src/engine.js +253 -239
  244. package/dist/src/evaluation-cadence.js +72 -209
  245. package/dist/src/evaluation-contracts.js +220 -843
  246. package/dist/src/flight-pack-adapter.js +54 -0
  247. package/dist/src/flight-pack-contract.js +2 -0
  248. package/dist/src/gotry-backend.js +70 -0
  249. package/dist/src/hotel-date-gate.js +118 -0
  250. package/dist/src/i18n.js +18 -7
  251. package/dist/src/index.js +1425 -1629
  252. package/dist/src/journey.js +132 -81
  253. package/dist/src/ledger-repair-apply.js +538 -0
  254. package/dist/src/ledger-repair-plan.js +463 -0
  255. package/dist/src/loop.js +257 -166
  256. package/dist/src/memory-capture.js +180 -32
  257. package/dist/src/memory-decay.js +30 -14
  258. package/dist/src/memory-lifecycle.js +1310 -0
  259. package/dist/src/memory-utility.js +27 -40
  260. package/dist/src/mock-llm.js +36 -61
  261. package/dist/src/model.js +81 -36
  262. package/dist/src/realtime-pricing.js +63 -72
  263. package/dist/src/slot-spec.js +84 -106
  264. package/dist/src/state-ledger.js +310 -471
  265. package/dist/src/subagent-job-id-guard.js +58 -0
  266. package/dist/src/time-anchor.js +231 -33
  267. package/dist/src/tool-packet.js +20 -8
  268. package/dist/src/travel-slots.js +58 -35
  269. package/dist/src/travel-timeline.js +44 -63
  270. package/dist/src/turn-deadline.js +121 -127
  271. package/dist/src/turn-policy.js +61 -89
  272. package/dist/src/tz-resolver.js +143 -0
  273. package/dist/src/unified.js +504 -381
  274. package/dist/src/wish-pool.js +39 -14
  275. package/dist/src/write-gate.js +495 -0
  276. package/dist/src/z3-shared.js +273 -7
  277. package/extension/README.md +4 -3
  278. package/extension/background.js +125 -14
  279. package/extension/content-bridge.js +23 -1
  280. package/extension/content-main.js +15 -4
  281. package/extension/manifest.json +16 -8
  282. package/package.json +251 -220
  283. package/ts/capabilities/agent-reach.ts +4 -4
  284. package/ts/capabilities/anything.ts +1 -1
  285. package/ts/capabilities/artifacts.ts +46 -9
  286. package/ts/capabilities/flyai.ts +156 -37
  287. package/ts/capabilities/hbcli.ts +1 -1
  288. package/ts/capabilities/incident-log.ts +88 -59
  289. package/ts/capabilities/session/adapters/ctrip-flight.ts +99 -36
  290. package/ts/capabilities/session/adapters/dida-portal.ts +250 -0
  291. package/ts/capabilities/session/adapters/rail-12306.ts +123 -24
  292. package/ts/capabilities/session/extension-bridge.ts +226 -101
  293. package/ts/capabilities/session/extension-channel.ts +22 -9
  294. package/ts/capabilities/session/wizard.ts +2 -2
  295. package/ts/capabilities/session-consent.ts +98 -14
  296. package/ts/capabilities/session-login.ts +1 -0
  297. package/ts/capabilities/session-search.ts +315 -31
  298. package/ts/dsh-runtime/vendor/README.md +8 -4
  299. package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +7 -1
  300. package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +5 -3
  301. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +1 -1
  302. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +1 -1
  303. package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +6 -6
  304. package/ts/package.json +236 -16
  305. package/ts/scripts/map-tools-vendor-package-proof.ts +152 -31
  306. package/ts/scripts/state-cli.ts +418 -51
  307. package/ts/src/artifact-gate.ts +561 -15
  308. package/ts/src/benchmark-environment-bridge.ts +37 -20
  309. package/ts/src/bookable-facts.ts +183 -7
  310. package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
  311. package/ts/src/booking-surface/dsh-planner.ts +240 -32
  312. package/ts/src/booking-surface/dsh-plugin.js +17 -1
  313. package/ts/src/booking-surface/error-codes.ts +75 -0
  314. package/ts/src/booking-surface/recovery-chain.ts +409 -0
  315. package/ts/src/booking-surface/server.ts +84 -51
  316. package/ts/src/booking-surface/validation.ts +2 -0
  317. package/ts/src/contracts.ts +12 -2
  318. package/ts/src/dsh-llm.ts +4 -8
  319. package/ts/src/index.ts +609 -90
  320. package/ts/src/loop.ts +116 -7
  321. package/ts/src/memory-capture.ts +227 -3
  322. package/ts/src/mock-llm.ts +4 -8
  323. package/ts/src/model.ts +88 -2
  324. package/ts/src/state-ledger.ts +47 -29
  325. package/ts/src/subagent-job-id-guard.ts +72 -0
  326. package/ts/src/time-anchor.ts +217 -13
  327. package/ts/src/unified.ts +316 -54
@@ -1,77 +1,112 @@
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
+ */
1
23
  import { randomUUID } from 'node:crypto';
2
24
  import { existsSync } from 'node:fs';
3
25
  import { homedir } from 'node:os';
4
26
  import { join } from 'node:path';
5
27
  import { createServer } from 'node:http';
6
- export const BRIDGE_PORTS = [
7
- 8791,
8
- 8792,
9
- 8793,
10
- 8794,
11
- 8795
12
- ];
28
+ /** 桥端口池(extension/manifest.json host_permissions 必须同面;§38 断言) */
29
+ export const BRIDGE_PORTS = [8791, 8792, 8793, 8794, 8795];
13
30
  export const BRIDGE_PROTOCOL = 'session-bridge.v1';
31
+ /** 扩展固定 ID(unpacked 通道:manifest key 派生;bundled/GitHub Releases 均此 ID。变更即失配,Node 侧拒绝) */
14
32
  export const EXTENSION_ID = 'olpgkofjhhiiiahdkkbcninhjmegghfe';
15
33
  export const EXTENSION_ORIGIN = `chrome-extension://${EXTENSION_ID}`;
34
+ /**
35
+ * Chrome Web Store 版扩展 ID(2026-09-02 上架):商店用自己生成的签名 key 重签,
36
+ * **不认 manifest 里的固定 key**——商店版 ID 与 unpacked 版不同(上架材料文档的
37
+ * 「若商店不认 key」预案坐实)。item ID 即商店详情页 URL 末段。
38
+ */
16
39
  export const EXTENSION_ID_STORE = 'oeajpiccmonococjcegddlooeeohlbgd';
17
40
  export const EXTENSION_ORIGIN_STORE = `chrome-extension://${EXTENSION_ID_STORE}`;
41
+ /** 商店详情页(一键安装 + 自动更新通道;推荐安装方式) */
18
42
  export const EXTENSION_STORE_URL = `https://chromewebstore.google.com/detail/gotry-session-bridge/${EXTENSION_ID_STORE}`;
19
- export const EXTENSION_ORIGINS = [
20
- EXTENSION_ORIGIN,
21
- EXTENSION_ORIGIN_STORE
22
- ];
43
+ /** 桥信任的扩展 Origin 白名单:unpacked(固定 key)+ 商店版,双通道同一扩展同源信任 */
44
+ export const EXTENSION_ORIGINS = [EXTENSION_ORIGIN, EXTENSION_ORIGIN_STORE];
45
+ /**
46
+ * 本地 unpacked 通道是否已落位(~/.gotry/extension/manifest.json)。
47
+ * 商店版没有本地文件面(装在 Chrome 里),离线不可探测——本函数只回答「本地通道在不在」,
48
+ * 商店版用户在这里恒 false,这正是 D-24 自适应文案的判定输入。
49
+ */
23
50
  export function localExtensionInstalled(home = homedir()) {
24
51
  return existsSync(join(home, '.gotry', 'extension', 'manifest.json'));
25
52
  }
53
+ /**
54
+ * needs-extension 自适应摘要(issue #117,D-24 残量):
55
+ * - 本地 unpacked 已落位 → 完整双通道指引(商店 + GitHub/开发者模式本地通道);
56
+ * - 未落位(商店版用户,或完全未装)→ 只推商店一键装,并给「已装商店版?打开 Chrome 即可」
57
+ * 提示——**不再出现开发者模式/本地通道文案**(已装商店版的用户不该再被指导 load unpacked)。
58
+ * 纯函数(默认 HOME 可注入)。
59
+ */
26
60
  export function needsExtensionSummary(opts = {}) {
27
61
  const local = opts.localInstalled ?? localExtensionInstalled(opts.home);
28
62
  if (local) {
29
- return 'GoTry Session Bridge 扩展未连接(未安装或已停用)。推荐 Chrome 应用商店一键装(自动更新) ' + `${EXTENSION_STORE_URL} ;或 npx gotry setup 落位后 chrome://extensions 开发者模式「加载已解压的扩展程序」指向 ~/.gotry/extension`;
63
+ return 'GoTry Session Bridge 扩展未连接(未安装或已停用)。推荐 Chrome 应用商店一键装(自动更新) '
64
+ + `${EXTENSION_STORE_URL} ;或 npx @danceiny/gotry setup 落位后 chrome://extensions 开发者模式「加载已解压的扩展程序」指向 ~/.gotry/extension`;
30
65
  }
31
- return 'GoTry Session Bridge 扩展未连接。若你已从 Chrome 商店安装(自动更新):打开 Chrome 并确认扩展已启用即可自动连接,无需再装。' + `若尚未安装:应用商店一键装 ${EXTENSION_STORE_URL} (装完零弹窗,浏览器自己当安装器)`;
66
+ return 'GoTry Session Bridge 扩展未连接。若你已从 Chrome 商店安装(自动更新):打开 Chrome 并确认扩展已启用即可自动连接,无需再装。'
67
+ + `若尚未安装:应用商店一键装 ${EXTENSION_STORE_URL} (装完零弹窗,浏览器自己当安装器)`;
32
68
  }
69
+ /** 扩展在线判定:/health 心跳 ≤30s 一次 + 轮询重连,1.5 倍容差 */
33
70
  export const EXTENSION_CONNECTED_WINDOW_MS = 45_000;
71
+ /** /jobs 长轮询 hold 上限(必须 < MV3 SW 30s 存活窗口) */
34
72
  export const JOBS_LONG_POLL_MS = 20_000;
73
+ /** 提交 job 时扩展尚未上线,等它连上桥的宽限(首次调用后桥才拉起,扩展 5s 内即回连) */
35
74
  export const DEFAULT_EXTENSION_WAIT_MS = 6_000;
75
+ /** /results 回包上限(batchSearch ~550KB,余量给未来酒店/多航段) */
36
76
  export const MAX_RESULT_BODY_BYTES = 8 * 1024 * 1024;
77
+ function jobMatches(entry, capabilities) {
78
+ if (capabilities == null || capabilities.size === 0)
79
+ return true;
80
+ return capabilities.has(entry.site);
81
+ }
37
82
  function readBody(req, cap) {
38
- return new Promise((resolve)=>{
83
+ return new Promise((resolve) => {
39
84
  const chunks = [];
40
85
  let size = 0;
41
86
  let done = false;
42
- req.on('data', (c)=>{
43
- if (done) return;
87
+ req.on('data', (c) => {
88
+ if (done)
89
+ return;
44
90
  size += c.length;
45
91
  if (size > cap) {
46
92
  done = true;
47
93
  req.removeAllListeners('data');
48
- resolve({
49
- err: `body 超 ${cap} 字节上限(防失控回包)`
50
- });
94
+ resolve({ err: `body 超 ${cap} 字节上限(防失控回包)` });
51
95
  return;
52
96
  }
53
97
  chunks.push(c);
54
98
  });
55
- req.on('end', ()=>{
56
- if (!done) {
57
- done = true;
58
- resolve(Buffer.concat(chunks).toString('utf8'));
59
- }
60
- });
61
- req.on('error', ()=>{
62
- if (!done) {
63
- done = true;
64
- resolve({
65
- err: 'body 读取失败'
66
- });
67
- }
68
- });
99
+ req.on('end', () => { if (!done) {
100
+ done = true;
101
+ resolve(Buffer.concat(chunks).toString('utf8'));
102
+ } });
103
+ req.on('error', () => { if (!done) {
104
+ done = true;
105
+ resolve({ err: 'body 读取失败' });
106
+ } });
69
107
  });
70
108
  }
71
- export async function createSessionBridge(opts = {}) {
72
- const ports = opts.ports ?? [
73
- ...BRIDGE_PORTS
74
- ];
109
+ export function createBridgeJobQueue(opts = {}) {
75
110
  const extensionOrigins = new Set(opts.extensionOrigins ?? EXTENSION_ORIGINS);
76
111
  const now = opts.now ?? Date.now;
77
112
  const queue = [];
@@ -79,199 +114,210 @@ export async function createSessionBridge(opts = {}) {
79
114
  const parked = [];
80
115
  let lastSeenAt = 0;
81
116
  let closed = false;
82
- let port = 0;
83
- const extensionConnected = ()=>lastSeenAt > 0 && now() - lastSeenAt < EXTENSION_CONNECTED_WINDOW_MS;
84
- function takeQueued() {
85
- const next = queue.shift();
86
- if (next) inFlight.set(next.job.jobId, next);
87
- return next ?? null;
117
+ const extensionConnected = () => lastSeenAt > 0 && now() - lastSeenAt < EXTENSION_CONNECTED_WINDOW_MS;
118
+ /** 领走首个 capability 匹配的 job(移入 inFlight——回包按 jobId 路由,两处都可找到) */
119
+ function takeMatching(capabilities) {
120
+ const idx = queue.findIndex((q) => jobMatches(q.job, capabilities));
121
+ if (idx < 0)
122
+ return null;
123
+ const [next] = queue.splice(idx, 1);
124
+ inFlight.set(next.job.jobId, next);
125
+ return next;
88
126
  }
89
- const server = createServer((req, res)=>{
90
- if (closed) {
91
- res.statusCode = 503;
92
- res.end();
93
- return;
94
- }
95
- const urlPath = (req.url ?? '').split('?')[0];
96
- const origin = req.headers.origin;
97
- const isPost = req.method === 'POST';
98
- const originTrusted = typeof origin === 'string' && extensionOrigins.has(origin);
99
- if (isPost && !originTrusted) {
100
- res.statusCode = 403;
101
- res.setHeader('content-type', 'application/json');
102
- res.end(JSON.stringify({
103
- ok: false,
104
- error: 'origin 不在桥白名单'
105
- }));
106
- return;
107
- }
108
- if (originTrusted) {
109
- lastSeenAt = now();
110
- }
111
- const finish = (code, body)=>{
112
- res.statusCode = code;
113
- res.setHeader('content-type', 'application/json');
114
- res.end(JSON.stringify(body));
115
- };
116
- if (req.method === 'GET' && urlPath === '/health') {
117
- finish(200, {
118
- ok: true,
119
- service: 'gotry-session-bridge',
120
- protocol: BRIDGE_PROTOCOL
121
- });
122
- return;
123
- }
124
- if (req.method === 'GET' && urlPath === '/status') {
125
- finish(200, {
126
- ok: true,
127
- port,
128
- protocol: BRIDGE_PROTOCOL,
129
- extensionConnected: extensionConnected(),
130
- lastSeenMsAgo: lastSeenAt > 0 ? now() - lastSeenAt : null,
131
- queued: queue.length,
132
- inFlight: inFlight.size,
133
- parked: parked.length
134
- });
135
- return;
136
- }
137
- if (isPost && urlPath === '/jobs') {
138
- const next = takeQueued();
139
- if (next) {
140
- finish(200, {
141
- job: next.job
142
- });
143
- return;
144
- }
145
- const parkTimer = setTimeout(()=>{
146
- const i = parked.findIndex((p)=>p.res === res);
147
- if (i >= 0) parked.splice(i, 1);
148
- finish(200, {
149
- job: null
150
- });
151
- }, JOBS_LONG_POLL_MS);
152
- if (!opts.keepBridge) parkTimer.unref();
153
- parked.push({
154
- res,
155
- timer: parkTimer
156
- });
157
- res.on('close', ()=>{
158
- const i = parked.findIndex((p)=>p.res === res);
159
- if (i >= 0) parked.splice(i, 1);
160
- clearTimeout(parkTimer);
161
- });
162
- return;
163
- }
164
- if (isPost && urlPath.startsWith('/results/')) {
165
- const jobId = decodeURIComponent(urlPath.slice('/results/'.length));
166
- void readBody(req, MAX_RESULT_BODY_BYTES).then((body)=>{
167
- if (typeof body !== 'string') {
168
- finish(400, {
169
- ok: false,
170
- error: body.err
171
- });
172
- return;
173
- }
174
- let parsed;
175
- try {
176
- parsed = JSON.parse(body);
177
- } catch {
178
- finish(400, {
179
- ok: false,
180
- error: '回包不是 JSON'
181
- });
182
- return;
183
- }
184
- finish(200, {
185
- ok: true
186
- });
187
- resolveJob(jobId, parsed);
188
- });
189
- return;
190
- }
191
- finish(404, {
192
- ok: false,
193
- error: 'unknown endpoint'
194
- });
195
- });
196
- server.on('connection', (socket)=>{
197
- if (!opts.keepBridge) socket.unref();
198
- });
127
+ /** 扩展回包:queued inFlight 两处都可寻址(job 执行失败 = result.ok:false,桥自身不吞) */
199
128
  function resolveJob(jobId, parsed) {
200
129
  const inFlightHit = inFlight.get(jobId);
201
130
  if (inFlightHit) {
202
131
  inFlight.delete(jobId);
203
- if (inFlightHit.timer) clearTimeout(inFlightHit.timer);
204
- inFlightHit.resolve({
205
- ok: true,
206
- result: parsed
207
- });
132
+ if (inFlightHit.timer)
133
+ clearTimeout(inFlightHit.timer);
134
+ inFlightHit.resolve({ ok: true, result: parsed });
208
135
  return;
209
136
  }
210
- const idx = queue.findIndex((q)=>q.job.jobId === jobId);
211
- if (idx < 0) return;
137
+ const idx = queue.findIndex((q) => q.job.jobId === jobId);
138
+ if (idx < 0)
139
+ return;
212
140
  const [entry] = queue.splice(idx, 1);
213
- if (entry.timer) clearTimeout(entry.timer);
214
- entry.resolve({
215
- ok: true,
216
- result: parsed
217
- });
141
+ if (entry.timer)
142
+ clearTimeout(entry.timer);
143
+ entry.resolve({ ok: true, result: parsed });
218
144
  }
145
+ /** 新 job 入队时即时派发给 capability 匹配的 parked 取活者(否则要白等一个 20s 长轮询周期) */
219
146
  function dispatchToParked(entry) {
220
- const p = parked.shift();
221
- if (!p) return false;
147
+ const i = parked.findIndex((p) => jobMatches(entry.job, p.capabilities));
148
+ if (i < 0)
149
+ return false;
150
+ const [p] = parked.splice(i, 1);
222
151
  clearTimeout(p.timer);
223
152
  inFlight.set(entry.job.jobId, entry);
224
153
  p.res.statusCode = 200;
225
154
  p.res.setHeader('content-type', 'application/json');
226
- p.res.end(JSON.stringify({
227
- job: entry.job
228
- }));
155
+ p.res.end(JSON.stringify({ job: entry.job }));
229
156
  return true;
230
157
  }
231
- const bridge = {
158
+ const stats = () => ({
159
+ queued: queue.length,
160
+ inFlight: inFlight.size,
161
+ parked: parked.length,
162
+ lastSeenMsAgo: lastSeenAt > 0 ? now() - lastSeenAt : null,
163
+ });
164
+ /**
165
+ * 协议处理器工厂:prefix='' 为独立桥形态(路径式 results);prefix='/v1/session/bridge'
166
+ * 为挂载形态(查询式 results——gotry-backend 内核只支持精确路由,jobId 走 query)。
167
+ * Origin 白名单校验内置于两种形态(远程形态同样强制:网页跨域请求必带邪恶 Origin)。
168
+ */
169
+ function makeHandler(prefix, resultsViaQuery, statusExtra) {
170
+ return (req, res) => {
171
+ if (closed) {
172
+ res.statusCode = 503;
173
+ res.end();
174
+ return;
175
+ }
176
+ const urlPath = (req.url ?? '').split('?')[0];
177
+ const path = prefix && urlPath.startsWith(prefix) ? urlPath.slice(prefix.length) : urlPath;
178
+ const origin = req.headers.origin;
179
+ const isPost = req.method === 'POST';
180
+ const originTrusted = typeof origin === 'string' && extensionOrigins.has(origin);
181
+ // 网页侧请求必带 Origin;桥端点只信任白名单内的 chrome-extension:// 源(诊断 GET 面放行)
182
+ if (isPost && !originTrusted) {
183
+ res.statusCode = 403;
184
+ res.setHeader('content-type', 'application/json');
185
+ res.end(JSON.stringify({ ok: false, error: 'origin 不在桥白名单' }));
186
+ return;
187
+ }
188
+ // 心跳:扩展平时只走 /jobs 长轮询(健康只在启动探测时 ping 一次)——
189
+ // 一切携带白名单扩展 Origin 的请求都刷新 lastSeen,否则 45s 后误判扩展掉线;
190
+ // 无 Origin 的诊断 GET(/health /status)不记心跳,curl 探活不能伪造「扩展在线」
191
+ if (originTrusted) {
192
+ lastSeenAt = now();
193
+ }
194
+ const finish = (code, body) => {
195
+ res.statusCode = code;
196
+ res.setHeader('content-type', 'application/json');
197
+ res.end(JSON.stringify(body));
198
+ };
199
+ if (req.method === 'GET' && path === '/health') {
200
+ finish(200, { ok: true, service: 'gotry-session-bridge', protocol: BRIDGE_PROTOCOL });
201
+ return;
202
+ }
203
+ if (req.method === 'GET' && path === '/status') {
204
+ finish(200, {
205
+ ok: true, protocol: BRIDGE_PROTOCOL, ...stats(),
206
+ extensionConnected: extensionConnected(),
207
+ ...(statusExtra ? statusExtra() : {}),
208
+ });
209
+ return;
210
+ }
211
+ if (isPost && path === '/jobs') {
212
+ // 轮询体携带 {extensionVersion, capabilities: SITES 键清单}——按站点 capability
213
+ // 路由(见 ParkedPoller 注);体缺失/非 JSON 视为 match-all,向前兼容旧扩展。
214
+ void readBody(req, 64 * 1024).then((body) => {
215
+ const pollerCaps = (() => {
216
+ if (typeof body !== 'string')
217
+ return null;
218
+ try {
219
+ const parsed = JSON.parse(body);
220
+ return Array.isArray(parsed.capabilities) ? new Set(parsed.capabilities.filter((c) => typeof c === 'string')) : null;
221
+ }
222
+ catch {
223
+ return null;
224
+ }
225
+ })();
226
+ const next = takeMatching(pollerCaps);
227
+ if (next) {
228
+ finish(200, { job: next.job });
229
+ return;
230
+ }
231
+ // 长轮询:hold ≤ JOBS_LONG_POLL_MS(必须 < MV3 SW 30s 存活窗口,每次响应都续命);
232
+ // 新 job 提交时即时唤醒 parked 取活者(见 dispatchToParked)
233
+ const poller = { res, timer: null, capabilities: pollerCaps };
234
+ const parkTimer = setTimeout(() => {
235
+ const i = parked.findIndex((p) => p.res === res);
236
+ if (i >= 0)
237
+ parked.splice(i, 1);
238
+ finish(200, { job: null });
239
+ }, JOBS_LONG_POLL_MS);
240
+ poller.timer = parkTimer;
241
+ // 默认桥是惰性能力:外部扩展的 parked 轮询不得在主流程结束后钉住 CLI。
242
+ // keepBridge wizard 反过来要靠它守住进程,因此只对默认形态 unref。
243
+ if (!opts.keepBridge)
244
+ parkTimer.unref();
245
+ parked.push(poller);
246
+ res.on('close', () => {
247
+ const i = parked.findIndex((p) => p.res === res);
248
+ if (i >= 0)
249
+ parked.splice(i, 1);
250
+ clearTimeout(parkTimer);
251
+ });
252
+ });
253
+ return;
254
+ }
255
+ const isResults = isPost && (resultsViaQuery ? path === '/results' : path.startsWith('/results/'));
256
+ if (isResults) {
257
+ const jobId = resultsViaQuery
258
+ ? new URL(req.url ?? '', 'http://bridge.local').searchParams.get('jobId') ?? ''
259
+ : decodeURIComponent(path.slice('/results/'.length));
260
+ if (!jobId) {
261
+ finish(400, { ok: false, error: '缺 jobId' });
262
+ return;
263
+ }
264
+ void readBody(req, MAX_RESULT_BODY_BYTES).then((body) => {
265
+ if (typeof body !== 'string') {
266
+ finish(400, { ok: false, error: body.err });
267
+ return;
268
+ }
269
+ let parsed;
270
+ try {
271
+ parsed = JSON.parse(body);
272
+ }
273
+ catch {
274
+ finish(400, { ok: false, error: '回包不是 JSON' });
275
+ return;
276
+ }
277
+ finish(200, { ok: true });
278
+ resolveJob(jobId, parsed);
279
+ });
280
+ return;
281
+ }
282
+ finish(404, { ok: false, error: 'unknown endpoint' });
283
+ };
284
+ }
285
+ const queueHandle = {
232
286
  port: 0,
233
287
  extensionConnected,
234
- submit (job, submitOpts = {}) {
288
+ stats,
289
+ handleRequest: makeHandler('', false),
290
+ handleMountedRequest: makeHandler('/v1/session/bridge', true),
291
+ submit(job, submitOpts = {}) {
235
292
  const extensionWaitMs = submitOpts.extensionWaitMs ?? DEFAULT_EXTENSION_WAIT_MS;
236
293
  const timeoutMs = submitOpts.timeoutMs ?? job.timeoutMs ?? 30_000;
237
- const full = {
238
- jobId: randomUUID(),
239
- ...job
240
- };
241
- return new Promise((resolve)=>{
294
+ const full = { jobId: randomUUID(), ...job };
295
+ return new Promise((resolve) => {
242
296
  if (closed) {
243
- resolve({
244
- ok: false,
245
- reason: 'bridge-unavailable',
246
- summary: '扩展桥已关闭'
247
- });
297
+ resolve({ ok: false, reason: 'bridge-unavailable', summary: '扩展桥已关闭' });
248
298
  return;
249
299
  }
250
- const entry = {
251
- job: full,
252
- resolve: null,
253
- timer: null
254
- };
255
- const settle = (outcome)=>{
256
- if (entry.timer) clearTimeout(entry.timer);
257
- const i = queue.findIndex((q)=>q.job.jobId === full.jobId);
258
- if (i >= 0) queue.splice(i, 1);
300
+ const entry = { job: full, resolve: null, timer: null };
301
+ const settle = (outcome) => {
302
+ if (entry.timer)
303
+ clearTimeout(entry.timer);
304
+ const i = queue.findIndex((q) => q.job.jobId === full.jobId);
305
+ if (i >= 0)
306
+ queue.splice(i, 1);
259
307
  inFlight.delete(full.jobId);
260
308
  resolve(outcome);
261
309
  };
262
310
  entry.resolve = settle;
263
- entry.timer = setTimeout(()=>{
264
- settle({
265
- ok: false,
266
- reason: 'timeout',
267
- summary: `扩展未在 ${timeoutMs}ms 内回包(标签页无嗅探命中或扩展已停用)`
268
- });
311
+ entry.timer = setTimeout(() => {
312
+ settle({ ok: false, reason: 'timeout', summary: `扩展未在 ${timeoutMs}ms 内回包(标签页无嗅探命中或扩展已停用)` });
269
313
  }, timeoutMs);
270
- if (!dispatchToParked(entry)) queue.push(entry);
314
+ if (!dispatchToParked(entry))
315
+ queue.push(entry);
316
+ // 扩展未上线:有界等待(桥刚拉起时扩展 ≤5s 即连上);宽限过仍无 → 立即 no-spend 失败,不空耗
271
317
  let waited = 0;
272
- const waitTimer = setInterval(()=>{
318
+ const waitTimer = setInterval(() => {
273
319
  waited += 250;
274
- if (bridge.extensionConnected()) {
320
+ if (queueHandle.extensionConnected()) {
275
321
  clearInterval(waitTimer);
276
322
  return;
277
323
  }
@@ -280,92 +326,107 @@ export async function createSessionBridge(opts = {}) {
280
326
  settle({
281
327
  ok: false,
282
328
  reason: 'extension-not-connected',
283
- summary: needsExtensionSummary()
329
+ // D-24 自适应文案(issue #117):按本地通道是否落位决定是否提及开发者模式
330
+ summary: needsExtensionSummary(),
284
331
  });
285
332
  }
286
333
  }, 250);
287
334
  });
288
335
  },
289
- close: ()=>new Promise((resolve)=>{
290
- closed = true;
291
- const voided = {
292
- ok: false,
293
- reason: 'bridge-unavailable',
294
- summary: '扩展桥关闭,任务作废(零花费)'
295
- };
296
- for (const q of queue.splice(0)){
297
- if (q.timer) clearTimeout(q.timer);
298
- q.resolve(voided);
299
- }
300
- for (const [, q] of inFlight){
301
- if (q.timer) clearTimeout(q.timer);
302
- q.resolve(voided);
303
- }
304
- inFlight.clear();
305
- for (const p of parked.splice(0)){
306
- clearTimeout(p.timer);
307
- p.res.destroy();
308
- }
309
- server.close(()=>resolve());
336
+ close: () => new Promise((resolve) => {
337
+ closed = true;
338
+ const voided = { ok: false, reason: 'bridge-unavailable', summary: '扩展桥关闭,任务作废(零花费)' };
339
+ for (const q of queue.splice(0)) {
340
+ if (q.timer)
341
+ clearTimeout(q.timer);
342
+ q.resolve(voided);
343
+ }
344
+ for (const [, q] of inFlight) {
345
+ if (q.timer)
346
+ clearTimeout(q.timer);
347
+ q.resolve(voided);
348
+ }
349
+ inFlight.clear();
350
+ for (const p of parked.splice(0)) {
351
+ clearTimeout(p.timer);
352
+ p.res.destroy();
353
+ }
354
+ resolve();
355
+ }),
356
+ };
357
+ return queueHandle;
358
+ }
359
+ /** 创 Bridge 服务(测试可直接调;生产走 getOrCreateSessionBridge 懒单例) */
360
+ export async function createSessionBridge(opts = {}) {
361
+ const ports = opts.ports ?? [...BRIDGE_PORTS];
362
+ const queueHandle = createBridgeJobQueue(opts);
363
+ let port = 0;
364
+ const server = createServer((req, res) => queueHandle.handleRequest(req, res));
365
+ server.on('connection', (socket) => {
366
+ // server.unref() 只解开监听器;已接受的扩展长轮询 socket 仍会引用事件循环。
367
+ if (!opts.keepBridge)
368
+ socket.unref();
369
+ });
370
+ const bridge = {
371
+ port: 0,
372
+ extensionConnected: queueHandle.extensionConnected,
373
+ submit: (job, submitOpts) => queueHandle.submit(job, submitOpts),
374
+ close: () => new Promise((resolve) => {
375
+ void queueHandle.close().then(() => {
376
+ server.close(() => resolve());
310
377
  server.closeAllConnections?.();
311
- })
378
+ });
379
+ }),
312
380
  };
313
- const listenAt = (p)=>new Promise((resolve, reject)=>{
314
- const onError = (e)=>{
315
- server.off('listening', onListening);
316
- reject(e);
317
- };
318
- const onListening = ()=>{
319
- server.off('error', onError);
320
- resolve();
321
- };
322
- server.once('error', onError);
323
- server.once('listening', onListening);
324
- server.listen(p, '127.0.0.1');
325
- });
326
- for (const p of ports){
381
+ const listenAt = (p) => new Promise((resolve, reject) => {
382
+ const onError = (e) => { server.off('listening', onListening); reject(e); };
383
+ const onListening = () => { server.off('error', onError); resolve(); };
384
+ server.once('error', onError);
385
+ server.once('listening', onListening);
386
+ server.listen(p, '127.0.0.1');
387
+ });
388
+ for (const p of ports) {
327
389
  try {
328
390
  await listenAt(p);
329
391
  if (!opts.keepBridge) {
392
+ // 桥是后台服务:不得钉住事件循环——否则任何跑完主流程的宿主进程(如 smoke/CI)无法退出
330
393
  server.unref();
331
394
  }
395
+ // keepBridge 模式下:wizard 期间要持续监听扩展心跳(30s 周期),
396
+ // 不 unref 让 server 钉事件循环,wizard main 退时再显式 close 释放。
332
397
  port = server.address().port;
333
398
  bridge.port = port;
334
- return {
335
- ok: true,
336
- bridge
337
- };
338
- } catch {}
399
+ return { ok: true, bridge };
400
+ }
401
+ catch {
402
+ // 端口被占(如另一个 gotry 实例)——试端口池下一个
403
+ }
339
404
  }
405
+ await queueHandle.close().catch(() => { });
340
406
  return {
341
407
  ok: false,
342
- summary: `扩展桥端口池 ${ports.join('/')} 全部被占(多为并行 gotry 实例);不影响官方通道 gotry_flyai_search。关闭多余实例后重试。`
408
+ summary: `扩展桥端口池 ${ports.join('/')} 全部被占(多为并行 gotry 实例);不影响官方通道 gotry_flyai_search。关闭多余实例后重试。`,
343
409
  };
344
410
  }
411
+ /* ---------- 生产懒单例(进程内随 dsh 存活;失败可重试) ---------- */
345
412
  let singleton = null;
346
413
  export async function getOrCreateSessionBridge(opts = {}) {
347
- if (singleton) return singleton;
414
+ if (singleton)
415
+ return singleton;
348
416
  const created = await createSessionBridge(opts);
349
- const result = created.ok ? {
350
- ok: true,
351
- bridge: created.bridge
352
- } : {
353
- ok: false,
354
- summary: created.summary
355
- };
417
+ const result = created.ok
418
+ ? { ok: true, bridge: created.bridge }
419
+ : { ok: false, summary: created.summary };
356
420
  singleton = result;
357
421
  return result;
358
422
  }
423
+ /** 测试隔离:丢弃单例(端口是进程内资源,close 后端口池立即可复用) */
359
424
  export async function __resetSessionBridgeForTest() {
360
- if (singleton?.ok) await singleton.bridge.close().catch(()=>{});
425
+ if (singleton?.ok)
426
+ await singleton.bridge.close().catch(() => { });
361
427
  singleton = null;
362
428
  }
429
+ /** 测试注入:以临时端口的桥替换单例(channel 层测试不碰真实端口池) */
363
430
  export function __setSessionBridgeForTest(bridge) {
364
- singleton = bridge ? {
365
- ok: true,
366
- bridge
367
- } : null;
431
+ singleton = bridge ? { ok: true, bridge } : null;
368
432
  }
369
-
370
-
371
- //# sourceURL=ts/capabilities/session/extension-bridge.ts