@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,148 +1,76 @@
1
+ /**
2
+ * 通道探针 tick 测试(外部事件接缝第 1 段,issue #82 兼容方向;全离线):
3
+ * 1. evaluateProbeResults 纯函数:fail→down / ok+此前down→'ok'恢复 / ok+健康→零写 / skip→不产事件
4
+ * 2. readLatestChannelEvents latest-wins:'ok' 事件超越同通道更早的 down(恢复语义落地)
5
+ * 3. doctor 消费口径:恢复后 `state === 'down'` 判定不再命中(飞行可达性注释消失)
6
+ * 4. metrics aggregateChannels:'ok' 超越后 newest 不含该通道,down 事件计数保留(历史频率面)
7
+ * 5. CLI:--list 列探测面(session 系与 flyai 显式 skip);--only 未匹配 exit 2
8
+ *
9
+ * 运行: cd ts && npx tsx scripts/channel-probe-tests.ts
10
+ */
1
11
  import assert from 'node:assert/strict';
2
12
  import { mkdtemp, writeFile, mkdir, readFile } from 'node:fs/promises';
3
13
  import { tmpdir } from 'node:os';
4
14
  import { join } from 'node:path';
5
15
  import { evaluateProbeResults, defaultProbes, parseArgs, main } from './channel-probe.js';
6
- import { readLatestChannelEvents, recordChannelEvent } from '../capabilities/channel-health.js';
16
+ import { readLatestChannelEvents } from '../capabilities/channel-health.js';
7
17
  import { aggregateChannels } from './build-metrics-report.js';
8
18
  const tmp = await mkdtemp(join(tmpdir(), 'gotry-probe-test-'));
19
+ // ── 1. evaluateProbeResults 纯函数 ───────────────────────────────────────────
9
20
  const probes = defaultProbes();
10
- const hb = probes.find((p)=>p.channel === 'hbcli-hotel');
11
- const om = probes.find((p)=>p.channel === 'open-meteo');
12
- const fly = probes.find((p)=>p.channel === 'flyai');
21
+ const hb = probes.find((p) => p.channel === 'hbcli-hotel');
22
+ const om = probes.find((p) => p.channel === 'open-meteo');
23
+ const fly = probes.find((p) => p.channel === 'flyai');
13
24
  const now = new Date('2026-09-07T12:00:00Z');
14
25
  const evs = evaluateProbeResults([
15
- {
16
- probe: hb,
17
- result: {
18
- ok: false,
19
- reason: 'auth-denied',
20
- ms: 5
21
- }
22
- },
23
- {
24
- probe: om,
25
- result: {
26
- ok: true,
27
- ms: 100
28
- }
29
- },
30
- {
31
- probe: fly,
32
- result: {
33
- ok: true,
34
- ms: 0
35
- }
36
- }
37
- ], new Set([
38
- 'open-meteo'
39
- ]), now);
26
+ { probe: hb, result: { ok: false, reason: 'auth-denied', ms: 5 } },
27
+ { probe: om, result: { ok: true, ms: 100 } },
28
+ { probe: fly, result: { ok: true, ms: 0 } },
29
+ ], new Set(['open-meteo']), now);
40
30
  assert.equal(evs.length, 2, `fail→down + ok恢复→'ok' 共 2 条,实际 ${evs.length}`);
