@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
package/dist/src/loop.js CHANGED
@@ -1,15 +1,21 @@
1
+ /**
2
+ * Stage 1 对话循环(S2 段1,docs/design/stage1-top-down-design.md §2.3)。
3
+ *
4
+ * 自顶向下:循环只依赖两个端口——LlmPort(mock/真)与确定性工具(interview_next)。
5
+ * 求解/渲染接线是 S2 后续段;本段验收:重放用户开场白,系统一轮内完成
6
+ * 日历断言 + 访谈补全启动(问出工作窗口与已订资源——Kimi 第 6 轮才问的事)。
7
+ */
8
+ import { buildTimeAnchor, isRealIsoDate, planningWindowBounds, parsePlanningWindow } from './time-anchor.js';
1
9
  import { anythingSearch } from '../capabilities/anything.js';
2
10
  export function newState(year = 2026) {
3
11
  return {
4
- calendar: {
5
- year,
6
- assertedWeekdays: {}
7
- },
12
+ calendar: { year, assertedWeekdays: {} },
8
13
  profile: {},
9
14
  gates: [],
10
- wishes: []
15
+ wishes: [],
11
16
  };
12
17
  }
18
+ /** ADR-9:访谈由缺失字段驱动(确定性,无 LLM 即兴) */
13
19
  export function interviewNext(state) {
14
20
  const p = state.profile;
15
21
  const questions = [];
@@ -18,14 +24,14 @@ export function interviewNext(state) {
18
24
  questions.push({
19
25
  key: 'workWindow',
20
26
  text: '你这两周要远程办公——工作时间是几点到几点、按哪个时区?这决定每天的可玩时段。',
21
- why: '工作窗口直接决定每日节奏(普吉 13:00-22:00 与 9:00-18:00 是两种人生)'
27
+ why: '工作窗口直接决定每日节奏(普吉 13:00-22:00 与 9:00-18:00 是两种人生)',
22
28
  });
23
29
  }
24
30
  if (!p.bookedResources) {
25
31
  questions.push({
26
32
  key: 'bookedResources',
27
33
  text: '已经有订好的航班或酒店吗?(哪怕只是意向)——已订资源是硬锚点,先告诉我再规划。',
28
- why: '已订资源决定哪些段不可动,Kimi 式规划会绕开它们重排'
34
+ why: '已订资源决定哪些段不可动,Kimi 式规划会绕开它们重排',
29
35
  });
30
36
  }
31
37
  if (!p.budgetTier) {
@@ -34,35 +40,28 @@ export function interviewNext(state) {
34
40
  text: '预算档位:经济(¥12.6k 级)/ 舒适(¥16.3k 级)/ 便利优先?',
35
41
  why: '分层预算决定班次与住宿的选型口径',
36
42
  options: [
37
- {
38
- label: '经济',
39
- tradeOff: '省钱,接驳多走路'
40
- },
41
- {
42
- label: '舒适',
43
- tradeOff: 'workation 办公质量优先'
44
- },
45
- {
46
- label: '便利优先',
47
- tradeOff: '时间最省'
48
- }
49
- ]
43
+ { label: '经济', tradeOff: '省钱,接驳多走路' },
44
+ { label: '舒适', tradeOff: 'workation 办公质量优先' },
45
+ { label: '便利优先', tradeOff: '时间最省' },
46
+ ],
50
47
  });
51
48
  }
52
- return {
53
- questions,
54
- missing: questions.map((q)=>q.key)
55
- };
49
+ return { questions, missing: questions.map(q => q.key) };
56
50
  }
