@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.
- package/README.md +102 -194
- package/README.zh-CN.md +115 -210
- package/bin/gotry-backend.js +53 -0
- package/bin/gotry-bootstrap.js +466 -35
- package/bin/gotry-inner.js +255 -36
- package/bin/gotry-process-liveness.d.ts +27 -0
- package/bin/gotry-process-liveness.js +97 -0
- package/bin/gotry-runtime-resolution.d.ts +1 -1
- package/bin/gotry-runtime-resolution.js +2 -2
- package/client/client.js +174 -0
- package/cordis.gotry-patch.yml +70 -93
- package/dist/capabilities/agent-reach-deep.js +45 -122
- package/dist/capabilities/agent-reach.js +63 -148
- package/dist/capabilities/anything.js +89 -75
- package/dist/capabilities/artifacts.js +123 -104
- package/dist/capabilities/channel-health.js +65 -45
- package/dist/capabilities/channel-registry.js +108 -87
- package/dist/capabilities/doctor.js +357 -235
- package/dist/capabilities/effect.js +223 -309
- package/dist/capabilities/fact-log.js +28 -14
- package/dist/capabilities/flyai.js +245 -156
- package/dist/capabilities/ground-transfer.js +509 -0
- package/dist/capabilities/hbcli.js +144 -148
- package/dist/capabilities/hotelbyte-transaction.js +194 -0
- package/dist/capabilities/incident-log.js +131 -61
- package/dist/capabilities/model-override.js +30 -10
- package/dist/capabilities/opensky.js +54 -43
- package/dist/capabilities/resilience.js +50 -32
- package/dist/capabilities/session/action-cache.js +50 -44
- package/dist/capabilities/session/adapters/ctrip-flight.js +119 -62
- package/dist/capabilities/session/adapters/ctrip-hotel.js +125 -110
- package/dist/capabilities/session/adapters/dida-portal.js +206 -0
- package/dist/capabilities/session/adapters/meituan-local.js +35 -54
- package/dist/capabilities/session/adapters/rail-12306.js +169 -112
- package/dist/capabilities/session/benchmark.js +85 -149
- package/dist/capabilities/session/extension-bridge.js +320 -259
- package/dist/capabilities/session/extension-channel.js +57 -76
- package/dist/capabilities/session/extension-distribution.js +115 -119
- package/dist/capabilities/session/extract.js +31 -21
- package/dist/capabilities/session/golden-score.js +55 -33
- package/dist/capabilities/session/health-watch.js +146 -154
- package/dist/capabilities/session/read-guard.js +55 -47
- package/dist/capabilities/session/static-flight-golden.js +49 -36
- package/dist/capabilities/session/transport.js +54 -61
- package/dist/capabilities/session/wizard.js +63 -93
- package/dist/capabilities/session-consent.js +143 -47
- package/dist/capabilities/session-login.js +95 -126
- package/dist/capabilities/session-search.js +391 -253
- package/dist/capabilities/sponsor-plugin.js +341 -0
- package/dist/capabilities/visa-policy.js +87 -73
- package/dist/capabilities/weather.js +163 -235
- package/dist/scripts/action-cache-tests.js +24 -59
- package/dist/scripts/agent-planning-turn-deadline-e2e.js +91 -142
- package/dist/scripts/agent-planning-turn-deadline-tests.js +80 -176
- package/dist/scripts/agent-reach-deep-tests.js +30 -40
- package/dist/scripts/agent-reach-tests.js +26 -18
- package/dist/scripts/agent-reach-wrapper-tests.js +46 -50
- package/dist/scripts/anything-tests.js +58 -62
- package/dist/scripts/artifact-client-contract-tests.js +69 -0
- package/dist/scripts/artifacts-capability-tests.js +235 -0
- package/dist/scripts/async-collect.js +26 -15
- package/dist/scripts/benchmark-environment-bridge-e2e.js +287 -684
- package/dist/scripts/benchmark-environment-bridge-tests.js +818 -3123
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +66 -279
- package/dist/scripts/booking-copilot-availability-policy-tests.js +153 -1257
- package/dist/scripts/booking-copilot-bin-proof-tests.js +22 -45
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +64 -76
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +69 -199
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +393 -759
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +33 -75
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +60 -110
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +28 -66
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +72 -271
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +73 -275
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +784 -3398
- package/dist/scripts/booking-copilot-server-proof-tests.js +63 -168
- package/dist/scripts/booking-copilot-startup-proof-tests.js +38 -124
- package/dist/scripts/booking-executor-tests.js +80 -0
- package/dist/scripts/booking-recovery-chain-tests.js +182 -0
- package/dist/scripts/booking-saga-tests.js +61 -123
- package/dist/scripts/booking-surface-contract-proof-tests.js +152 -1167
- package/dist/scripts/bootstrap-tests.js +1383 -128
- package/dist/scripts/build-changelog.js +89 -130
- package/dist/scripts/build-metrics-report.js +135 -143
- package/dist/scripts/changelog-tests.js +35 -82
- package/dist/scripts/channel-probe-tests.js +44 -116
- package/dist/scripts/channel-probe.js +84 -167
- package/dist/scripts/channel-registry-tests.js +97 -185
- package/dist/scripts/companion-tests.js +25 -70
- package/dist/scripts/dida-cookie-probe.js +87 -0
- package/dist/scripts/dida-sw-probe.js +114 -0
- package/dist/scripts/diff-test.js +17 -12
- package/dist/scripts/doctor-tests.js +428 -120
- package/dist/scripts/dsh-artifact-e2e.js +150 -0
- package/dist/scripts/dsh-artifact-web-e2e.js +443 -0
- package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
- package/dist/scripts/dsh-runtime-closure-tests.js +110 -153
- package/dist/scripts/dsh-runtime-closure.js +75 -63
- package/dist/scripts/dsh-session-v3-migration-proof.js +106 -0
- package/dist/scripts/dsh-subprocess-local-proof.js +149 -0
- package/dist/scripts/dsh-target-closure-proof.js +178 -0
- package/dist/scripts/effect-tests.js +165 -552
- package/dist/scripts/engine-run.js +1 -3
- package/dist/scripts/engine-tests.js +16 -10
- package/dist/scripts/evaluation-cadence-tests.js +69 -242
- package/dist/scripts/evaluation-contract-tests.js +193 -499
- package/dist/scripts/extension-distribution-cli.js +19 -15
- package/dist/scripts/extension-distribution-tests.js +98 -211
- package/dist/scripts/extension-tests.js +366 -389
- package/dist/scripts/fact-gate-tests.js +725 -263
- package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
- package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
- package/dist/scripts/flight-malformed-tests.js +159 -0
- package/dist/scripts/flyai-tests.js +325 -68
- package/dist/scripts/g5-guard-tests.js +131 -0
- package/dist/scripts/g5-guard.js +189 -0
- package/dist/scripts/gen-lunar-anchors.js +106 -0
- package/dist/scripts/gotry-backend-kernel-tests.js +79 -0
- package/dist/scripts/ground-transfer-tests.js +956 -0
- package/dist/scripts/hbcli-e2e-tests.js +71 -90
- package/dist/scripts/hbcli-live-optin-tests.js +96 -0
- package/dist/scripts/hbcli-release-contract-tests.js +122 -0
- package/dist/scripts/hbcli-tests.js +41 -115
- package/dist/scripts/health-watch-cli.js +42 -28
- package/dist/scripts/hotel-date-gate-tests.js +496 -0
- package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
- package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
- package/dist/scripts/i18n-tests.js +16 -9
- package/dist/scripts/incident-tests.js +281 -87
- package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
- package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
- package/dist/scripts/issue-271-liveness-tests.js +547 -0
- package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
- package/dist/scripts/issue-289-model-retry-tests.js +441 -0
- package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
- package/dist/scripts/issue-338-home-city-e2e.js +577 -0
- package/dist/scripts/issue343-real-entry-e2e.js +160 -0
- package/dist/scripts/journey-tests.js +22 -27
- package/dist/scripts/kernel-manifest-evidence.js +109 -0
- package/dist/scripts/kernel-manifest-gate.js +126 -0
- package/dist/scripts/kernel-manifest-tests.js +212 -0
- package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
- package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
- package/dist/scripts/kernel-manifest.js +369 -0
- package/dist/scripts/ledger-repair-apply-tests.js +316 -0
- package/dist/scripts/ledger-repair-plan-tests.js +375 -0
- package/dist/scripts/ledger-tests.js +325 -340
- package/dist/scripts/ledger-workflow-crash.js +10 -20
- package/dist/scripts/map-tools-vendor-package-proof.js +206 -134
- package/dist/scripts/memory-capture-tests.js +242 -61
- package/dist/scripts/memory-decay-tests.js +18 -52
- package/dist/scripts/memory-lifecycle-tests.js +657 -0
- package/dist/scripts/memory-lifecycle.js +196 -0
- package/dist/scripts/memory-metrics.js +24 -12
- package/dist/scripts/memory-value-report.js +474 -222
- package/dist/scripts/memory-value-tests.js +328 -0
- package/dist/scripts/metrics-report-tests.js +62 -199
- package/dist/scripts/model-override-e2e.js +66 -105
- package/dist/scripts/nightly-evidence-tests.js +64 -89
- package/dist/scripts/nightly-evidence.js +108 -111
- package/dist/scripts/nudge-digest.js +39 -28
- package/dist/scripts/onboarding-tests.js +63 -88
- package/dist/scripts/opensky-check.js +18 -13
- package/dist/scripts/opensky-tests.js +28 -28
- package/dist/scripts/persona-surface-guard-tests.js +37 -12
- package/dist/scripts/pnpm-dsh-closure-proof.js +2 -5
- package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
- package/dist/scripts/price-drift-tests.js +126 -227
- package/dist/scripts/price-drift-watch.js +209 -253
- package/dist/scripts/probe-poi-tests.js +52 -45
- package/dist/scripts/product-metrics.js +187 -273
- package/dist/scripts/publish-preverify.js +47 -41
- package/dist/scripts/realtime-pricing-tests.js +37 -76
- package/dist/scripts/replay-async.js +20 -18
- package/dist/scripts/replay-real.js +16 -18
- package/dist/scripts/replay.js +52 -82
- package/dist/scripts/session-attach-diagnose.js +16 -12
- package/dist/scripts/session-attach-poc.js +34 -37
- package/dist/scripts/session-benchmark.js +66 -147
- package/dist/scripts/session-dida-live-e2e.js +151 -0
- package/dist/scripts/session-extract-tests.js +14 -35
- package/dist/scripts/session-login.js +14 -13
- package/dist/scripts/session-tests.js +545 -583
- package/dist/scripts/sf-live-benchmark.js +92 -110
- package/dist/scripts/sf-live-cli-tests.js +2 -9
- package/dist/scripts/sf-soft-score-tests.js +19 -60
- package/dist/scripts/sf-summary-tests.js +149 -0
- package/dist/scripts/sf-summary.js +466 -84
- package/dist/scripts/skeleton-check.js +16 -14
- package/dist/scripts/skeleton-integration-test.js +11 -10
- package/dist/scripts/skills-contract-tests.js +51 -46
- package/dist/scripts/smoke-session-gate-tests.js +12 -20
- package/dist/scripts/smoke.js +541 -422
- package/dist/scripts/sponsor-reuse-tests.js +262 -0
- package/dist/scripts/state-cli-tests.js +226 -101
- package/dist/scripts/state-cli.js +549 -212
- package/dist/scripts/static-golden-tests.js +46 -180
- package/dist/scripts/time-eval-tests.js +210 -192
- package/dist/scripts/travel-timeline-tests.js +29 -93
- package/dist/scripts/turn-handoff-collect-tests.js +46 -93
- package/dist/scripts/turn-handoff-collect.js +71 -99
- package/dist/scripts/turn-policy-tests.js +28 -29
- package/dist/scripts/typed-contract-canary.js +77 -162
- package/dist/scripts/tz-resolver-sanity.js +50 -0
- package/dist/scripts/tz-resolver-tests.js +584 -0
- package/dist/scripts/unified-tests.js +23 -19
- package/dist/scripts/visa-policy-tests.js +31 -44
- package/dist/scripts/weather-tests.js +121 -543
- package/dist/scripts/wish-channel-gate-tests.js +29 -86
- package/dist/scripts/write-gate-tests.js +522 -0
- package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
- package/dist/scripts/z3-lifecycle-tests.js +170 -0
- package/dist/scripts/z3-race-repeat-tests.js +39 -0
- package/dist/scripts/z3-race-tests.js +18 -12
- package/dist/src/artifact-gate.js +652 -221
- package/dist/src/backend/kernel.js +91 -0
- package/dist/src/backend/modules/booking-copilot.js +55 -0
- package/dist/src/backend/modules/booking-executor.js +124 -0
- package/dist/src/backend/modules/session-search.js +194 -0
- package/dist/src/benchmark-agent-conformance.js +296 -262
- package/dist/src/benchmark-environment-bridge.js +298 -413
- package/dist/src/benchmark-headless-child-diagnostics.js +93 -100
- package/dist/src/benchmark-tool-isolation.js +94 -67
- package/dist/src/bookable-facts.js +299 -174
- package/dist/src/booking-saga.js +64 -120
- package/dist/src/booking-surface/availability-policy.js +443 -576
- package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
- package/dist/src/booking-surface/contracts.js +24 -80
- package/dist/src/booking-surface/dsh-planner.js +510 -281
- package/dist/src/booking-surface/dsh-plugin.js +17 -1
- package/dist/src/booking-surface/error-codes.js +82 -10
- package/dist/src/booking-surface/index.js +0 -3
- package/dist/src/booking-surface/profile.js +10 -54
- package/dist/src/booking-surface/recovery-chain.js +367 -0
- package/dist/src/booking-surface/runtime.js +765 -891
- package/dist/src/booking-surface/server.js +200 -251
- package/dist/src/booking-surface/startup.js +58 -54
- package/dist/src/booking-surface/validation.js +150 -218
- package/dist/src/bridge.js +20 -13
- package/dist/src/companions.js +48 -74
- package/dist/src/contracts.js +21 -43
- package/dist/src/dsh-llm.js +82 -120
- package/dist/src/engine.js +253 -239
- package/dist/src/evaluation-cadence.js +72 -209
- package/dist/src/evaluation-contracts.js +220 -843
- package/dist/src/flight-pack-adapter.js +54 -0
- package/dist/src/flight-pack-contract.js +2 -0
- package/dist/src/gotry-backend.js +70 -0
- package/dist/src/hotel-date-gate.js +118 -0
- package/dist/src/i18n.js +18 -7
- package/dist/src/index.js +1425 -1629
- package/dist/src/journey.js +132 -81
- package/dist/src/ledger-repair-apply.js +538 -0
- package/dist/src/ledger-repair-plan.js +463 -0
- package/dist/src/loop.js +257 -166
- package/dist/src/memory-capture.js +180 -32
- package/dist/src/memory-decay.js +30 -14
- package/dist/src/memory-lifecycle.js +1310 -0
- package/dist/src/memory-utility.js +27 -40
- package/dist/src/mock-llm.js +36 -61
- package/dist/src/model.js +81 -36
- package/dist/src/realtime-pricing.js +63 -72
- package/dist/src/slot-spec.js +84 -106
- package/dist/src/state-ledger.js +310 -471
- package/dist/src/subagent-job-id-guard.js +58 -0
- package/dist/src/time-anchor.js +231 -33
- package/dist/src/tool-packet.js +20 -8
- package/dist/src/travel-slots.js +58 -35
- package/dist/src/travel-timeline.js +44 -63
- package/dist/src/turn-deadline.js +121 -127
- package/dist/src/turn-policy.js +61 -89
- package/dist/src/tz-resolver.js +143 -0
- package/dist/src/unified.js +504 -381
- package/dist/src/wish-pool.js +39 -14
- package/dist/src/write-gate.js +495 -0
- package/dist/src/z3-shared.js +273 -7
- package/extension/README.md +4 -3
- package/extension/background.js +125 -14
- package/extension/content-bridge.js +23 -1
- package/extension/content-main.js +15 -4
- package/extension/manifest.json +16 -8
- package/package.json +251 -220
- package/ts/capabilities/agent-reach.ts +4 -4
- package/ts/capabilities/anything.ts +1 -1
- package/ts/capabilities/artifacts.ts +46 -9
- package/ts/capabilities/flyai.ts +156 -37
- package/ts/capabilities/hbcli.ts +1 -1
- package/ts/capabilities/incident-log.ts +88 -59
- package/ts/capabilities/session/adapters/ctrip-flight.ts +99 -36
- package/ts/capabilities/session/adapters/dida-portal.ts +250 -0
- package/ts/capabilities/session/adapters/rail-12306.ts +123 -24
- package/ts/capabilities/session/extension-bridge.ts +226 -101
- package/ts/capabilities/session/extension-channel.ts +22 -9
- package/ts/capabilities/session/wizard.ts +2 -2
- package/ts/capabilities/session-consent.ts +98 -14
- package/ts/capabilities/session-login.ts +1 -0
- package/ts/capabilities/session-search.ts +315 -31
- package/ts/dsh-runtime/vendor/README.md +8 -4
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +7 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +5 -3
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +1 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +1 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +6 -6
- package/ts/package.json +236 -16
- package/ts/scripts/map-tools-vendor-package-proof.ts +152 -31
- package/ts/scripts/state-cli.ts +418 -51
- package/ts/src/artifact-gate.ts +561 -15
- package/ts/src/benchmark-environment-bridge.ts +37 -20
- package/ts/src/bookable-facts.ts +183 -7
- package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
- package/ts/src/booking-surface/dsh-planner.ts +240 -32
- package/ts/src/booking-surface/dsh-plugin.js +17 -1
- package/ts/src/booking-surface/error-codes.ts +75 -0
- package/ts/src/booking-surface/recovery-chain.ts +409 -0
- package/ts/src/booking-surface/server.ts +84 -51
- package/ts/src/booking-surface/validation.ts +2 -0
- package/ts/src/contracts.ts +12 -2
- package/ts/src/dsh-llm.ts +4 -8
- package/ts/src/index.ts +609 -90
- package/ts/src/loop.ts +116 -7
- package/ts/src/memory-capture.ts +227 -3
- package/ts/src/mock-llm.ts +4 -8
- package/ts/src/model.ts +88 -2
- package/ts/src/state-ledger.ts +47 -29
- package/ts/src/subagent-job-id-guard.ts +72 -0
- package/ts/src/time-anchor.ts +217 -13
- package/ts/src/unified.ts +316 -54
|
@@ -1,168 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hotelbyte-cli(hbcli)能力层封装:进程内 spawn bun + hbcli --json。
|
|
3
|
+
*
|
|
4
|
+
* 拿到外部资源信息能力的三条路径:
|
|
5
|
+
* 1. 实时 hbcli 调用(能力强,延迟 1-3s,需凭证 + 证书有效)
|
|
6
|
+
* 2. 静态数据包(data/hotels_2026.json)——证据标注 [静态包:估算]
|
|
7
|
+
* 3. 简易降级返回(什么都不查得到时,提供"工具暂不可用"而不是抛错)
|
|
8
|
+
*
|
|
9
|
+
* 证书/凭证/网络问题一律降级,不抛到调用方——能力层契约:永远返回一种结果。
|
|
10
|
+
*
|
|
11
|
+
* §7-1/§7-2 L4 不变量:证据链标注([实时API:hbcli@<ts>] / [静态包:估算]);
|
|
12
|
+
* 估算必须显式标记。这是 L4 与 L1 透明卡片的接缝。
|
|
13
|
+
*/
|
|
1
14
|
import { spawn } from 'node:child_process';
|
|
2
15
|
import { homedir } from 'node:os';
|
|
3
16
|
import { join } from 'node:path';
|
|
17
|
+
/**
|
|
18
|
+
* hbcli 二进制候选路径(gotry setup 按官方脚本装到 ~/.local/bin/hbcli,
|
|
19
|
+
* symlink 指向 ~/.staicli/current/hbcli——当 PATH 不含 ~/.local/bin 时裸名
|
|
20
|
+
* spawn 仍 ENOENT,按已知安装位回退)。仅对默认名 'hbcli' 扩展;显式自定义
|
|
21
|
+
* 名(如测试注入的不存在路径)不扩展,保持配置即所用的可测性。
|
|
22
|
+
*/
|
|
4
23
|
export function hbcliBinCandidates(bin, homeDir = homedir()) {
|
|
5
|
-
if (bin !== 'hbcli')
|
|
6
|
-
bin
|
|
7
|
-
];
|
|
8
|
-
return [
|
|
9
|
-
bin,
|
|
10
|
-
join(homeDir, '.local/bin/hbcli'),
|
|
11
|
-
join(homeDir, '.staicli/current/hbcli')
|
|
12
|
-
];
|
|
24
|
+
if (bin !== 'hbcli')
|
|
25
|
+
return [bin];
|
|
26
|
+
return [bin, join(homeDir, '.local/bin/hbcli'), join(homeDir, '.staicli/current/hbcli')];
|
|
13
27
|
}
|
|
28
|
+
/** 单个候选的一次 spawn 封装:失败不抛,返回降级结果(spawnError 标记 ENOENT 类失败供上层换候选) */
|
|
14
29
|
function attemptHbcli(bin, args, opts) {
|
|
15
30
|
const started = Date.now();
|
|
16
|
-
return new Promise((resolve)=>{
|
|
31
|
+
return new Promise((resolve) => {
|
|
17
32
|
let stdout = '';
|
|
18
33
|
let stderr = '';
|
|
19
34
|
let settled = false;
|
|
20
|
-
const child = spawn(bin, args, {
|
|
21
|
-
|
|
22
|
-
...process.env,
|
|
23
|
-
...opts.envVars
|
|
24
|
-
}
|
|
25
|
-
});
|
|
26
|
-
const timer = setTimeout(()=>{
|
|
35
|
+
const child = spawn(bin, args, { env: { ...process.env, ...opts.envVars } });
|
|
36
|
+
const timer = setTimeout(() => {
|
|
27
37
|
if (!settled) {
|
|
28
38
|
settled = true;
|
|
29
39
|
child.kill('SIGKILL');
|
|
30
40
|
resolve({
|
|
31
|
-
via: 'hbcli-error',
|
|
32
|
-
exitCode: -1,
|
|
33
|
-
result: null,
|
|
41
|
+
via: 'hbcli-error', exitCode: -1, result: null,
|
|
34
42
|
evidence: `[实时API:hbcli@timeout@${new Date().toISOString()}]`,
|
|
35
|
-
latencyMs: Date.now() - started,
|
|
36
|
-
error: `timeout after ${opts.timeoutMs}ms`
|
|
43
|
+
latencyMs: Date.now() - started, error: `timeout after ${opts.timeoutMs}ms`,
|
|
37
44
|
});
|
|
38
45
|
}
|
|
39
46
|
}, opts.timeoutMs);
|
|
40
|
-
child.stdout.on('data', (d)=>{
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
});
|
|
46
|
-
child.on('close', (code)=>{
|
|
47
|
-
if (settled) return;
|
|
47
|
+
child.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
48
|
+
child.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
49
|
+
child.on('close', (code) => {
|
|
50
|
+
if (settled)
|
|
51
|
+
return;
|
|
48
52
|
settled = true;
|
|
49
53
|
clearTimeout(timer);
|
|
50
54
|
const latencyMs = Date.now() - started;
|
|
51
55
|
if (code !== 0) {
|
|
56
|
+
// 不抛错,降级返回。证据链显式标注:实时 API 调用失败+原因+时间戳。
|
|
52
57
|
resolve({
|
|
53
|
-
via: 'hbcli-error',
|
|
54
|
-
exitCode: code ?? -1,
|
|
55
|
-
result: null,
|
|
58
|
+
via: 'hbcli-error', exitCode: code ?? -1, result: null,
|
|
56
59
|
evidence: `[实时API:hbcli@error@${new Date().toISOString()}]`,
|
|
57
60
|
stderr: stderr.slice(0, 2000),
|
|
58
|
-
latencyMs,
|
|
59
|
-
error: stderr.trim().slice(0, 200) || `exit ${code}`
|
|
61
|
+
latencyMs, error: stderr.trim().slice(0, 200) || `exit ${code}`,
|
|
60
62
|
});
|
|
61
63
|
return;
|
|
62
64
|
}
|
|
65
|
+
// 尝试 JSON 解析
|
|
63
66
|
const jStart = stdout.search(/[\{\[]/);
|
|
64
67
|
const jsonStr = jStart >= 0 ? stdout.slice(jStart) : '';
|
|
65
68
|
let result = null;
|
|
66
69
|
if (jsonStr) {
|
|
67
70
|
try {
|
|
68
71
|
result = JSON.parse(jsonStr);
|
|
69
|
-
}
|
|
72
|
+
}
|
|
73
|
+
catch { /* 非 JSON 输出,留给调用方处理 */ }
|
|
70
74
|
}
|
|
71
75
|
resolve({
|
|
72
|
-
via: 'hbcli-realtime',
|
|
73
|
-
exitCode: 0,
|
|
74
|
-
result,
|
|
76
|
+
via: 'hbcli-realtime', exitCode: 0, result,
|
|
75
77
|
evidence: `[实时API:hbcli@${new Date().toISOString()}]`,
|
|
76
|
-
stdout: stdout.slice(0, 2000),
|
|
77
|
-
latencyMs
|
|
78
|
+
stdout: stdout.slice(0, 2000), latencyMs,
|
|
78
79
|
});
|
|
79
80
|
});
|
|
80
|
-
child.on('error', (e)=>{
|
|
81
|
-
if (settled)
|
|
81
|
+
child.on('error', (e) => {
|
|
82
|
+
if (settled)
|
|
83
|
+
return;
|
|
82
84
|
settled = true;
|
|
83
85
|
clearTimeout(timer);
|
|
86
|
+
// ENOENT (二进制不存在) 等也走降级路径;spawnError 供上层按候选路径重试
|
|
84
87
|
resolve({
|
|
85
|
-
via: 'hbcli-error',
|
|
86
|
-
exitCode: -1,
|
|
87
|
-
result: null,
|
|
88
|
+
via: 'hbcli-error', exitCode: -1, result: null,
|
|
88
89
|
evidence: `[实时API:hbcli@spawn_error@${new Date().toISOString()}]`,
|
|
89
|
-
latencyMs: Date.now() - started,
|
|
90
|
-
error: e.message,
|
|
91
|
-
spawnError: true
|
|
90
|
+
latencyMs: Date.now() - started, error: e.message, spawnError: true,
|
|
92
91
|
});
|
|
93
92
|
});
|
|
94
93
|
});
|
|
95
94
|
}
|
|
95
|
+
/** 通用 hbcli JSON 调用封装:失败不抛,而是返回降级结果 */
|
|
96
96
|
export async function callHbcliJson(args, opts = {}) {
|
|
97
97
|
const env = opts.env ?? 'uat';
|
|
98
|
-
const envVars = {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const callOpts = {
|
|
103
|
-
timeoutMs: opts.timeoutMs ?? 15_000,
|
|
104
|
-
env,
|
|
105
|
-
envVars
|
|
106
|
-
};
|
|
98
|
+
const envVars = { HOTELBYTE_ENV: env };
|
|
99
|
+
if (opts.token)
|
|
100
|
+
envVars['HOTELBYTE_TOKEN'] = opts.token;
|
|
101
|
+
const callOpts = { timeoutMs: opts.timeoutMs ?? 15_000, env, envVars };
|
|
107
102
|
const candidates = hbcliBinCandidates(opts.hbcliBin ?? 'hbcli');
|
|
108
103
|
let last;
|
|
109
|
-
for (const bin of candidates){
|
|
104
|
+
for (const bin of candidates) {
|
|
110
105
|
last = await attemptHbcli(bin, args, callOpts);
|
|
111
|
-
|
|
106
|
+
// spawn 级失败(ENOENT 等)且还有候选 → 换下一个已知安装位;其余失败(退码/超时)无重试意义
|
|
107
|
+
if (!(last.spawnError && candidates.indexOf(bin) < candidates.length - 1))
|
|
108
|
+
return last;
|
|
112
109
|
}
|
|
113
110
|
return last;
|
|
114
111
|
}
|
|
112
|
+
/** 上游 i18n 名称容错读取(zh 优先,中文优先产品口径;回退 en/ar) */
|
|
115
113
|
function i18nName(v) {
|
|
116
|
-
if (!v)
|
|
117
|
-
|
|
114
|
+
if (!v)
|
|
115
|
+
return '';
|
|
116
|
+
if (typeof v === 'string')
|
|
117
|
+
return v;
|
|
118
118
|
return v.zh || v.en || v.ar || '';
|
|
119
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* 实时 hotelList 响应 → 模型可读紧凑行(id/名称/星级/最低价)。
|
|
122
|
+
* issue #195:render 面只透 summary,若 summary 不含数据,模型只看到
|
|
123
|
+
* 「实时返回」四个字——工具调用等于白调。id 必须在行内,hotel-rates 的入参
|
|
124
|
+
* 就来自这里。上游 wire:HotelListResp{list:[Hotel{id,name:{en,zh},star,minPrice:{amount,currency}}]}。
|
|
125
|
+
*/
|
|
120
126
|
function hotelLines(result, max = 8) {
|
|
121
127
|
const list = result?.list;
|
|
122
|
-
if (!Array.isArray(list) || list.length === 0)
|
|
128
|
+
if (!Array.isArray(list) || list.length === 0)
|
|
129
|
+
return '';
|
|
123
130
|
const lines = [];
|
|
124
|
-
for (const item of list.slice(0, max)){
|
|
131
|
+
for (const item of list.slice(0, max)) {
|
|
125
132
|
const h = item;
|
|
126
133
|
const name = i18nName(h.name) || `hotel ${h.id ?? '?'}`;
|
|
127
134
|
const star = typeof h.star === 'number' && h.star > 0 ? ` ${h.star}★` : '';
|
|
128
|
-
const price = h.minPrice && typeof h.minPrice.amount === 'number'
|
|
135
|
+
const price = h.minPrice && typeof h.minPrice.amount === 'number'
|
|
136
|
+
? ` 低至 ${h.minPrice.amount}${h.minPrice.currency ? ` ${h.minPrice.currency}` : ''}`
|
|
137
|
+
: '';
|
|
129
138
|
lines.push(`- id=${h.id ?? '?'} ${name}${star}${price}`);
|
|
130
139
|
}
|
|
131
|
-
if (list.length > max)
|
|
140
|
+
if (list.length > max)
|
|
141
|
+
lines.push(`…(共 ${list.length} 家,仅列前 ${max})`);
|
|
132
142
|
return lines.join('\n');
|
|
133
143
|
}
|
|
144
|
+
/** 高层语义化封装:酒店列表查询(down-tier to 静态包 + 证据链标注) */
|
|
134
145
|
export async function searchHotels(query, opts = {}) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
'
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (query.
|
|
144
|
-
|
|
145
|
-
if (query.adults)
|
|
146
|
-
{
|
|
147
|
-
adultCount: query.adults,
|
|
148
|
-
childrenAges: []
|
|
149
|
-
}
|
|
150
|
-
]));
|
|
146
|
+
// 旗标对齐上游 CLI(hotelbyte-com/hotelbyte-cli):--destination-name + --room-occupancies;
|
|
147
|
+
// hotel-list 支持日期旗标(--check-in/--check-out,上游帮助文档在册)——issue #195 教训:
|
|
148
|
+
// 「日期不传上游」会让实时价退化为当前窗口价,用户要下周五的房给的是今天的价。
|
|
149
|
+
const hbArgs = ['search', 'hotel-list', '--json', '--page-size', '10'];
|
|
150
|
+
if (query.destination)
|
|
151
|
+
hbArgs.push('--destination-name', query.destination);
|
|
152
|
+
if (query.checkIn)
|
|
153
|
+
hbArgs.push('--check-in', query.checkIn);
|
|
154
|
+
if (query.checkOut)
|
|
155
|
+
hbArgs.push('--check-out', query.checkOut);
|
|
156
|
+
if (query.adults)
|
|
157
|
+
hbArgs.push('--room-occupancies', JSON.stringify([{ adultCount: query.adults, childrenAges: [] }]));
|
|
151
158
|
const live = await callHbcliJson(hbArgs, opts);
|
|
152
159
|
if (live.via === 'hbcli-realtime') {
|
|
160
|
+
// summary 即模型面(render 只透 summary):必须自带紧凑数据行 + 证据链,
|
|
161
|
+
// 否则模型只看到「实时返回」占位一句话(issue #195 实锤)。
|
|
153
162
|
const list = live.result?.list;
|
|
154
163
|
const n = Array.isArray(list) ? list.length : 0;
|
|
155
|
-
const dateTag = query.checkIn
|
|
164
|
+
const dateTag = query.checkIn
|
|
165
|
+
? `入住 ${query.checkIn}${query.checkOut ? ` → 退房 ${query.checkOut}` : ''}`
|
|
166
|
+
: '未传日期(上游按当前窗口价)';
|
|
156
167
|
const lines = hotelLines(live.result);
|
|
157
168
|
const summary = `${query.destination}:hbcli 实时 ${n} 家(${dateTag})${lines ? `\n${lines}` : ''}\n${live.evidence}`;
|
|
158
|
-
return {
|
|
159
|
-
...live,
|
|
160
|
-
hotels: live.result,
|
|
161
|
-
summary
|
|
162
|
-
};
|
|
169
|
+
return { ...live, hotels: live.result, summary };
|
|
163
170
|
}
|
|
171
|
+
// 降级原因人话化(issue #24):hbcli 未安装时按 gotry setup 指引(npm 安装期已
|
|
172
|
+
// 自动跑过官方脚本;PATH 未含 ~/.local/bin 时上方候选路径也已兜住),
|
|
173
|
+
// 裸 "spawn hbcli ENOENT" 读起来像工具坏了——实际静态包降级是设计行为
|
|
164
174
|
const rawReason = live.error ?? live.via;
|
|
165
|
-
const reason = /ENOENT/i.test(rawReason) ? '未安装 hbcli(可选实时源;npx gotry setup 可按官方脚本安装)' : rawReason;
|
|
175
|
+
const reason = /ENOENT/i.test(rawReason) ? '未安装 hbcli(可选实时源;npx @danceiny/gotry setup 可按官方脚本安装)' : rawReason;
|
|
176
|
+
// 降级:读静态包,按目的地过滤命中的住宿块(issue #24)——整包倾倒会把无关场景
|
|
177
|
+
// (深圳/普吉/曼谷/云南/大理混装)灌给模型且不指明哪块相关;包内无该目的地时明示
|
|
178
|
+
// 「无数据」而不是伪装成可用结果。
|
|
166
179
|
const fallback = opts.fallbackPath;
|
|
167
180
|
if (fallback) {
|
|
168
181
|
try {
|
|
@@ -170,81 +183,64 @@ export async function searchHotels(query, opts = {}) {
|
|
|
170
183
|
const pack = JSON.parse(await readFile(fallback, 'utf-8'));
|
|
171
184
|
const kw = query.destination.trim();
|
|
172
185
|
const stays = Array.isArray(pack.stays) ? pack.stays : [];
|
|
173
|
-
const matched = kw ? stays.filter(
|
|
186
|
+
const matched = kw ? stays.filter(s => JSON.stringify(s).includes(kw)) : stays;
|
|
174
187
|
if (matched.length) {
|
|
175
188
|
return {
|
|
176
189
|
...live,
|
|
177
|
-
hotels: {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
summary: `${query.destination}:hbcli 实时源不可用(${reason}),已降级到静态包(公开渠道估算,非实时),命中 ${matched.length} 个住宿块:\n${JSON.stringify(matched).slice(0, 1600)}
|
|
190
|
+
hotels: { stays: matched },
|
|
191
|
+
// summary 即模型面:静态块内容(note/options/recommendation)直接内联,
|
|
192
|
+
// 模型不必再猜「命中 N 块」里有什么(issue #195)
|
|
193
|
+
summary: `${query.destination}:hbcli 实时源不可用(${reason}),已降级到静态包(公开渠道估算,非实时),命中 ${matched.length} 个住宿块:\n${JSON.stringify(matched).slice(0, 1600)}`,
|
|
181
194
|
};
|
|
182
195
|
}
|
|
183
196
|
return {
|
|
184
197
|
...live,
|
|
185
198
|
hotels: null,
|
|
186
|
-
summary: `${query.destination}:hbcli 实时源不可用(${reason}),且静态包无「${query.destination}」住宿数据(静态包仅覆盖内置场景)
|
|
199
|
+
summary: `${query.destination}:hbcli 实时源不可用(${reason}),且静态包无「${query.destination}」住宿数据(静态包仅覆盖内置场景)`,
|
|
187
200
|
};
|
|
188
|
-
}
|
|
201
|
+
}
|
|
202
|
+
catch { /* 静态包读不到也优雅降级 */ }
|
|
189
203
|
}
|
|
190
|
-
return {
|
|
191
|
-
...live,
|
|
192
|
-
summary: `${query.destination}:hbcli 实时源不可用(${reason})且无静态包(仅返回错误)`
|
|
193
|
-
};
|
|
204
|
+
return { ...live, summary: `${query.destination}:hbcli 实时源不可用(${reason})且无静态包(仅返回错误)` };
|
|
194
205
|
}
|
|
206
|
+
/** 高层语义化封装:目的地列表(无数据依赖,通常 hbcli dest 命令可独立调通) */
|
|
195
207
|
export async function listDestinations(opts = {}) {
|
|
196
|
-
const r = await callHbcliJson([
|
|
197
|
-
|
|
198
|
-
'destinations',
|
|
199
|
-
'--json'
|
|
200
|
-
], opts);
|
|
201
|
-
return {
|
|
202
|
-
...r,
|
|
203
|
-
destinations: r.result
|
|
204
|
-
};
|
|
208
|
+
const r = await callHbcliJson(['search', 'destinations', '--json'], opts);
|
|
209
|
+
return { ...r, destinations: r.result };
|
|
205
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* 高层语义化封装:房型报价(hotel-rates;上游 hotelRates 创建后端 session,
|
|
213
|
+
* 产出带 RatePkgId 的房型列表——check-avail 与 book 的入参来源)。
|
|
214
|
+
*
|
|
215
|
+
* 价格面无静态降级(fail-closed):hotel-list 的静态包降级是定性住宿信息,
|
|
216
|
+
* 房价是钱——估算价格冒充实时价是红线;不可用就诚实失败,证据链标注,
|
|
217
|
+
* 与 bookable-facts 证据分级(live_inventory 才可进确认卡)同口径。
|
|
218
|
+
*/
|
|
206
219
|
export async function hotelRates(query, opts = {}) {
|
|
207
|
-
const hbArgs = [
|
|
208
|
-
|
|
209
|
-
'
|
|
210
|
-
|
|
211
|
-
'--
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (query.
|
|
215
|
-
|
|
216
|
-
if (query.
|
|
217
|
-
|
|
218
|
-
if (query.
|
|
219
|
-
|
|
220
|
-
{
|
|
221
|
-
adultCount: query.adults,
|
|
222
|
-
childrenAges: []
|
|
223
|
-
}
|
|
224
|
-
]));
|
|
220
|
+
const hbArgs = ['search', 'hotel-rates', '--json', '--hotel-id', query.hotelId];
|
|
221
|
+
if (query.checkIn)
|
|
222
|
+
hbArgs.push('--check-in', query.checkIn);
|
|
223
|
+
if (query.checkOut)
|
|
224
|
+
hbArgs.push('--check-out', query.checkOut);
|
|
225
|
+
if (query.countryCode)
|
|
226
|
+
hbArgs.push('--country-code', query.countryCode);
|
|
227
|
+
if (query.nationalityCode)
|
|
228
|
+
hbArgs.push('--nationality-code', query.nationalityCode);
|
|
229
|
+
if (query.residencyCode)
|
|
230
|
+
hbArgs.push('--residency-code', query.residencyCode);
|
|
231
|
+
if (query.adults)
|
|
232
|
+
hbArgs.push('--room-occupancies', JSON.stringify([{ adultCount: query.adults, childrenAges: [] }]));
|
|
225
233
|
const live = await callHbcliJson(hbArgs, opts);
|
|
226
|
-
const summary = live.via === 'hbcli-realtime'
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
summary
|
|
231
|
-
};
|
|
234
|
+
const summary = live.via === 'hbcli-realtime'
|
|
235
|
+
? `hotel ${query.hotelId}:hbcli 实时报价${query.checkIn ? `(${query.checkIn} → ${query.checkOut ?? '?'})` : ''}`
|
|
236
|
+
: `hotel ${query.hotelId}:hbcli 实时报价不可用(${live.error ?? live.via});价格面无静态降级,fail-closed 不估算`;
|
|
237
|
+
return { ...live, rates: live.result, summary };
|
|
232
238
|
}
|
|
239
|
+
/** 高层语义化封装:下单前实时验价(check-avail;入参 RatePkgId 来自 hotel-rates 产物)。价格面同上:不可用即诚实失败。 */
|
|
233
240
|
export async function checkAvail(query, opts = {}) {
|
|
234
|
-
const live = await callHbcliJson([
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
query.ratePkgId
|
|
240
|
-
], opts);
|
|
241
|
-
const summary = live.via === 'hbcli-realtime' ? `ratePkg ${query.ratePkgId}:hbcli 实时验价(库存与价格以后端为准)` : `ratePkg ${query.ratePkgId}:hbcli 实时验价不可用(${live.error ?? live.via});价格面无静态降级,fail-closed`;
|
|
242
|
-
return {
|
|
243
|
-
...live,
|
|
244
|
-
avail: live.result,
|
|
245
|
-
summary
|
|
246
|
-
};
|
|
241
|
+
const live = await callHbcliJson(['search', 'check-avail', '--json', '--rate-pkg-id', query.ratePkgId], opts);
|
|
242
|
+
const summary = live.via === 'hbcli-realtime'
|
|
243
|
+
? `ratePkg ${query.ratePkgId}:hbcli 实时验价(库存与价格以后端为准)`
|
|
244
|
+
: `ratePkg ${query.ratePkgId}:hbcli 实时验价不可用(${live.error ?? live.via});价格面无静态降级,fail-closed`;
|
|
245
|
+
return { ...live, avail: live.result, summary };
|
|
247
246
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
//# sourceURL=ts/capabilities/hbcli.ts
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HotelByte 交易 bridge 与 unknown 查单对账契约层(issue #232 契约/模块切片)。
|
|
3
|
+
*
|
|
4
|
+
* ⚠ 非运行时激活:本模块是纯函数契约层,不 spawn 子进程、不发起网络请求、
|
|
5
|
+
* 不读取任何凭据,与真实 HotelByte 二进制/UAT 零接触(见 issue #318 的
|
|
6
|
+
* GOTRY_HBCLI_LIVE 显式 opt-in 门——真实调用面在 M5 Entry 后才允许接线,
|
|
7
|
+
* 且必须挂在该门之后)。一切行为由 fixture/合成数据证明(hotelbyte-reconcile-tests)。
|
|
8
|
+
*
|
|
9
|
+
* 词汇与边界(上游文档为准):
|
|
10
|
+
* - docs/design/write-gate-production-design.md §3(SupplierOutcome/WriteEffectIntent/
|
|
11
|
+
* attempt_id/fencing)、§5.3(dispatch 未知态)、§6(准入矩阵)、§7(unknown 查单/恢复窗口);
|
|
12
|
+
* - docs/design/fact-writegate-seam.md(读侧 fact 与写侧 SupplierOutcome 两闸分离)。
|
|
13
|
+
*
|
|
14
|
+
* 与 #231(WriteGate outbox)的接口边界:本模块只消费 outbox intent 的**引用 ID 字符串**
|
|
15
|
+
* (`outboxIntentRef` 占位)与审批凭证摘要,不实现、不触碰 outbox/dispatch 持久化——
|
|
16
|
+
* attempt 领取、fencing token、账本事务由 #231 落地;两者以该 ID 关联(#231 未合前为占位)。
|
|
17
|
+
*
|
|
18
|
+
* 对账红线(全部结构性编码,见 advanceReconciliation):
|
|
19
|
+
* - unknown/无法确认的查单结果必须有显式对账分类——不静默当成功,也不静默丢弃;
|
|
20
|
+
* - CLI exit0 非成功证明:timeout/kill/坏响应/无绑定成功/部分确认一律 unknown;
|
|
21
|
+
* - 查单 miss 在恢复窗口内(后端 180s Phase1 + 最长 10min Phase2)保持 unknown,禁止重订;
|
|
22
|
+
* - 时间届满本身不是无订单证明——只转人工,不自动 reconciled_failed;
|
|
23
|
+
* - 只有绑定 attempt/customerReferenceNo 的权威终态负证据(供应商终态取消/失败、
|
|
24
|
+
* 绑定 attempt 的人工无订单确认)才允许新 intent;
|
|
25
|
+
* - 同 ref 多单/参考号不匹配 = 冲突,显式 manual_reconcile;
|
|
26
|
+
* - 终态后出现反证只置 conflict 交人工,不静默翻转;
|
|
27
|
+
* - 每次推进追加审计事件(带 probeDigest),记录不可变(pure function)。
|
|
28
|
+
*/
|
|
29
|
+
import { createHash } from 'node:crypto';
|
|
30
|
+
/** HotelByte 后端恢复窗口:Phase1 180s + Phase2 最长 10min(600s);上界 780s。 */
|
|
31
|
+
export const RECOVERY_WINDOW_PHASE1_SECONDS = 180;
|
|
32
|
+
export const RECOVERY_WINDOW_PHASE2_MAX_SECONDS = 600;
|
|
33
|
+
export const RECOVERY_WINDOW_TOTAL_MAX_SECONDS = RECOVERY_WINDOW_PHASE1_SECONDS + RECOVERY_WINDOW_PHASE2_MAX_SECONDS;
|
|
34
|
+
function requireBinding(key, value) {
|
|
35
|
+
const v = (value ?? '').trim();
|
|
36
|
+
if (!v)
|
|
37
|
+
throw new Error(`hotelbyte-reconcile: 对账记录缺绑定键 ${key}(fail-closed;无绑定键即无法追溯)`);
|
|
38
|
+
return v;
|
|
39
|
+
}
|
|
40
|
+
function probeDigest(input) {
|
|
41
|
+
return createHash('sha256').update(JSON.stringify(input ?? null)).digest('hex');
|
|
42
|
+
}
|
|
43
|
+
/** 恢复窗口上界:enterUnknownAt + 780s(180s Phase1 + 最长 10min Phase2)。 */
|
|
44
|
+
export function recoveryWindowEndsAt(enteredUnknownAt) {
|
|
45
|
+
return new Date(Date.parse(enteredUnknownAt) + RECOVERY_WINDOW_TOTAL_MAX_SECONDS * 1000).toISOString();
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 进入 unknown:booking 结果不可判定(超时/被杀/坏响应/部分确认)即建对账记录。
|
|
49
|
+
* 绑定键缺失直接抛错——没有绑定键的对账是不可追溯的黑洞,红线拒绝。
|
|
50
|
+
*/
|
|
51
|
+
export function enterUnknown(seed) {
|
|
52
|
+
const attemptId = requireBinding('attemptId', seed.attemptId);
|
|
53
|
+
const customerReferenceNo = requireBinding('customerReferenceNo', seed.customerReferenceNo);
|
|
54
|
+
const intentIdemKey = requireBinding('intentIdemKey', seed.intentIdemKey);
|
|
55
|
+
const requestFingerprintSha256 = requireBinding('requestFingerprintSha256', seed.requestFingerprintSha256);
|
|
56
|
+
const recoveryWindowEndsAtV = requireBinding('recoveryWindowEndsAt', seed.recoveryWindowEndsAt);
|
|
57
|
+
const enteredUnknownAt = requireBinding('enteredUnknownAt', seed.enteredUnknownAt);
|
|
58
|
+
if (Number.isNaN(Date.parse(enteredUnknownAt)) || Number.isNaN(Date.parse(recoveryWindowEndsAtV))) {
|
|
59
|
+
throw new Error('hotelbyte-reconcile: enteredUnknownAt/recoveryWindowEndsAt 必须是可解析时间');
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
attemptId,
|
|
63
|
+
customerReferenceNo,
|
|
64
|
+
intentIdemKey,
|
|
65
|
+
requestFingerprintSha256,
|
|
66
|
+
enteredUnknownAt,
|
|
67
|
+
recoveryWindowEndsAt: recoveryWindowEndsAtV,
|
|
68
|
+
factId: seed.factId,
|
|
69
|
+
approvalReceiptDigest: seed.approvalReceiptDigest,
|
|
70
|
+
outboxIntentRef: seed.outboxIntentRef,
|
|
71
|
+
status: 'unknown',
|
|
72
|
+
nextAction: 'keep_querying',
|
|
73
|
+
conflict: false,
|
|
74
|
+
history: [{
|
|
75
|
+
at: enteredUnknownAt,
|
|
76
|
+
action: 'enter_unknown',
|
|
77
|
+
from: 'unknown',
|
|
78
|
+
to: 'unknown',
|
|
79
|
+
reason: 'booking 结果不可判定,进入显式对账(禁止盲目重试/重订)',
|
|
80
|
+
probeDigest: probeDigest(seed),
|
|
81
|
+
}],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function parseBookPayload(stdout) {
|
|
85
|
+
const start = stdout.search(/[{[]/);
|
|
86
|
+
if (start < 0)
|
|
87
|
+
return { json: null, parseError: 'no-json-marker' };
|
|
88
|
+
try {
|
|
89
|
+
const parsed = JSON.parse(stdout.slice(start));
|
|
90
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
91
|
+
return { json: null, parseError: 'non-object-json' };
|
|
92
|
+
}
|
|
93
|
+
return { json: parsed };
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
return { json: null, parseError: `unparseable:${e.message.slice(0, 80)}` };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* book 执行分类(验收 §3:CLI exit0 非成功证明;timeout/kill/坏响应/结果不确定归 unknown):
|
|
101
|
+
* - timedOut / signal / 非零退码且无显式业务结论 → unknown;
|
|
102
|
+
* - exit0 + 非 JSON → unknown;exit0 + 显式业务失败 → supplier_failed;
|
|
103
|
+
* - exit0 + success 但缺/错 customerReferenceNo 绑定 → unknown(不可归因即不可当成功);
|
|
104
|
+
* - exit0 + pending/部分确认 → unknown;
|
|
105
|
+
* - exit0 + success + 参考号匹配本 attempt → binding_verified。
|
|
106
|
+
*/
|
|
107
|
+
export function classifyBookExecution(r, expectedReferenceNo) {
|
|
108
|
+
if (r.timedOut)
|
|
109
|
+
return { kind: 'unknown', reason: 'timeout(30s abort;后端可能仍在处理,进入 unknown 对账)' };
|
|
110
|
+
if (r.signal)
|
|
111
|
+
return { kind: 'unknown', reason: `process_killed:${r.signal}(副作用不可判定)` };
|
|
112
|
+
const { json, parseError } = parseBookPayload(r.stdout);
|
|
113
|
+
if (r.exitCode !== 0) {
|
|
114
|
+
if (json?.status === 'failed')
|
|
115
|
+
return { kind: 'supplier_failed', reason: 'nonzero-exit + explicit failed payload', payload: json };
|
|
116
|
+
return { kind: 'unknown', reason: `nonzero_exit:${r.exitCode ?? 'null'}(无显式业务结论,不假定无副作用${parseError ? `;${parseError}` : ''})` };
|
|
117
|
+
}
|
|
118
|
+
if (!json)
|
|
119
|
+
return { kind: 'unknown', reason: `exit0_non_json(坏响应,不可当成功;${parseError ?? 'empty'})` };
|
|
120
|
+
if (json.status === 'failed')
|
|
121
|
+
return { kind: 'supplier_failed', reason: 'explicit failed payload', payload: json };
|
|
122
|
+
if (json.status === 'success') {
|
|
123
|
+
const ref = typeof json.customerReferenceNo === 'string' ? json.customerReferenceNo.trim() : '';
|
|
124
|
+
if (!ref)
|
|
125
|
+
return { kind: 'unknown', reason: 'exit0_success_missing_reference(无绑定成功不可归因)' };
|
|
126
|
+
if (ref !== expectedReferenceNo)
|
|
127
|
+
return { kind: 'unknown', reason: `exit0_success_reference_mismatch(返回 ${ref},本 attempt ${expectedReferenceNo})` };
|
|
128
|
+
return { kind: 'binding_verified', customerReferenceNo: ref, payload: json };
|
|
129
|
+
}
|
|
130
|
+
// pending / 其它一切不确定状态:部分确认 ≠ 成功
|
|
131
|
+
return { kind: 'unknown', reason: `partial_or_indeterminate(status=${String(json.status ?? 'absent')})` };
|
|
132
|
+
}
|
|
133
|
+
/** 供应商终态取消/失败词汇(权威负证据来源;迟到自动取消按回执收敛)。 */
|
|
134
|
+
const SUPPLIER_TERMINAL_NEGATIVE = new Set(['cancelled', 'canceled', 'auto_canceled', 'failed', 'expired']);
|
|
135
|
+
/**
|
|
136
|
+
* 对账状态机推进(纯函数;原状态不可变,返回带追加审计事件的新状态)。
|
|
137
|
+
* 转移表对齐 write-gate-production-design §7 矩阵,红线见模块头注释。
|
|
138
|
+
*/
|
|
139
|
+
export function advanceReconciliation(state, probe, now) {
|
|
140
|
+
const digest = probeDigest(probe);
|
|
141
|
+
const append = (next, action, reason) => ({
|
|
142
|
+
...next,
|
|
143
|
+
history: [...state.history, { at: now, action, from: state.status, to: next.status, reason, probeDigest: digest }],
|
|
144
|
+
});
|
|
145
|
+
// 终态吸收 + 反证只置 conflict(不静默翻转)
|
|
146
|
+
if (state.status !== 'unknown') {
|
|
147
|
+
if (probe.kind === 'hit' && probe.orders.length > 0) {
|
|
148
|
+
return append({ ...state, conflict: true, nextAction: 'manual_reconcile' }, 'conflict_late_hit_after_terminal', `终态(${state.status})后出现订单证据 → 显式冲突交人工复核,不静默翻转`);
|
|
149
|
+
}
|
|
150
|
+
return { ...state };
|
|
151
|
+
}
|
|
152
|
+
if (probe.kind === 'manual_no_order') {
|
|
153
|
+
const evidence = (probe.operatorEvidence ?? '').trim();
|
|
154
|
+
if (!evidence)
|
|
155
|
+
throw new Error('hotelbyte-reconcile: 人工无订单确认必须携带非空操作证据(绑定 attempt 可追溯)');
|
|
156
|
+
return append({ ...state, status: 'reconciled_failed', nextAction: 'terminal', conflict: false }, 'manual_no_order_confirmed', `人工权威负证据:${evidence.slice(0, 200)}`);
|
|
157
|
+
}
|
|
158
|
+
const withinWindow = Date.parse(now) <= Date.parse(state.recoveryWindowEndsAt);
|
|
159
|
+
switch (probe.kind) {
|
|
160
|
+
case 'miss': {
|
|
161
|
+
if (withinWindow) {
|
|
162
|
+
return append({ ...state, status: 'unknown', nextAction: 'keep_querying', conflict: false }, 'query_miss_within_window', '恢复窗口内查无 → 保持 unknown;供应商可能迟到建单,禁止重订');
|
|
163
|
+
}
|
|
164
|
+
return append({ ...state, status: 'unknown', nextAction: 'manual_reconcile', conflict: false }, 'query_miss_after_window', '窗口届满查无 → 仍 unknown(时间届满本身不是无订单证明),转人工');
|
|
165
|
+
}
|
|
166
|
+
case 'hit': {
|
|
167
|
+
if (probe.orders.length > 1) {
|
|
168
|
+
return append({ ...state, status: 'unknown', nextAction: 'manual_reconcile', conflict: true }, 'conflict_multiple_orders', `同参考号返回 ${probe.orders.length} 笔订单 → 冲突,人工对账(不静默择一)`);
|
|
169
|
+
}
|
|
170
|
+
const order = probe.orders[0];
|
|
171
|
+
if (order.customerReferenceNo !== state.customerReferenceNo) {
|
|
172
|
+
return append({ ...state, status: 'unknown', nextAction: 'manual_reconcile', conflict: true }, 'conflict_reference_mismatch', `返回订单参考号 ${order.customerReferenceNo} ≠ 本 attempt ${state.customerReferenceNo} → 冲突,人工对账`);
|
|
173
|
+
}
|
|
174
|
+
const status = order.supplierStatus.toLowerCase();
|
|
175
|
+
if (SUPPLIER_TERMINAL_NEGATIVE.has(status)) {
|
|
176
|
+
return append({ ...state, status: 'reconciled_failed', nextAction: 'terminal', conflict: false }, 'supplier_terminal_negative', `绑定 attempt 的供应商终态(${order.supplierStatus},orderRef=${order.orderRef})→ 权威负证据,收敛 reconciled_failed`);
|
|
177
|
+
}
|
|
178
|
+
if (status === 'confirmed' || status === 'success' || status === 'completed') {
|
|
179
|
+
return append({ ...state, status: 'reconciled_success', nextAction: 'terminal', conflict: false }, 'supplier_terminal_success', `绑定 attempt 的供应商终态成功(orderRef=${order.orderRef})→ reconciled_success(含迟到成功,不重订)`);
|
|
180
|
+
}
|
|
181
|
+
return append({ ...state, status: 'unknown', nextAction: 'keep_querying', conflict: false }, 'hit_not_terminal', `订单存在但非终态(${order.supplierStatus})→ 保持 unknown 继续查`);
|
|
182
|
+
}
|
|
183
|
+
case 'query_failed':
|
|
184
|
+
return append({ ...state, status: 'unknown', nextAction: 'keep_querying', conflict: false }, 'query_failed', `查询自身失败(${probe.reason.slice(0, 120)})→ 探针失败不证明订单不存在,保持 unknown`);
|
|
185
|
+
case 'malformed':
|
|
186
|
+
return append({ ...state, status: 'unknown', nextAction: 'keep_querying', conflict: false }, 'query_malformed', `畸形响应(${probe.reason.slice(0, 120)})→ 不静默当 miss,保持 unknown`);
|
|
187
|
+
case 'permission_denied':
|
|
188
|
+
return append({ ...state, status: 'unknown', nextAction: 'manual_reconcile', conflict: false }, 'query_permission_denied', '查询权限拒绝 → 自助查单不可用,转人工对账');
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/** 是否允许对同一请求发起新 intent:仅当收敛自绑定 attempt 的权威终态负证据且无未决冲突。 */
|
|
192
|
+
export function newIntentAllowed(state) {
|
|
193
|
+
return state.status === 'reconciled_failed' && !state.conflict;
|
|
194
|
+
}
|