41
- assert.deepEqual({
42
- channel: evs[0].channel,
43
- state: evs[0].state,
44
- reason: evs[0].reason
45
- }, {
46
- channel: 'hbcli-hotel',
47
- state: 'down',
48
- reason: 'auth-denied'
49
- }, 'fail 应产 down 事件带 reason');
50
- assert.deepEqual({
51
- channel: evs[1].channel,
52
- state: evs[1].state
53
- }, {
54
- channel: 'open-meteo',
55
- state: 'ok'
56
- }, 'ok 且此前 down 应产 ok 恢复事件');
57
- const noWrites = evaluateProbeResults([
58
- {
59
- probe: om,
60
- result: {
61
- ok: true,
62
- ms: 90
63
- }
64
- }
65
- ], new Set(), now);
31
+ assert.deepEqual({ channel: evs[0].channel, state: evs[0].state, reason: evs[0].reason }, { channel: 'hbcli-hotel', state: 'down', reason: 'auth-denied' }, 'fail 应产 down 事件带 reason');
32
+ assert.deepEqual({ channel: evs[1].channel, state: evs[1].state }, { channel: 'open-meteo', state: 'ok' }, 'ok 且此前 down 应产 ok 恢复事件');
33
+ const noWrites = evaluateProbeResults([{ probe: om, result: { ok: true, ms: 90 } }], new Set(), now);
66
34
  assert.equal(noWrites.length, 0, 'ok 且此前健康 → 零写放大');
35
+ // ── 2. latest-wins:'ok' 超越更早的 down ──────────────────────────────────────
67
36
  const stateRoot = join(tmp, 'state');
68
- await mkdir(join(stateRoot, 'gotry-state'), {
69
- recursive: true
70
- });
37
+ await mkdir(join(stateRoot, 'gotry-state'), { recursive: true });
71
38
  const jsonl = join(stateRoot, 'gotry-state', 'channel-health.jsonl');
72
39
  await writeFile(jsonl, [
73
- JSON.stringify({
74
- channel: 'opensky',
75
- state: 'down',
76
- reason: 'http-503',
77
- at: '2026-09-07T10:00:00.000Z'
78
- }),
79
- JSON.stringify({
80
- channel: 'opensky',
81
- state: 'ok',
82
- at: '2026-09-07T11:00:00.000Z'
83
- }),
84
- JSON.stringify({
85
- channel: 'hbcli-hotel',
86
- state: 'down',
87
- reason: 'auth-denied',
88
- at: '2026-09-07T10:30:00.000Z'
89
- }),
40
+ JSON.stringify({ channel: 'opensky', state: 'down', reason: 'http-503', at: '2026-09-07T10:00:00.000Z' }),
41
+ JSON.stringify({ channel: 'opensky', state: 'ok', at: '2026-09-07T11:00:00.000Z' }),
42
+ JSON.stringify({ channel: 'hbcli-hotel', state: 'down', reason: 'auth-denied', at: '2026-09-07T10:30:00.000Z' }),
90
43
  'not-json-garbage',
91
- ''
44
+ '',
92
45
  ].join('\n'), 'utf-8');
93
- const latest = await readLatestChannelEvents(stateRoot, {
94
- now: Date.parse('2026-09-07T12:00:00Z')
95
- });
46
+ const latest = await readLatestChannelEvents(stateRoot, { now: Date.parse('2026-09-07T12:00:00Z') });
96
47
  assert.equal(latest.get('opensky')?.state, 'ok', "'ok' 应超越同通道更早的 down");
97
48
  assert.equal(latest.get('hbcli-hotel')?.state, 'down', '未恢复通道保持 down');
98
49
  assert.equal(latest.size, 2, '坏行跳过,latest 只含两通道');
50
+ // ── 3. doctor 消费口径:恢复后 '=== down' 不命中 ─────────────────────────────
99
51
  const openskyEv = latest.get('opensky');
100
52
  assert.ok(openskyEv && openskyEv.state !== 'down', 'doctor 的 down 判定应不再命中已恢复通道');