51
+ /** spec 校验闸:LLM 翻译产物进求解器前的确定性形状检查(责任边界的执行点)。 */
57
52
  export function validateSpec(spec) {
58
- if (!Array.isArray(spec.segments) || spec.segments.length === 0) return '没有段(segments)';
59
- for (const seg of spec.segments){
60
- if (!seg.id) return '存在缺少 id 的段';
61
- if (!Array.isArray(seg.options) || seg.options.length === 0) return `段 ${seg.id} 没有可选方案`;
62
- for (const opt of seg.options){
53
+ if (!Array.isArray(spec.segments) || spec.segments.length === 0)
54
+ return '没有段(segments)';
55
+ for (const seg of spec.segments) {
56
+ if (!seg.id)
57
+ return '存在缺少 id 的段';
58
+ if (!Array.isArray(seg.options) || seg.options.length === 0)
59
+ return `段 ${seg.id} 没有可选方案`;
60
+ for (const opt of seg.options) {
63
61
  const svcs = opt.move?.services;
64
- if (!Array.isArray(svcs) || svcs.length === 0) return `段 ${seg.id} 的方案 ${opt.id} 缺少班次(services)`;
65
- for (const s of svcs){
62
+ if (!Array.isArray(svcs) || svcs.length === 0)
63
+ return `段 ${seg.id} 的方案 ${opt.id} 缺少班次(services)`;
64
+ for (const s of svcs) {
66
65
  if (typeof s.depMin !== 'number' || typeof s.arrMin !== 'number') {
67
66
  return `段 ${seg.id}/${opt.id} 的班次 ${s.id ?? '?'} 缺少时刻(depMin/arrMin)`;
68
67
  }
@@ -71,9 +70,78 @@ export function validateSpec(spec) {
71
70
  }
72
71
  return null;
73
72
  }
73
+ function isoDateInBounds(date, bounds) {
74
+ const m = date.match(/^(\d{4})-(\d{2})-(\d{2})$/);
75
+ if (!m || !isRealIsoDate(Number(m[1]), Number(m[2]), Number(m[3])))
76
+ return false;
77
+ return date >= bounds.start && date <= bounds.end;
78
+ }
79
+ function hasDatedCandidates(spec) {
80
+ return spec.segments.some(segment => segment.date !== undefined || segment.options.some(option => option.date !== undefined));
81
+ }
82
+ /**
83
+ * Enforce a named-year future window at the planner boundary. Historical
84
+ * lookup/backtesting remains an explicit bypass; an expired named year fails
85
+ * instead of being silently rolled into a different year.
86
+ */
87
+ export function applyPlanningWindow(spec, window, anchor, options = {}) {
88
+ // Dated recommendations are future-oriented even without optional named-year context.
89
+ // Fully dateless legacy calculations remain untouched; historical bypass is explicit.
90
+ if (window?.intent === 'historical')
91
+ return { spec, rejected: [] };
92
+ const implicitFutureFloor = !window && options.defaultDatedFuture === true && hasDatedCandidates(spec);
93
+ if (!window && !implicitFutureFloor)
94
+ return { spec, rejected: [] };
95
+ const currentYear = Number(anchor.today.slice(0, 4));
96
+ if (window && window.requestedYear < currentYear) {
97
+ return {
98
+ spec,
99
+ rejected: [],
100
+ error: `${window.requestedYear} 年已结束(当前规划参考日 ${anchor.today}),不会自动改用其他年份;如需历史查询请明确说明“历史/回测”。`,
101
+ };
102
+ }
103
+ const bounds = window
104
+ ? planningWindowBounds(window, anchor)
105
+ : { start: anchor.today, end: '9999-12-31' };
106
+ if (!bounds)
107
+ return { spec, rejected: [], error: `无法建立 ${window.requestedYear} 年的未来规划窗口。` };
108
+ const rejected = [];
109
+ const guardedSegments = spec.segments.map(segment => ({
110
+ ...segment,
111
+ options: segment.options.filter(option => {
112
+ const date = option.date ?? segment.date;
113
+ if (date && isoDateInBounds(date, bounds))
114
+ return true;
115
+ rejected.push({ segmentId: segment.id, optionId: option.id, date: date ?? '未提供明确日期' });
116
+ return false;
117
+ }),
118
+ }));
119
+ if (guardedSegments.some(segment => segment.options.length === 0)) {
120
+ return {
121
+ spec: { ...spec, segments: guardedSegments },
122
+ rejected,
123
+ error: implicitFutureFloor
124
+ ? `带日期的未来推荐必须不早于当前规划参考日 ${bounds.start};没有带明确日期且在未来窗口内的可接受方案。`
125
+ : `${window.requestedYear} 年未来规划窗口为 ${bounds.start} 至 ${bounds.end};没有带明确日期且落在窗口内的可接受方案。`,
126
+ };
127
+ }
128
+ return { spec: { ...spec, segments: guardedSegments }, rejected };
129
+ }
130
+ function latestPlanningWindow(history, userMsg, anchor) {
131
+ const turns = [...history, { role: 'user', text: userMsg }];
132
+ for (let i = turns.length - 1; i >= 0; i--) {
133
+ const turn = turns[i];
134
+ if (turn?.role !== 'user')
135
+ continue;
136
+ const parsed = parsePlanningWindow(turn.text, anchor);
137
+ if (parsed)
138
+ return parsed;
139
+ }
140
+ return null;
141
+ }
74
142
  export const ASYNC_TERMINAL_SCHEMA = 'gotry_async_terminal.v1';
75
143
  function asyncTerminalOutcome(ticketId, checks) {
76
- const failedChecks = Object.entries(checks).filter(([, passed])=>!passed).map(([name])=>name);
144
+ const failedChecks = Object.entries(checks).filter(([, passed]) => !passed).map(([name]) => name);
77
145
  return {
78
146
  schema: ASYNC_TERMINAL_SCHEMA,
79
147
  ticket_id: ticketId,
@@ -81,7 +149,7 @@ function asyncTerminalOutcome(ticketId, checks) {
81
149
  passed: Object.values(checks).filter(Boolean).length,
82
150
  total: 4,
83
151
  checks,
84
- failed_checks: failedChecks
152
+ failed_checks: failedChecks,
85
153
  };
86
154
  }
87
155
  function terminalOutcomeFromState(ticketId, state) {
@@ -90,17 +158,20 @@ function terminalOutcomeFromState(ticketId, state) {
90
158
  '1_承诺时间后必有明确产物': false,
91
159
  '2_产物通过自检清单': false,
92
160
  '3_待决问题全部是简单选择题': false,
93
- '4_做不到的诚实说': false
161
+ '4_做不到的诚实说': false,
94
162
  });
95
163
  }
96
164
  return asyncTerminalOutcome(ticketId, {
97
165
  '1_承诺时间后必有明确产物': Boolean(state.solve.legs?.length || state.solve.verdicts?.length),
98
- '2_产物通过自检清单': state.solve.feasible ? state.solve.legs?.every((l)=>l['energy_pct'] !== undefined) ?? false : Boolean(state.solve.unsat_core?.length),
99
- '3_待决问题全部是简单选择题': state.gates.every((g)=>g.id === 'budget' || g.options.length >= 2),
100
- '4_做不到的诚实说': state.solve.feasible || Boolean(state.solve.suggestions?.length)
166
+ '2_产物通过自检清单': state.solve.feasible
167
+ ? (state.solve.legs?.every(l => l['energy_pct'] !== undefined) ?? false)
168
+ : Boolean(state.solve.unsat_core?.length),
169
+ '3_待决问题全部是简单选择题': state.gates.every(g => g.id === 'budget' || g.options.length >= 2),
170
+ '4_做不到的诚实说': state.solve.feasible || Boolean(state.solve.suggestions?.length),
101
171
  });
102
172
  }
103
173
  export function isComplex(state) {
174
+ /** 复杂度判据(架构占位,后续可细化):多段或多约束即深规划 */
104
175
  const p = state.profile;
105
176
  return Boolean(p.workWindow && p.bookedResources) && (state.gates.length > 0 || Boolean(state.spec));
106
177
  }
@@ -109,135 +180,135 @@ export async function requestDeepPlanning(state) {
109
180
  id: `dp-${Date.now().toString(36)}`,
110
181
  objective: '生成已验证的行程方案:全成本核算 + 动机/约束匹配 + 不失望四条',
111
182
  requestedAt: new Date().toISOString(),
112
- etaLabel: '约 1 小时(mock 期:秒级)'
113
- };
114
- state.gates.push({
115
- id: `async-${ticket.id}`,
116
- question: `深度规划已启动(${ticket.etaLabel})`,
117
- options: []
118
- });
119
- const reply = '这趟行程跨度大(两周 workation + 三城 + 红眼返程),我切到**深度规划模式**:后台做多轮校验' + '(锚点/工作窗口/全成本/负例排查),做好后通知你。**先留一道选择题给你**,回来直接定:' + '\n1. 预算档位?(经济/舒适/便利优先)';
120
- return {
121
- reply,
122
- ticket
183
+ etaLabel: '约 1 小时(mock 期:秒级)',
123
184
  };
185
+ state.gates.push({ id: `async-${ticket.id}`, question: `深度规划已启动(${ticket.etaLabel})`, options: [] });
186
+ const reply = '这趟行程跨度大(两周 workation + 三城 + 红眼返程),我切到**深度规划模式**:后台做多轮校验'
187
+ + '(锚点/工作窗口/全成本/负例排查),做好后通知你。**先留一道选择题给你**,回来直接定:'
188
+ + '\n1. 预算档位?(经济/舒适/便利优先)';
189
+ return { reply, ticket };
124
190
  }
125
191
  export async function collectDeepPlanning(state, ticket, solve) {
126
192
  const spec = state.spec;
127
193
  if (!spec) {
128
194
  const outcome = terminalOutcomeFromState(ticket.id, state);
129
- return {
130
- reply: '(内部状态缺失 spec——深度规划未就绪,这是不应发生的路径)',
131
- state,
132
- outcome
133
- };
195
+ return { reply: '(内部状态缺失 spec——深度规划未就绪,这是不应发生的路径)', state, outcome };
134
196
  }
135
197
  state.solve = await solve(spec);
136
- state.gates = state.gates.filter((g)=>!g.id.startsWith('async-'));
198
+ state.gates = state.gates.filter(g => !g.id.startsWith('async-'));
199
+ // 不失望四条自检(交付物自带,总纲 3.6)
137
200
  const outcome = terminalOutcomeFromState(ticket.id, state);
138
201
  const head = `# 回访交付:${ticket.objective}\n(工单 ${ticket.id},不失望四条:${outcome.status === 'succeeded' ? '4/4 ✅' : '有未达项 ❌'})`;
139
- return {
140
- reply: `${head}\n\n${renderSolve(state)}`,
141
- state,
142
- outcome
143
- };
202
+ return { reply: `${head}\n\n${renderSolve(state)}`, state, outcome };
144
203
  }
204
+ /** 求解结果 → 人话(模板;S4 可由 LLM 润色,数字与判定不可改) */
145
205
  export function renderSolve(state) {
146
206
  const s = state.solve;
147
- if (!s) return '(无求解结果)';
207
+ if (!s)
208
+ return '(无求解结果)';
148
209
  const lines = [];
149
210
  if (s.feasible) {
150
211
  lines.push(`**方案可行,机票合计 ¥${s.money_cny}**`);
151
- for (const lg of s.legs ?? []){
212
+ for (const lg of s.legs ?? []) {
152
213
  const l = lg;
153
- lines.push(`- ${l['leg']} ${l['service']}:${l['dep']} 起飞,${l['wake']} 出发,${l['arrive_stay']} 到,` + `门到门 ${l['door_to_door']},落地精力 ${l['energy_pct']}%,¥${l['price_cny']}`);
154
- }
155
- for (const e of s.work_window_exclusions ?? []){
156
- lines.push(`- 已排除 ${e.option}(工作窗口):${e.reason}`);
214
+ lines.push(`- ${l['leg']} ${l['service']}:${l['dep']} 起飞,${l['wake']} 出发,${l['arrive_stay']} 到,`
215
+ + `门到门 ${l['door_to_door']},落地精力 ${l['energy_pct']}%,¥${l['price_cny']}`);
157
216
  }
158
- for (const n of s.skeleton_notes ?? [])lines.push(`- ${n}`);
159
- for (const f of s.red_flags ?? [])lines.push(`- ⚠️ ${f}`);
160
- } else {
217
+ for (const n of s.skeleton_notes ?? [])
218
+ lines.push(`- ${n}`);
219
+ for (const f of s.red_flags ?? [])
220
+ lines.push(`- ⚠️ ${f}`);
221
+ }
222
+ else {
161
223
  lines.push(`**当前约束下不可行——冲突:${(s.unsat_core ?? []).join('、')}`);
162
- for (const sg of s.suggestions ?? [])lines.push(`- 放宽「${sg.relax}」可解(约 ¥${sg.money_cny})`);
224
+ for (const sg of s.suggestions ?? [])
225
+ lines.push(`- 放宽「${sg.relax}」可解(约 ¥${sg.money_cny})`);
226
+ }
227
+ for (const e of s.work_window_exclusions ?? []) {
228
+ lines.push(`- 已排除 ${e.option}(工作窗口):${e.reason}`);
163
229
  }
164
230
  if (state.gates.length) {
165
231
  lines.push('', '**待你决定(选择题)**');
166
- for (const g of state.gates){
167
- lines.push(`- ${g.question} → ${g.options.map((o)=>o.label + (o.tradeOff ? `(${o.tradeOff})` : '')).join(' / ')}`);
232
+ for (const g of state.gates) {
233
+ lines.push(`- ${g.question} → ${g.options.map(o => o.label + (o.tradeOff ? `(${o.tradeOff})` : '')).join(' / ')}`);
168
234
  }
169
235
  }
170
236
  return lines.join('\n');
171
237
  }
172
- export async function runTurn(state, userMsg, llm, history = [], solve = null) {
173
- const facts = await llm.extractFacts([
174
- ...history,
175
- {
176
- role: 'user',
177
- text: userMsg
178
- }
179
- ], state);
238
+ /** 一次对话回合:抽取事实(冲突即指出)→ 增量访谈 约束齐备则求解+渲染。 */
239
+ export async function runTurn(state, userMsg, llm, history = [], solve = null, now = new Date()) {
240
+ const anchor = buildTimeAnchor(now);
241
+ const facts = await llm.extractFacts([...history, { role: 'user', text: userMsg }], state);
180
242
  const conflicts = [];
181
243
  if (facts.calendar?.assertedWeekdays) {
182
- for (const [date, wd] of Object.entries(facts.calendar.assertedWeekdays)){
244
+ for (const [date, wd] of Object.entries(facts.calendar.assertedWeekdays)) {
183
245
  const prev = state.calendar.assertedWeekdays[date];
184
246
  if (prev && prev !== wd) {
185
247
  conflicts.push(`${date}:已有断言 ${prev},新说法 ${wd}——以先断言为准,如需改请明确说`);
186
- } else {
248
+ }
249
+ else {
187
250
  state.calendar.assertedWeekdays[date] = wd;
188
251
  }
189
252
  }
190
253
  }
191
254
  Object.assign(state.profile, facts.profile ?? {});
255
+ // 访谈:workWindow/bookedResources 是求解前置;budgetTier 转为 gate(不阻塞规划)
192
256
  const { questions } = interviewNext(state);
193
- const blocking = questions.filter((q)=>q.key !== 'budgetTier');
194
- const budgetQ = questions.find((q)=>q.key === 'budgetTier');
195
- if (budgetQ && !state.gates.some((g)=>g.id === 'budget')) {
257
+ const blocking = questions.filter(q => q.key !== 'budgetTier');
258
+ const budgetQ = questions.find(q => q.key === 'budgetTier');
259
+ if (budgetQ && !state.gates.some(g => g.id === 'budget')) {
196
260
  state.gates.push({
197
261
  id: 'budget',
198
262
  question: budgetQ.text,
199
- options: budgetQ.options ?? [
200
- {
201
- label: '经济'
202
- },
203
- {
204
- label: '舒适'
205
- },
206
- {
207
- label: '便利优先'
208
- }
209
- ]
263
+ options: budgetQ.options ?? [{ label: '经济' }, { label: '舒适' }, { label: '便利优先' }],
210
264
  });
211
265
  }
212
266
  const parts = [];
213
- if (conflicts.length) parts.push(`⚠️ 日历冲突:\n${conflicts.map((c)=>`- ${c}`).join('\n')}`);
267
+ if (conflicts.length)
268
+ parts.push(`⚠️ 日历冲突:\n${conflicts.map(c => `- ${c}`).join('\n')}`);
269
+ // D-7a 例外: blocking>0 时,用户消息含"查+地名/酒店/天气"信号时,直接调 anything_search
270
+ // 走 PoI 真相(datasources 层主动调,不动 polling 状态机,不改求解器)
214
271
  const poiProbe = probePoi(userMsg);
215
272
  if (poiProbe) {
216
- const ar = await anythingSearch({
217
- keyword: poiProbe
218
- });
273
+ const ar = await anythingSearch({ keyword: poiProbe });
219
274
  if (ar.verdict === 'hit' && ar.hits && ar.hits.length > 0) {
220
275
  const top = ar.hits.slice(0, 5);
221
- const lines = top.map((h, i)=>{
222
- const latlng = h.latitude !== undefined && h.longitude !== undefined ? ` @ (${h.latitude.toFixed(3)},${h.longitude.toFixed(3)})` : '';
276
+ const lines = top.map((h, i) => {
277
+ const latlng = h.latitude !== undefined && h.longitude !== undefined
278
+ ? ` @ (${h.latitude.toFixed(3)},${h.longitude.toFixed(3)})`
279
+ : '';
223
280
  return ` ${i + 1}. [${h.type}] ${h.name}${latlng}`;
224
281
  }).join('\n');
225
282
  parts.push(`**${poiProbe} → hit (${ar.hits.length} 候选项)**\n${lines}\n${ar.evidence}`);
226
- } else if (ar.verdict === 'miss') {
283
+ }
284
+ else if (ar.verdict === 'miss') {
227
285
  parts.push(`**${poiProbe} → miss** (酒店-be Anything 一切正常但无候选)\n${ar.evidence}`);
228
- } else {
286
+ }
287
+ else {
229
288
  parts.push(`**${poiProbe} → unavailable** (${ar.error ?? 'hbcli 不可达'})\n${ar.evidence}`);
230
289
  }
231
290
  }
232
- for (const q of blocking)parts.push(await llm.polishQuestion(q));
291
+ for (const q of blocking)
292
+ parts.push(await llm.polishQuestion(q));
293
+ // 约束齐备(无阻塞问题)→ 翻译 spec → **校验闸 + 日期一致性闸** → 求解 → 渲染
233
294
  if (blocking.length === 0 && solve) {
234
- const spec = await llm.extractSpec([
235
- ...history,
236
- {
237
- role: 'user',
238
- text: userMsg
239
- }
240
- ], state);
295
+ const extractedSpec = await llm.extractSpec([...history, { role: 'user', text: userMsg }], state);
296
+ const planning = latestPlanningWindow(history, userMsg, anchor);
297
+ const planningCheck = extractedSpec ? applyPlanningWindow(extractedSpec, planning, anchor) : null;
298
+ if (planningCheck?.error) {
299
+ state.spec = undefined;
300
+ state.solve = undefined;
301
+ parts.push(`⚠️ ${planningCheck.error}`);
302
+ return { reply: parts.join('\n\n'), state };
303
+ }
304
+ if (planningCheck?.rejected.length) {
305
+ const rejected = planningCheck.rejected
306
+ .map(r => `${r.segmentId}/${r.optionId}(${r.date})`)
307
+ .join('、');
308
+ parts.push(`已排除规划窗口外的候选:${rejected}`);
309
+ }
310
+ const spec = planningCheck?.spec ?? extractedSpec;
311
+ // 场景路由:erhai 候选标记 → 候选求解(洱海金标准的引擎判定)——纯 TS unify 路径
241
312
  if (spec && spec.note === 'erhai-candidates') {
242
313
  const { readFile } = await import('node:fs/promises');
243
314
  const { join } = await import('node:path');
@@ -251,56 +322,52 @@ export async function runTurn(state, userMsg, llm, history = [], solve = null) {
251
322
  const result = solveChoiceSegment(erhaiSpec, req);
252
323
  state.solve = result;
253
324
  parts.push(result.answer_md ?? '(候选求解完成)');
254
- } catch {
325
+ }
326
+ catch {
255
327
  parts.push('(洱海候选求解暂不可用——退回访谈)');
256
328
  }
257
- } else {
329
+ }
330
+ else {
258
331
  const invalid = spec ? validateSpec(spec) : '翻译器未产出 spec';
259
332
  if (spec && !invalid) {
260
- const ext = await llm.extractSlots([
261
- ...history,
262
- {
263
- role: 'user',
264
- text: userMsg
265
- }
266
- ]);
333
+ // D-10 切片 C:spec↔槽位日期一致性闸(ADR-10 翻译≠造数的执行点)。
334
+ // LLM 翻译的 spec 日期与代码层换算的槽位日期分歧时不求解、不猜,追问确认——
335
+ // 日期是求解关键输入,错日期 = 错判决。槽位缺失/unresolved 不参与(不造假阳性)。
336
+ const ext = await llm.extractSlots([...history, { role: 'user', text: userMsg }], now);
267
337
  if (ext) {
268
338
  const { resolveSlots, specDateMismatches } = await import('./slot-spec.js');
269
- const { buildTimeAnchor } = await import('./time-anchor.js');
270
- const mismatches = specDateMismatches(spec, resolveSlots(ext, buildTimeAnchor()));
339
+ const mismatches = specDateMismatches(spec, resolveSlots(ext, anchor));
271
340
  if (mismatches.length > 0) {
272
- const lines = mismatches.map((m)=>`- ${m.specAt}:翻译说 ${m.specDate},你的原话换算是 ${m.slotDate}(${m.field})`);
341
+ const lines = mismatches.map(m => `- ${m.specAt}:翻译说 ${m.specDate},你的原话换算是 ${m.slotDate}(${m.field})`);
273
342
  parts.push(`⚠️ 日期分歧,确认后我再算(以你为准):\n${lines.join('\n')}`);
274
- return {
275
- reply: parts.join('\n\n'),
276
- state
277
- };
343
+ return { reply: parts.join('\n\n'), state };
278
344
  }
279
345
  }
280
346
  state.spec = spec;
281
347
  state.solve = await solve(spec);
282
348
  parts.push(llm.render ? await llm.render(state) : renderSolve(state));
283
- } else if (blocking.length === 0 && invalid) {
349
+ }
350
+ else if (blocking.length === 0 && invalid) {
284
351
  parts.push(`(行程骨架还不完整:${invalid}——请补充对应信息,我不猜)`);
285
352
  }
286
353
  }
287
- } else if (blocking.length === 0) {
354
+ }
355
+ else if (blocking.length === 0) {
288
356
  parts.push('(约束齐备——进入规划,S3 接线后此处产出方案与选择题)');
289
357
  }
290
- return {
291
- reply: parts.join('\n\n'),
292
- state
293
- };
358
+ return { reply: parts.join('\n\n'), state };
294
359
  }
360
+ // ---- 异步工单持久化(S5 编排半段 → ADR-15 durable 工单):真正的「一小时后」必须跨进程存续 ----
361
+ // 权威 = 账本 workflow_runs/workflow_steps(单事务;崩溃后 done 步骤不重执行);
362
+ // {id}.json / {id}.deliverable.md 降级为视图(AGENTS.md 清扫合同 + 人工检视),
363
+ // tmp+rename 原子写——此前裸 writeFile 的非原子缺口(RFC §1.1)就此关闭。
295
364
  import { join } from 'node:path';
296
365
  import { mkdir, rename, writeFile, readFile } from 'node:fs/promises';
297
366
  import { ensureLedger, openLedgerIfExists } from './state-ledger.js';
298
367
  async function asyncDir(stateRoot) {
299
368
  const root = stateRoot === '.' ? process.cwd() : stateRoot;
300
369
  const dir = join(root, 'gotry-state', 'async');
301
- await mkdir(dir, {
302
- recursive: true
303
- });
370
+ await mkdir(dir, { recursive: true });
304
371
  return dir;
305
372
  }
306
373
  async function atomicWrite(path, text) {
@@ -310,33 +377,27 @@ async function atomicWrite(path, text) {
310
377
  }
311
378
  export async function persistAsyncTicket(ticket, state, stateRoot = '.') {
312
379
  const ledger = ensureLedger(stateRoot);
313
- ledger.createWorkflowRun({
314
- id: ticket.id,
315
- goal: ticket.objective,
316
- ticket,
317
- state
318
- });
380
+ ledger.createWorkflowRun({ id: ticket.id, goal: ticket.objective, ticket, state });
319
381
  const dir = await asyncDir(stateRoot);
320
382
  const p = join(dir, `${ticket.id}.json`);
321
- await atomicWrite(p, JSON.stringify({
322
- ticket,
323
- state
324
- }, null, 2));
383
+ await atomicWrite(p, JSON.stringify({ ticket, state }, null, 2));
325
384
  return p;
326
385
  }
327
386
  export async function loadAsyncTicket(ticketId, stateRoot = '.') {
387
+ // 账本优先;无账本的 root 回退旧 json 文件(只读,兼容存量已交付工单)
328
388
  const ledger = openLedgerIfExists(stateRoot);
329
389
  const run = ledger?.getWorkflowRun(ticketId);
330
390
  if (run) {
331
391
  return {
332
392
  ticket: JSON.parse(run.ticket_json),
333
- state: JSON.parse(run.state_json)
393
+ state: JSON.parse(run.state_json),
334
394
  };
335
395
  }
336
396
  try {
337
397
  const p = join(await asyncDir(stateRoot), `${ticketId}.json`);
338
398
  return JSON.parse(await readFile(p, 'utf-8'));
339
- } catch {
399
+ }
400
+ catch {
340
401
  return null;
341
402
  }
342
403
  }
@@ -346,31 +407,43 @@ export async function settleAsyncTicket(ticketId, reply, stateRoot = '.', outcom
346
407
  if (ledger && !terminalOutcome) {
347
408
  throw new Error(`workflow ${ticketId} 缺少可恢复的 ${ASYNC_TERMINAL_SCHEMA},拒绝误结算`);
348
409
  }
349
- if (terminalOutcome?.status === 'failed') ledger?.failWorkflowRun(ticketId, reply, terminalOutcome);
350
- else ledger?.settleWorkflowRun(ticketId, reply, terminalOutcome);
410
+ if (terminalOutcome?.status === 'failed')
411
+ ledger?.failWorkflowRun(ticketId, reply, terminalOutcome);
412
+ else
413
+ ledger?.settleWorkflowRun(ticketId, reply, terminalOutcome);
351
414
  const dir = await asyncDir(stateRoot);
352
415
  const p = join(dir, `${ticketId}.deliverable.md`);
353
416
  await atomicWrite(p, reply);
354
417
  return p;
355
418
  }
356
419
  function recoverAsyncTerminalOutcome(ledger, ticketId) {
357
- if (!ledger) return undefined;
420
+ if (!ledger)
421
+ return undefined;
358
422
  const run = ledger.getWorkflowRun(ticketId);
359
- if (!run) return undefined;
423
+ if (!run)
424
+ return undefined;
360
425
  try {
361
426
  const state = JSON.parse(run.state_json);
362
- if (!state.spec) return terminalOutcomeFromState(ticketId, state);
427
+ if (!state.spec)
428
+ return terminalOutcomeFromState(ticketId, state);
363
429
  const solveStep = ledger.getWorkflowStep(ticketId, 'solve');
364
- if (solveStep?.status !== 'done' || !solveStep.result) return undefined;
430
+ if (solveStep?.status !== 'done' || !solveStep.result)
431
+ return undefined;
365
432
  state.solve = JSON.parse(solveStep.result);
366
- state.gates = state.gates.filter((g)=>!g.id.startsWith('async-'));
433
+ state.gates = state.gates.filter(g => !g.id.startsWith('async-'));
367
434
  return terminalOutcomeFromState(ticketId, state);
368
- } catch {
435
+ }
436
+ catch {
369
437
  return undefined;
370
438
  }
371
439
  }
440
+ /**
441
+ * 可日志化 solve 端口(ADR-15 步骤日志,intent-before-execute):
442
+ * 先记 intent 再执行,done 后结果落账本——任意进程恢复时 done 的步骤直接复用
443
+ * 结果不重执行(exactly-once:LLM/求解调用不重复花钱),intent 悬挂的重试。
444
+ */
372
445
  export function makeJournaledSolvePort(ledger, runId, solve, opts) {
373
- return async (spec)=>{
446
+ return async (spec) => {
374
447
  const step = ledger.getWorkflowStep(runId, 'solve');
375
448
  if (step?.status === 'done' && step.result) {
376
449
  return JSON.parse(step.result);
@@ -382,30 +455,48 @@ export function makeJournaledSolvePort(ledger, runId, solve, opts) {
382
455
  return r;
383
456
  };
384
457
  }
458
+ /**
459
+ * probePoi:从 user msg 探测"查 POI/酒店" 信号,返回关键词。
460
+ * 触发的不是关键词,是**结构**;关键词抓取有方向性——
461
+ * 1) 显式搜索动词(查/搜/找/看看/推荐/告诉我/检索)+ 宾语在动词后;
462
+ * 2) 住宿名词(酒店/民宿/客栈/饭店):优先取名词**之后**的名称段(须含拉丁/数字,
463
+ * 「我订了酒店:The Title…」→ The Title…);退路是名词前 2-6 字紧邻地名
464
+ * (「大理酒店」→ 大理;「机票和」类垃圾后缀拒);
465
+ * 3) 开放问(有什么/玩什么/有哪些):取名词前的地名;
466
+ * 4) 短裸地名:≤12 字、无标点、无陈述动词(「大理」✓;「明天有空」「我的工作时间是…」✗——
467
+ * 2026-08-28 巡检收紧:旧版 ≤24 直通把访谈答案整句当关键词,垃圾 hbcli 调用+证据噪音)。
468
+ * 不是意图(改求解器),只是 datasources 编排层的"提早调 anything"提示。
469
+ */
385
470
  export function probePoi(msg) {
386
471
  const trimmed = msg.trim();
387
- if (!trimmed) return null;
388
- const cutAtPunct = (s)=>s.split(/[,,。.;;!!??]/)[0]?.trim() ?? '';
472
+ if (!trimmed)
473
+ return null;
474
+ const cutAtPunct = (s) => s.split(/[,,。.;;!!??]/)[0]?.trim() ?? '';
475
+ // 1) 显式搜索动词 + 宾语
389
476
  const m = trimmed.match(/(查一下|查|搜|找|看看|推荐|告诉我|检索)\s*(.{2,24})/);
390
477
  if (m?.[2]) {
391
478
  const obj = cutAtPunct(m[2]);
392
- if (obj.length >= 2) return obj.slice(0, 24);
479
+ if (obj.length >= 2)
480
+ return obj.slice(0, 24);
393
481
  }
482
+ // 2) 住宿名词:名称段在名词后
394
483
  if (/(酒店|民宿|客栈|饭店)/.test(trimmed)) {
395
484
  const parts = trimmed.split(/(酒店|民宿|客栈|饭店)/);
396
485
  const after = cutAtPunct((parts[2] ?? '').replace(/^[的::\s]+/, ''));
397
- if (after.length >= 2 && /[A-Za-z0-9]/.test(after)) return after.slice(0, 24);
486
+ if (after.length >= 2 && /[A-Za-z0-9]/.test(after))
487
+ return after.slice(0, 24);
398
488
  const nounAt = trimmed.search(/(酒店|民宿|客栈|饭店)/);
399
489
  const pre = trimmed.slice(Math.max(0, nounAt - 6), nounAt).match(/[一-龥]{2,6}$/);
400
- if (pre && !/(机票|车票|和|做|订|的)$/.test(pre[0])) return pre[0];
490
+ if (pre && !/(机票|车票|和|做|订|的)$/.test(pre[0]))
491
+ return pre[0];
401
492
  }
493
+ // 3) 开放问:地名在疑问词前
402
494
  const q = trimmed.match(/([一-龥a-zA-Z]{2,12})\s*(?:有什么|玩什么|有哪些)/);
403
- if (q?.[1]) return q[1];
495
+ if (q?.[1])
496
+ return q[1];
497
+ // 4) 短裸地名(≤12 字,无标点,无陈述动词)
404
498
  if (trimmed.length <= 12 && !/[,,。.;;!!??::]/.test(trimmed) && !/[是有在要想得到说订吧呢]/.test(trimmed)) {
405
499
  return trimmed.slice(0, 24);
406
500
  }
407
501
  return null;
408
502
  }
409
-
410
-
411
- //# sourceURL=ts/src/loop.ts