101
- const rows = (await readFile(jsonl, 'utf-8')).split('\n').filter((l)=>l.trim()).flatMap((l)=>{
53
+ // ── 4. metrics:ok 超越后 newest 不含该通道,计数保留 ─────────────────────────
54
+ const rows = (await readFile(jsonl, 'utf-8')).split('\n').filter((l) => l.trim()).flatMap((l) => {
102
55
  try {
103
- return [
104
- JSON.parse(l)
105
- ];
106
- } catch {
56
+ return [JSON.parse(l)];
57
+ }
58
+ catch {
107
59
  return [];
108
60
  }
109
61
  });
110
- const stats = aggregateChannels(rows, {
111
- now: new Date('2026-09-07T12:00:00Z'),
112
- windowDays: 30
113
- });
114
- const openskyStat = stats.latest.find((c)=>c.channel === 'opensky');
62
+ const stats = aggregateChannels(rows, { now: new Date('2026-09-07T12:00:00Z'), windowDays: 30 });
63
+ const openskyStat = stats.latest.find((c) => c.channel === 'opensky');
115
64
  assert.equal(openskyStat, undefined, 'ok 超越的通道不应出现在当前 down/cooldown newest 面');
116
- const hbStat = stats.latest.find((c)=>c.channel === 'hbcli-hotel');
65
+ const hbStat = stats.latest.find((c) => c.channel === 'hbcli-hotel');
117
66
  assert.ok(hbStat && hbStat.state === 'down', '未恢复通道仍在 newest 面');
67
+ // ── 5. CLI 面 ────────────────────────────────────────────────────────────────
118
68
  const listed = defaultProbes();
119
- assert.ok(listed.find((p)=>p.channel === 'flyai')?.probeable === false, 'flyai 默认不探(共享额度)');
120
- assert.ok(listed.filter((p)=>p.channel.startsWith('session:')).every((p)=>!p.probeable), 'session:* 全部 skip');
121
- assert.deepEqual(parseArgs([
122
- '--state-root',
123
- '/tmp/x',
124
- '--only',
125
- 'a, b',
126
- '--dry-run'
127
- ]), {
128
- stateRoot: '/tmp/x',
129
- only: [
130
- 'a',
131
- 'b'
132
- ],
133
- dryRun: true,
134
- list: false
135
- }, 'parseArgs 解析');
136
- const rcList = await main([
137
- '--list'
138
- ]);
69
+ assert.ok(listed.find((p) => p.channel === 'flyai')?.probeable === false, 'flyai 默认不探(共享额度)');
70
+ assert.ok(listed.filter((p) => p.channel.startsWith('session:')).every((p) => !p.probeable), 'session:* 全部 skip');
71
+ assert.deepEqual(parseArgs(['--state-root', '/tmp/x', '--only', 'a, b', '--dry-run']), { stateRoot: '/tmp/x', only: ['a', 'b'], dryRun: true, list: false }, 'parseArgs 解析');
72
+ const rcList = await main(['--list']);
139
73
  assert.equal(rcList, 0, '--list exit 0');
140
- const rcBad = await main([
141
- '--only',
142
- 'no-such-channel'
143
- ]);
74
+ const rcBad = await main(['--only', 'no-such-channel']);
144
75
  assert.equal(rcBad, 2, '--only 未匹配 exit 2');
145
76
  console.log('CHANNEL PROBE TESTS: 5/5 OK(evaluate 纯函数 / latest-wins 恢复 / doctor 口径 / metrics 超越 / CLI 面)');
146
-
147
-
148
- //# sourceURL=ts/scripts/channel-probe-tests.ts
@@ -1,3 +1,25 @@
1
+ /**
2
+ * 通道探针 tick(外部事件接缝落地第 1 段,docs/design/external-event-seam.md §6①;
3
+ * issue #82 兼容方向)——外部事件作为健康面第二个生产者的本地形态。
4
+ *
5
+ * 设计边界(设计文档 §4,边界即信任):
6
+ * - 只读探测:每通道一个无副作用最小调用(whoami/免费公开 GET),不检索、不下单;
7
+ * - 本地生产者免鉴权(§3.2 本地可信):异常写 `down` 事件进
8
+ * <stateRoot>/gotry-state/channel-health.jsonl,routingAdvice/doctor 零改动受益;
9
+ * - 恢复走事件:探测通过且该通道此前处于 down → 写 'ok' 恢复事件
10
+ * (readLatestChannelEvents latest-wins 超越 down);此前健康则零写放大;
11
+ * - session:* 通道(需用户 Chrome 会话)无头不可探,显式 skip 不误报;
12
+ * - flyai 默认不探(匿名共享额度,探针烧额度弊大于利)——`--only flyai` 显式选入;
13
+ * - 不做常驻监听:tick 由 loopx/cron 驱动,一次运行一轮探测后退出。
14
+ *
15
+ * 用法(loopx/cron 驱动):
16
+ * npx tsx ts/scripts/channel-probe.ts --state-root <root> # 探测+落账
17
+ * npx tsx ts/scripts/channel-probe.ts --dry-run # 只打印不落账
18
+ * npx tsx ts/scripts/channel-probe.ts --only hbcli-hotel,open-meteo # 选通道
19
+ * npx tsx ts/scripts/channel-probe.ts --list # 列探测面
20
+ *
21
+ * @module scripts/channel-probe
22
+ */
1
23
  import { recordChannelEvent, readLatestChannelEvents } from '../capabilities/channel-health.js';
2
24
  function timeout(ms) {
3
25
  return AbortSignal.timeout(ms);
@@ -5,179 +27,81 @@ function timeout(ms) {
5
27
  async function fetchOk(url, what, ms = 8000) {
6
28
  const t0 = Date.now();
7
29
  try {
8
- const res = await fetch(url, {
9
- signal: timeout(ms),
10
- headers: {
11
- Accept: 'application/json'
12
- }
13
- });
14
- if (res.ok) return {
15
- ok: true,
16
- ms: Date.now() - t0
17
- };
18
- return {
19
- ok: false,
20
- reason: `${what}-http-${res.status}`,
21
- ms: Date.now() - t0
22
- };
23
- } catch (err) {
30
+ const res = await fetch(url, { signal: timeout(ms), headers: { Accept: 'application/json' } });
31
+ if (res.ok)
32
+ return { ok: true, ms: Date.now() - t0 };
33
+ return { ok: false, reason: `${what}-http-${res.status}`, ms: Date.now() - t0 };
34
+ }
35
+ catch (err) {
24
36
  const msg = err instanceof Error ? err.name : String(err);
25
- return {
26
- ok: false,
27
- reason: msg === 'TimeoutError' ? 'timeout' : 'unreachable',
28
- ms: Date.now() - t0
29
- };
37
+ return { ok: false, reason: msg === 'TimeoutError' ? 'timeout' : 'unreachable', ms: Date.now() - t0 };
30
38
  }
31
39
  }
32
40
  function spawnOk(cmd, args, what, ms = 10000) {
33
- return async ()=>{
41
+ return async () => {
34
42
  const t0 = Date.now();
35
43
  try {
36
44
  const { spawn } = await import('node:child_process');
37
- const child = spawn(cmd, args, {
38
- stdio: [
39
- 'ignore',
40
- 'ignore',
41
- 'ignore'
42
- ]
43
- });
44
- const code = await new Promise((resolve, reject)=>{
45
- const timer = setTimeout(()=>{
46
- child.kill('SIGKILL');
47
- reject(new Error('timeout'));
48
- }, ms);
49
- child.on('exit', (c)=>{
50
- clearTimeout(timer);
51
- resolve(c ?? 1);
52
- });
53
- child.on('error', (e)=>{
54
- clearTimeout(timer);
55
- reject(e);
56
- });
45
+ const child = spawn(cmd, args, { stdio: ['ignore', 'ignore', 'ignore'] });
46
+ const code = await new Promise((resolve, reject) => {
47
+ const timer = setTimeout(() => { child.kill('SIGKILL'); reject(new Error('timeout')); }, ms);
48
+ child.on('exit', (c) => { clearTimeout(timer); resolve(c ?? 1); });
49
+ child.on('error', (e) => { clearTimeout(timer); reject(e); });
57
50
  });
58
- return code === 0 ? {
59
- ok: true,
60
- ms: Date.now() - t0
61
- } : {
62
- ok: false,
63
- reason: `${what}-exit-${code}`,
64
- ms: Date.now() - t0
65
- };
66
- } catch (err) {
51
+ return code === 0 ? { ok: true, ms: Date.now() - t0 } : { ok: false, reason: `${what}-exit-${code}`, ms: Date.now() - t0 };
52
+ }
53
+ catch (err) {
67
54
  const msg = err instanceof Error ? err.message : String(err);
68
- return {
69
- ok: false,
70
- reason: msg === 'timeout' ? 'timeout' : 'spawn-failed',
71
- ms: Date.now() - t0
72
- };
55
+ return { ok: false, reason: msg === 'timeout' ? 'timeout' : 'spawn-failed', ms: Date.now() - t0 };
73
56
  }
74
57
  };
75
58
  }
59
+ /** 默认探测面(设计文档 §6①「关键通道」的无头子集;session 系与 flyai 显式 skip) */
76
60
  export function defaultProbes() {
77
61
  return [
78
62
  {
79
63
  channel: 'hbcli-hotel',
80
64
  probeable: true,
81
- run: spawnOk('hbcli', [
82
- 'auth',
83
- 'whoami'
84
- ], 'auth-denied')
65
+ run: spawnOk('hbcli', ['auth', 'whoami'], 'auth-denied'),
85
66
  },
86
67
  {
87
68
  channel: 'open-meteo',
88
69
  probeable: true,
89
- run: ()=>fetchOk('https://api.open-meteo.com/v1/forecast?latitude=25.2048&longitude=55.2708&current=temperature_2m', 'weather')
70
+ run: () => fetchOk('https://api.open-meteo.com/v1/forecast?latitude=25.2048&longitude=55.2708&current=temperature_2m', 'weather'),
90
71
  },
91
72
  {
92
73
  channel: 'opensky',
93
74
  probeable: true,
94
- run: ()=>fetchOk('https://opensky-network.org/api/states/all?lamin=24.0&lomin=53.0&lamax=26.5&lomax=56.5', 'flights', 12000)
95
- },
96
- {
97
- channel: 'flyai',
98
- probeable: false,
99
- skipReason: 'anonymous-shared-quota——探针烧额度弊大于利;--only flyai 显式选入',
100
- run: async ()=>({
101
- ok: true,
102
- ms: 0
103
- })
75
+ run: () => fetchOk('https://opensky-network.org/api/states/all?lamin=24.0&lomin=53.0&lamax=26.5&lomax=56.5', 'flights', 12000),
104
76
  },
105
- {
106
- channel: 'session:ctrip-flight',
107
- probeable: false,
108
- skipReason: '需用户 Chrome 会话,无头不可探',
109
- run: async ()=>({
110
- ok: true,
111
- ms: 0
112
- })
113
- },
114
- {
115
- channel: 'session:ctrip-hotel',
116
- probeable: false,
117
- skipReason: '需用户 Chrome 会话,无头不可探',
118
- run: async ()=>({
119
- ok: true,
120
- ms: 0
121
- })
122
- },
123
- {
124
- channel: 'session:12306-train',
125
- probeable: false,
126
- skipReason: '需用户 Chrome 会话,无头不可探',
127
- run: async ()=>({
128
- ok: true,
129
- ms: 0
130
- })
131
- },
132
- {
133
- channel: 'anything-geo',
134
- probeable: false,
135
- skipReason: '静态包通道,无远端可探',
136
- run: async ()=>({
137
- ok: true,
138
- ms: 0
139
- })
140
- },
141
- {
142
- channel: 'static-hotel',
143
- probeable: false,
144
- skipReason: '静态包通道,无远端可探',
145
- run: async ()=>({
146
- ok: true,
147
- ms: 0
148
- })
149
- },
150
- {
151
- channel: 'web-read',
152
- probeable: false,
153
- skipReason: '依赖 agent-reach 装配态,探测意义低;--only 显式选入可探',
154
- run: async ()=>({
155
- ok: true,
156
- ms: 0
157
- })
158
- }
77
+ { channel: 'flyai', probeable: false, skipReason: 'anonymous-shared-quota——探针烧额度弊大于利;--only flyai 显式选入', run: async () => ({ ok: true, ms: 0 }) },
78
+ { channel: 'session:ctrip-flight', probeable: false, skipReason: '需用户 Chrome 会话,无头不可探', run: async () => ({ ok: true, ms: 0 }) },
79
+ { channel: 'session:ctrip-hotel', probeable: false, skipReason: '需用户 Chrome 会话,无头不可探', run: async () => ({ ok: true, ms: 0 }) },
80
+ { channel: 'session:12306-train', probeable: false, skipReason: '需用户 Chrome 会话,无头不可探', run: async () => ({ ok: true, ms: 0 }) },
81
+ { channel: 'anything-geo', probeable: false, skipReason: '静态包通道,无远端可探', run: async () => ({ ok: true, ms: 0 }) },
82
+ { channel: 'static-hotel', probeable: false, skipReason: '静态包通道,无远端可探', run: async () => ({ ok: true, ms: 0 }) },
83
+ { channel: 'web-read', probeable: false, skipReason: '依赖 agent-reach 装配态,探测意义低;--only 显式选入可探', run: async () => ({ ok: true, ms: 0 }) },
159
84
  ];
160
85
  }
86
+ /**
87
+ * 探测结果 → 事件序列(纯函数,测试锚点):
88
+ * - fail → down 事件(永远落,哪怕已 down——重申事实,消费方幂等);
89
+ * - ok 且此前处于 down → 'ok' 恢复事件(latest-wins 超越);
90
+ * - ok 且此前健康 → 不落(零写放大);
91
+ * - skip(probeable=false)→ 不产生事件。
92
+ */
161
93
  export function evaluateProbeResults(results, priorDown, now = new Date()) {
162
94
  const events = [];
163
- for (const { probe, result } of results){
164
- if (!probe.probeable) continue;
95
+ for (const { probe, result } of results) {
96
+ if (!probe.probeable)
97
+ continue;
165
98
  if (result.ok) {
166
99
  if (priorDown.has(probe.channel)) {
167
- events.push({
168
- channel: probe.channel,
169
- state: 'ok',
170
- at: now.toISOString()
171
- });
100
+ events.push({ channel: probe.channel, state: 'ok', at: now.toISOString() });
172
101
  }
173
102
  continue;
174
103
  }
175
- events.push({
176
- channel: probe.channel,
177
- state: 'down',
178
- reason: result.reason,
179
- at: now.toISOString()
180
- });
104
+ events.push({ channel: probe.channel, state: 'down', reason: result.reason, at: now.toISOString() });
181
105
  }
182
106
  return events;
183
107
  }
@@ -186,67 +110,60 @@ export function parseArgs(argv) {
186
110
  let only;
187
111
  let dryRun = false;
188
112
  let list = false;
189
- for(let i = 0; i < argv.length; i += 1){
113
+ for (let i = 0; i < argv.length; i += 1) {
190
114
  const a = argv[i];
191
- if (a === '--state-root') stateRoot = argv[++i];
192
- else if (a === '--only') only = (argv[++i] ?? '').split(',').map((s)=>s.trim()).filter(Boolean);
193
- else if (a === '--dry-run') dryRun = true;
194
- else if (a === '--list') list = true;
115
+ if (a === '--state-root')
116
+ stateRoot = argv[++i];
117
+ else if (a === '--only')
118
+ only = (argv[++i] ?? '').split(',').map((s) => s.trim()).filter(Boolean);
119
+ else if (a === '--dry-run')
120
+ dryRun = true;
121
+ else if (a === '--list')
122
+ list = true;
195
123
  }
196
- return {
197
- stateRoot,
198
- only,
199
- dryRun,
200
- list
201
- };
124
+ return { stateRoot, only, dryRun, list };
202
125
  }
203
126
  export async function main(argv) {
204
127
  const args = parseArgs(argv);
205
128
  const all = defaultProbes();
206
129
  if (args.list) {
207
- for (const p of all){
130
+ for (const p of all) {
208
131
  console.log(`${p.probeable ? 'probe' : 'skip '} ${p.channel}${p.skipReason ? ` # ${p.skipReason}` : ''}`);
209
132
  }
210
133
  return 0;
211
134
  }
212
- const selected = args.only?.length ? all.filter((p)=>args.only.includes(p.channel)) : all.filter((p)=>p.probeable);
135
+ const selected = args.only?.length ? all.filter((p) => args.only.includes(p.channel)) : all.filter((p) => p.probeable);
213
136
  if (selected.length === 0) {
214
137
  console.error('[channel-probe] --only 未匹配任何通道;--list 查看探测面');
215
138
  return 2;
216
139
  }
217
140
  const results = [];
218
- for (const probe of selected){
141
+ for (const probe of selected) {
219
142
  const result = await probe.run();
220
- results.push({
221
- probe,
222
- result
223
- });
143
+ results.push({ probe, result });
224
144
  console.log(`[channel-probe] ${result.ok ? 'ok ' : 'FAIL'} ${probe.channel} (${result.ms}ms)${result.ok ? '' : ` reason=${result.reason}`}`);
225
145
  }
226
- const failures = results.filter((r)=>!r.result.ok).length;
146
+ const failures = results.filter((r) => !r.result.ok).length;
227
147
  if (args.dryRun || !args.stateRoot) {
228
- if (!args.stateRoot) console.log('[channel-probe] 未给 --state-root:只探测不落账(等价 --dry-run)');
148
+ if (!args.stateRoot)
149
+ console.log('[channel-probe] 未给 --state-root:只探测不落账(等价 --dry-run)');
229
150
  return failures > 0 ? 1 : 0;
230
151
  }
231
152
  let priorDown = new Set();
232
153
  try {
233
154
  const latest = await readLatestChannelEvents(args.stateRoot);
234
- priorDown = new Set([
235
- ...latest.entries()
236
- ].filter(([, ev])=>ev.state === 'down' || ev.state === 'cooldown').map(([ch])=>ch));
237
- } catch {}
238
- for (const ev of evaluateProbeResults(results, priorDown)){
155
+ priorDown = new Set([...latest.entries()].filter(([, ev]) => ev.state === 'down' || ev.state === 'cooldown').map(([ch]) => ch));
156
+ }
157
+ catch { /* 读不到按全健康处理 */ }
158
+ for (const ev of evaluateProbeResults(results, priorDown)) {
239
159
  await recordChannelEvent(args.stateRoot, ev);
240
160
  console.log(`[channel-probe] 记录 ${ev.state} ${ev.channel}${ev.reason ? `(${ev.reason})` : ''}`);
241
161
  }
242
162
  return failures > 0 ? 1 : 0;
243
163
  }
244
164
  if (import.meta.url === `file://${process.argv[1]}`) {
245
- main(process.argv.slice(2)).then((code)=>process.exit(code)).catch((err)=>{
165
+ main(process.argv.slice(2)).then((code) => process.exit(code)).catch((err) => {
246
166
  console.error('[channel-probe] fatal:', err instanceof Error ? err.message : err);
247
167
  process.exit(1);
248
168
  });
249
169
  }
250
-
251
-
252
- //# sourceURL=ts/scripts/channel-probe.ts