@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,3 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 可下单事实侧车持久化(issue #46,ADR-19):append-only JSONL 证据日志。
|
|
3
|
+
*
|
|
4
|
+
* 定位:这是**会话证据**而非用户状态——与 bridge-latency.jsonl / incidents.jsonl
|
|
5
|
+
* 同类(审计痕迹),不进账本投影。append-only 保历史(同 route+date 重查的旧快照
|
|
6
|
+
* 可回溯),读取侧经 dedupeFacts 取同 fact_id 最新。
|
|
7
|
+
*
|
|
8
|
+
* 路径:<stateRoot>/gotry-state/bookable-facts.jsonl
|
|
9
|
+
*/
|
|
1
10
|
import { appendFile, mkdir, readFile } from 'node:fs/promises';
|
|
2
11
|
import { join } from 'node:path';
|
|
3
12
|
import { dedupeFacts } from '../src/bookable-facts.js';
|
|
@@ -6,36 +15,41 @@ export function factLogPath(stateRoot) {
|
|
|
6
15
|
const root = stateRoot === '.' ? process.cwd() : stateRoot;
|
|
7
16
|
return join(root, 'gotry-state', FACT_LOG_NAME);
|
|
8
17
|
}
|
|
18
|
+
/** 追加事实(永不抛错:落盘失败不阻塞检索工具主路径,返回 false 由调用方记 incident) */
|
|
9
19
|
export async function appendFacts(stateRoot, facts) {
|
|
10
|
-
if (facts.length === 0)
|
|
20
|
+
if (facts.length === 0)
|
|
21
|
+
return true;
|
|
11
22
|
try {
|
|
12
23
|
const p = factLogPath(stateRoot);
|
|
13
|
-
await mkdir(join(p, '..'), {
|
|
14
|
-
|
|
15
|
-
});
|
|
16
|
-
await appendFile(p, facts.map((f)=>JSON.stringify(f)).join('\n') + '\n', 'utf-8');
|
|
24
|
+
await mkdir(join(p, '..'), { recursive: true });
|
|
25
|
+
await appendFile(p, facts.map(f => JSON.stringify(f)).join('\n') + '\n', 'utf-8');
|
|
17
26
|
return true;
|
|
18
|
-
}
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
19
29
|
return false;
|
|
20
30
|
}
|
|
21
31
|
}
|
|
32
|
+
/** 读注册表(去重后视图;无文件 = 空注册表,不抛) */
|
|
22
33
|
export async function loadFactRegistry(stateRoot) {
|
|
23
34
|
try {
|
|
24
35
|
const raw = await readFile(factLogPath(stateRoot), 'utf-8');
|
|
25
36
|
const facts = [];
|
|
26
|
-
for (const line of raw.split('\n')){
|
|
37
|
+
for (const line of raw.split('\n')) {
|
|
27
38
|
const t = line.trim();
|
|
28
|
-
if (!t)
|
|
39
|
+
if (!t)
|
|
40
|
+
continue;
|
|
29
41
|
try {
|
|
30
42
|
const f = JSON.parse(t);
|
|
31
|
-
if (f.schema === 'gotry_bookable_fact.v1' && f.fact_id)
|
|
32
|
-
|
|
43
|
+
if (f.schema === 'gotry_bookable_fact.v1' && f.fact_id)
|
|
44
|
+
facts.push(f);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// 单行损坏不拖垮整本注册表(与 incidents/latency 日志同纪律)
|
|
48
|
+
}
|
|
33
49
|
}
|
|
34
50
|
return dedupeFacts(facts);
|
|
35
|
-
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
36
53
|
return [];
|
|
37
54
|
}
|
|
38
55
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
//# sourceURL=ts/capabilities/fact-log.ts
|
|
@@ -1,14 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FlyAI 官方通道能力层(飞猪开放平台,data-sources.md §8 / RFC docs/rfc/user-session-data-rfc.md P0):
|
|
3
|
+
*
|
|
4
|
+
* 链路(同构 anything.ts 的 CLI spawn 模式):
|
|
5
|
+
* gotry capabilities/flyai.ts
|
|
6
|
+
* → spawn `npx -y @fly-ai/flyai-cli search-flight|search-train --origin X --destination Y --dep-date D`
|
|
7
|
+
* | `search-hotel --dest-name X [--key-words K][--check-in-date A --check-out-date B]`
|
|
8
|
+
* → 飞猪 MCP API(实时直连官方商品库)
|
|
9
|
+
*
|
|
10
|
+
* 契约(与 hbcli/weather/opensky/anything 同构,L4 不变量):
|
|
11
|
+
* - 只读:8 工具全只读,交易经 jumpUrl/detailUrl 由人完成(与 WriteGate 哲学同构);
|
|
12
|
+
* - 永不抛错:网络/超时/解析失败一律降级返回 verdict='error';
|
|
13
|
+
* - 证据链:成功 [实时API:flyai@ts];失败 [实时API:flyai@error@ts];
|
|
14
|
+
* - 匿名试用额度可用但共享易达限(2026-09-02 迪拜 session:429 Trial limit reached
|
|
15
|
+
* 被当通用 error,LLM 跨轮盲重试 4 次)——达限归类 verdict='needs-setup' +
|
|
16
|
+
* setup 指引(申请正式 key 配 FLYAI_API_KEY),工具层据此阻断盲目重试。
|
|
17
|
+
* FLYAI_API_KEY 已配时 env 透传,不再走试用额度。
|
|
18
|
+
*/
|
|
1
19
|
import { spawn } from 'node:child_process';
|
|
2
20
|
import { closeSync, openSync, readFileSync, unlinkSync } from 'node:fs';
|
|
3
21
|
import { tmpdir } from 'node:os';
|
|
4
22
|
import { join } from 'node:path';
|
|
23
|
+
function nonEmptyText(value) {
|
|
24
|
+
if (typeof value !== 'string')
|
|
25
|
+
return undefined;
|
|
26
|
+
const text = value.trim();
|
|
27
|
+
return text || undefined;
|
|
28
|
+
}
|
|
29
|
+
function optionalText(value, allowNull = false) {
|
|
30
|
+
if (value === undefined)
|
|
31
|
+
return undefined;
|
|
32
|
+
if (allowNull && value === null)
|
|
33
|
+
return null;
|
|
34
|
+
return nonEmptyText(value);
|
|
35
|
+
}
|
|
36
|
+
function positiveFiniteNumber(value) {
|
|
37
|
+
const number = typeof value === 'number'
|
|
38
|
+
? value
|
|
39
|
+
: typeof value === 'string' && value.trim() ? Number(value.trim()) : Number.NaN;
|
|
40
|
+
return Number.isFinite(number) && number > 0 ? number : undefined;
|
|
41
|
+
}
|
|
42
|
+
function positiveFiniteDuration(value) {
|
|
43
|
+
if (typeof value === 'number')
|
|
44
|
+
return positiveFiniteNumber(value);
|
|
45
|
+
const text = nonEmptyText(value);
|
|
46
|
+
if (!text)
|
|
47
|
+
return undefined;
|
|
48
|
+
const minutes = /^(\d+(?:\.\d+)?)\s*分钟$/.exec(text)?.[1];
|
|
49
|
+
return positiveFiniteNumber(minutes ?? text);
|
|
50
|
+
}
|
|
51
|
+
function parseTransportPrice(value) {
|
|
52
|
+
const raw = nonEmptyText(value);
|
|
53
|
+
if (!raw)
|
|
54
|
+
return undefined;
|
|
55
|
+
const bare = raw.replace(/^¥\s*/, '');
|
|
56
|
+
if (/^\d+(?:\.\d+)?$/.test(bare)) {
|
|
57
|
+
const price = Number(bare);
|
|
58
|
+
return Number.isFinite(price) && price > 0 ? { price } : undefined;
|
|
59
|
+
}
|
|
60
|
+
// 未鉴权态可能返回 1xxx/¥7xx;保留原值,但绝不把模糊串猜成数字。
|
|
61
|
+
if (/^\d+x+$/.test(bare))
|
|
62
|
+
return { price: 0, priceRaw: raw };
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 从 CLI stdout 中提取首个完整且含 itemList 的 JSON 对象。
|
|
67
|
+
*
|
|
68
|
+
* npx/CLI 偶发在业务 JSON 前后追加提示;不能从首个 `{` 一直切到 EOF。
|
|
69
|
+
* 扫描器识别字符串与转义,因此 JSON 字符串里的花括号不会破坏深度计数。
|
|
70
|
+
*/
|
|
5
71
|
export function parseFlyaiItemList(stdout) {
|
|
6
72
|
let start = -1;
|
|
7
73
|
let depth = 0;
|
|
8
74
|
let inString = false;
|
|
9
75
|
let escaped = false;
|
|
10
76
|
let sawIncompleteObject = false;
|
|
11
|
-
for(let index = 0; index < stdout.length; index += 1){
|
|
77
|
+
for (let index = 0; index < stdout.length; index += 1) {
|
|
12
78
|
const char = stdout[index];
|
|
13
79
|
if (start < 0) {
|
|
14
80
|
if (char === '{') {
|
|
@@ -21,243 +87,266 @@ export function parseFlyaiItemList(stdout) {
|
|
|
21
87
|
continue;
|
|
22
88
|
}
|
|
23
89
|
if (inString) {
|
|
24
|
-
if (escaped)
|
|
25
|
-
|
|
26
|
-
else if (char === '
|
|
90
|
+
if (escaped)
|
|
91
|
+
escaped = false;
|
|
92
|
+
else if (char === '\\')
|
|
93
|
+
escaped = true;
|
|
94
|
+
else if (char === '"')
|
|
95
|
+
inString = false;
|
|
27
96
|
continue;
|
|
28
97
|
}
|
|
29
|
-
if (char === '"')
|
|
30
|
-
|
|
98
|
+
if (char === '"')
|
|
99
|
+
inString = true;
|
|
100
|
+
else if (char === '{')
|
|
101
|
+
depth += 1;
|
|
31
102
|
else if (char === '}') {
|
|
32
103
|
depth -= 1;
|
|
33
|
-
if (depth !== 0)
|
|
104
|
+
if (depth !== 0)
|
|
105
|
+
continue;
|
|
34
106
|
const candidate = stdout.slice(start, index + 1);
|
|
35
107
|
start = -1;
|
|
36
108
|
sawIncompleteObject = false;
|
|
37
109
|
try {
|
|
38
110
|
const parsed = JSON.parse(candidate);
|
|
39
|
-
if (Array.isArray(parsed.data?.itemList))
|
|
40
|
-
|
|
111
|
+
if (Array.isArray(parsed.data?.itemList))
|
|
112
|
+
return parsed.data.itemList;
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// 前缀日志可能包含成对花括号但不是 JSON;继续找下一个完整对象。
|
|
116
|
+
}
|
|
41
117
|
}
|
|
42
118
|
}
|
|
43
|
-
throw new Error(sawIncompleteObject
|
|
119
|
+
throw new Error(sawIncompleteObject
|
|
120
|
+
? 'incomplete FlyAI JSON object'
|
|
121
|
+
: 'no complete FlyAI itemList JSON object');
|
|
44
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* 子进程 stdout 走临时文件而非管道(issue #84):
|
|
125
|
+
* flyai CLI 异步写 stdout 后立即 process.exit();stdout 是管道时未 flush 的
|
|
126
|
+
* 尾部会被丢掉(实测截断在 ~7.6KB,exit=0 静默)。stdout 是文件时 Node 同步写,
|
|
127
|
+
* exit 不丢数据;且文件无 64KB 管道缓冲上限。close 后读文件,语义不变。
|
|
128
|
+
*/
|
|
45
129
|
function sh(cmd, args, opts) {
|
|
46
130
|
const outFile = join(tmpdir(), `gotry-flyai-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.out`);
|
|
47
131
|
const outFd = openSync(outFile, 'w');
|
|
48
|
-
const child = spawn(cmd, args, {
|
|
49
|
-
env: process.env,
|
|
50
|
-
cwd: process.cwd(),
|
|
51
|
-
stdio: [
|
|
52
|
-
'ignore',
|
|
53
|
-
outFd,
|
|
54
|
-
'pipe'
|
|
55
|
-
]
|
|
56
|
-
});
|
|
132
|
+
const child = spawn(cmd, args, { env: process.env, cwd: process.cwd(), stdio: ['ignore', outFd, 'pipe'] });
|
|
57
133
|
let stderr = '';
|
|
58
134
|
let error;
|
|
59
|
-
const timer = setTimeout(()=>{
|
|
135
|
+
const timer = setTimeout(() => {
|
|
60
136
|
try {
|
|
61
137
|
child.kill('SIGKILL');
|
|
62
|
-
}
|
|
138
|
+
}
|
|
139
|
+
catch { /* ignore */ }
|
|
63
140
|
}, opts.timeoutMs);
|
|
64
|
-
return new Promise((resolve)=>{
|
|
65
|
-
child.stderr?.on('data', (d)=>{
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
child.on('error', (e)=>{
|
|
69
|
-
error = e.message.slice(0, 200);
|
|
70
|
-
});
|
|
71
|
-
child.on('close', (code)=>{
|
|
141
|
+
return new Promise((resolve) => {
|
|
142
|
+
child.stderr?.on('data', (d) => { stderr += d.toString(); });
|
|
143
|
+
child.on('error', (e) => { error = e.message.slice(0, 200); });
|
|
144
|
+
child.on('close', (code) => {
|
|
72
145
|
clearTimeout(timer);
|
|
73
146
|
closeSync(outFd);
|
|
74
147
|
let stdout = '';
|
|
75
148
|
try {
|
|
76
149
|
stdout = readFileSync(outFile, 'utf8');
|
|
77
|
-
}
|
|
150
|
+
}
|
|
151
|
+
catch { /* 子进程未启动时文件可能为空,按空 stdout 处理 */ }
|
|
78
152
|
try {
|
|
79
153
|
unlinkSync(outFile);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
stdout,
|
|
84
|
-
stderr,
|
|
85
|
-
error
|
|
86
|
-
});
|
|
154
|
+
}
|
|
155
|
+
catch { /* ignore */ }
|
|
156
|
+
resolve({ code: code ?? -1, stdout, stderr, error });
|
|
87
157
|
});
|
|
88
|
-
}).finally(()=>clearTimeout(timer));
|
|
158
|
+
}).finally(() => clearTimeout(timer));
|
|
89
159
|
}
|
|
90
160
|
const YMD = /^\d{4}-\d{2}-\d{2}$/;
|
|
161
|
+
/** FlyAI 官方只读检索(机票/火车票/酒店) — 任何失败走降级;不抛错 */
|
|
91
162
|
export async function flyaiSearch(q) {
|
|
92
163
|
const started = Date.now();
|
|
93
164
|
const ts = new Date().toISOString();
|
|
94
|
-
const base = {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
};
|
|
98
|
-
const argErr = (error)=>({
|
|
99
|
-
...base,
|
|
100
|
-
ok: false,
|
|
101
|
-
via: 'flyai-error',
|
|
102
|
-
verdict: 'error',
|
|
103
|
-
evidence: `[实时API:flyai@error@${ts}] bad args`,
|
|
104
|
-
error
|
|
105
|
-
});
|
|
165
|
+
const base = { kind: q.kind, latencyMs: 0 };
|
|
166
|
+
const argErr = (error) => ({ ...base, ok: false, via: 'flyai-error', verdict: 'error', evidence: `[实时API:flyai@error@${ts}] bad args`, error });
|
|
167
|
+
// per-kind 参数闸:机/火与酒店参数面不同,各自校验,不合法走 error 不静默容错
|
|
106
168
|
let cliArgs;
|
|
107
169
|
if (q.kind === 'hotel') {
|
|
108
170
|
const dest = (q.destName ?? q.destination ?? '').trim();
|
|
109
|
-
if (!dest)
|
|
110
|
-
|
|
111
|
-
|
|
171
|
+
if (!dest)
|
|
172
|
+
return argErr('hotel 需要 to/destName(目的地中文)');
|
|
173
|
+
const badPair = (q.checkInDate ? 1 : 0) !== (q.checkOutDate ? 1 : 0)
|
|
174
|
+
|| (q.checkInDate && q.checkOutDate && (!YMD.test(q.checkInDate) || !YMD.test(q.checkOutDate)));
|
|
175
|
+
if (badPair)
|
|
176
|
+
return argErr('checkInDate/checkOutDate 须成对且为 YYYY-MM-DD(未定档期可不带日期先摸底)');
|
|
112
177
|
cliArgs = [
|
|
113
|
-
'search-hotel',
|
|
114
|
-
'--
|
|
115
|
-
|
|
116
|
-
...q.checkInDate && q.checkOutDate ? [
|
|
117
|
-
'--check-in-date',
|
|
118
|
-
q.checkInDate,
|
|
119
|
-
'--check-out-date',
|
|
120
|
-
q.checkOutDate
|
|
121
|
-
] : [],
|
|
122
|
-
...q.keyWords ? [
|
|
123
|
-
'--key-words',
|
|
124
|
-
q.keyWords
|
|
125
|
-
] : []
|
|
178
|
+
'search-hotel', '--dest-name', dest,
|
|
179
|
+
...(q.checkInDate && q.checkOutDate ? ['--check-in-date', q.checkInDate, '--check-out-date', q.checkOutDate] : []),
|
|
180
|
+
...(q.keyWords ? ['--key-words', q.keyWords] : []),
|
|
126
181
|
];
|
|
127
|
-
}
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
128
184
|
const origin = (q.origin ?? '').trim();
|
|
129
185
|
const destination = (q.destination ?? '').trim();
|
|
130
186
|
if (!origin || !destination || !YMD.test(q.depDate ?? '')) {
|
|
131
187
|
return argErr('origin/destination/depDate(YYYY-MM-DD) required');
|
|
132
188
|
}
|
|
133
189
|
const sub = q.kind === 'flight' ? 'search-flight' : 'search-train';
|
|
134
|
-
cliArgs = [
|
|
135
|
-
sub,
|
|
136
|
-
'--origin',
|
|
137
|
-
origin,
|
|
138
|
-
'--destination',
|
|
139
|
-
destination,
|
|
140
|
-
'--dep-date',
|
|
141
|
-
q.depDate ?? ''
|
|
142
|
-
];
|
|
190
|
+
cliArgs = [sub, '--origin', origin, '--destination', destination, '--dep-date', (q.depDate ?? '')];
|
|
143
191
|
}
|
|
144
|
-
const r = await sh(q.cliBin ?? 'npx', [
|
|
145
|
-
|
|
146
|
-
'@fly-ai/flyai-cli',
|
|
147
|
-
...cliArgs
|
|
148
|
-
], {
|
|
149
|
-
timeoutMs: q.timeoutMs ?? 30_000
|
|
192
|
+
const r = await sh(q.cliBin ?? 'npx', ['-y', '@fly-ai/flyai-cli', ...cliArgs], {
|
|
193
|
+
timeoutMs: q.timeoutMs ?? 30_000,
|
|
150
194
|
});
|
|
151
195
|
const latencyMs = Date.now() - started;
|
|
152
196
|
if (r.error || r.code !== 0) {
|
|
197
|
+
// 试用额度达限(2026-09-02 迪拜 session 实况:exit 1 + MCP HTTP 429 "Trial limit
|
|
198
|
+
// reached")是配置问题不是瞬时故障——归 needs-setup 带补配指引,让工具层阻断
|
|
199
|
+
// LLM 拿同一把 429 跨轮盲重试;正式 key 经 FLYAI_API_KEY 配入后此路径不再触发。
|
|
153
200
|
const upstream = `${r.stderr}\n${r.stdout}`;
|
|
154
201
|
if (/Trial limit reached|HTTP\s*429|\b429\b/.test(upstream)) {
|
|
155
202
|
return {
|
|
156
|
-
...base,
|
|
157
|
-
latencyMs,
|
|
158
|
-
ok: false,
|
|
159
|
-
via: 'flyai-error',
|
|
160
|
-
verdict: 'needs-setup',
|
|
203
|
+
...base, latencyMs, ok: false, via: 'flyai-error', verdict: 'needs-setup',
|
|
161
204
|
evidence: `[实时API:flyai@error@${ts}] trial quota exhausted`,
|
|
162
205
|
error: `FlyAI 匿名试用额度已用尽(上游 429):${upstream.replace(/\s+/g, ' ').slice(0, 160)}`,
|
|
163
|
-
setup: 'FlyAI 试用额度达限——到 flyai.open.fliggy.com 控制台申请正式 API Key,配入环境变量 FLYAI_API_KEY 即恢复;本会话请勿重试 gotry_flyai_search,机/火/酒改走 gotry_session_search(账号会话)或 web 检索推进。'
|
|
206
|
+
setup: 'FlyAI 试用额度达限——到 flyai.open.fliggy.com 控制台申请正式 API Key,配入环境变量 FLYAI_API_KEY 即恢复;本会话请勿重试 gotry_flyai_search,机/火/酒改走 gotry_session_search(账号会话)或 web 检索推进。',
|
|
164
207
|
};
|
|
165
208
|
}
|
|
166
|
-
return {
|
|
167
|
-
...base,
|
|
168
|
-
latencyMs,
|
|
169
|
-
ok: false,
|
|
170
|
-
via: 'flyai-error',
|
|
171
|
-
verdict: 'error',
|
|
172
|
-
evidence: `[实时API:flyai@error@${ts}] ${r.error ?? `exit ${r.code}`}`,
|
|
173
|
-
error: r.error ?? r.stderr.replace(/\s+/g, ' ').slice(0, 200)
|
|
174
|
-
};
|
|
209
|
+
return { ...base, latencyMs, ok: false, via: 'flyai-error', verdict: 'error', evidence: `[实时API:flyai@error@${ts}] ${r.error ?? `exit ${r.code}`}`, error: r.error ?? r.stderr.replace(/\s+/g, ' ').slice(0, 200) };
|
|
175
210
|
}
|
|
176
211
|
let items;
|
|
177
212
|
try {
|
|
213
|
+
// main-lane 加固的扫描器:容忍 npx 前缀日志/不完整对象——data:null 语义失败
|
|
214
|
+
// (issue #24)的「出发日期非法」原话经 raw stdout 片段进 error 终态,语义不丢
|
|
178
215
|
items = parseFlyaiItemList(r.stdout);
|
|
179
|
-
}
|
|
216
|
+
}
|
|
217
|
+
catch (e) {
|
|
218
|
+
// 实测(2026-08-28):Sentinel 限流时 CLI exit=0 但 stdout 是 {"message":"SentinelBlockException..."}
|
|
180
219
|
const raw = r.stdout.replace(/\s+/g, ' ').slice(0, 160);
|
|
181
220
|
const reason = e instanceof Error ? e.message : String(e);
|
|
182
|
-
return {
|
|
183
|
-
...base,
|
|
184
|
-
latencyMs,
|
|
185
|
-
ok: false,
|
|
186
|
-
via: 'flyai-error',
|
|
187
|
-
verdict: 'error',
|
|
188
|
-
evidence: `[实时API:flyai@error@${ts}] parse failed(${reason}): ${raw}`,
|
|
189
|
-
error: `failed to parse flyai output as JSON (${reason}): ${raw}`
|
|
190
|
-
};
|
|
221
|
+
return { ...base, latencyMs, ok: false, via: 'flyai-error', verdict: 'error', evidence: `[实时API:flyai@error@${ts}] parse failed(${reason}): ${raw}`, error: `failed to parse flyai output as JSON (${reason}): ${raw}` };
|
|
191
222
|
}
|
|
192
223
|
if (q.kind === 'hotel') {
|
|
193
|
-
const
|
|
224
|
+
const parsed = parseHotelItems(items);
|
|
225
|
+
if (parsed.malformedCount > 0)
|
|
226
|
+
return malformedItemListResult(q.kind, latencyMs, ts, parsed.malformedCount, items.length);
|
|
227
|
+
const hotels = parsed.options;
|
|
194
228
|
const verdict = hotels.length > 0 ? 'hit' : 'miss';
|
|
195
|
-
return {
|
|
196
|
-
kind: q.kind,
|
|
197
|
-
latencyMs,
|
|
198
|
-
ok: true,
|
|
199
|
-
via: 'flyai',
|
|
200
|
-
verdict,
|
|
201
|
-
evidence: `[实时API:flyai@${ts}] ${hotels.length}/${items.length} hotel options`,
|
|
202
|
-
hotels
|
|
203
|
-
};
|
|
229
|
+
return { kind: q.kind, latencyMs, ok: true, via: 'flyai', verdict, evidence: `[实时API:flyai@${ts}] ${hotels.length}/${items.length} hotel options`, hotels };
|
|
204
230
|
}
|
|
205
|
-
const
|
|
231
|
+
const parsed = parseTransportItems(items);
|
|
232
|
+
if (parsed.malformedCount > 0)
|
|
233
|
+
return malformedItemListResult(q.kind, latencyMs, ts, parsed.malformedCount, items.length);
|
|
234
|
+
const options = parsed.options;
|
|
206
235
|
const verdict = options.length > 0 ? 'hit' : 'miss';
|
|
236
|
+
return { kind: q.kind, latencyMs, ok: true, via: 'flyai', verdict, evidence: `[实时API:flyai@${ts}] ${options.length}/${items.length} ${q.kind} options`, options };
|
|
237
|
+
}
|
|
238
|
+
function malformedItemListResult(kind, latencyMs, ts, malformedCount, total) {
|
|
239
|
+
const label = kind === 'hotel' ? 'hotel' : 'transport';
|
|
240
|
+
const error = `FlyAI ${label} itemList malformed: ${malformedCount}/${total} item(s) failed typed validation`;
|
|
207
241
|
return {
|
|
208
|
-
kind
|
|
242
|
+
kind,
|
|
209
243
|
latencyMs,
|
|
210
|
-
ok:
|
|
211
|
-
via: 'flyai',
|
|
212
|
-
verdict,
|
|
213
|
-
evidence: `[实时API:flyai@${ts}] ${
|
|
214
|
-
|
|
244
|
+
ok: false,
|
|
245
|
+
via: 'flyai-error',
|
|
246
|
+
verdict: 'error',
|
|
247
|
+
evidence: `[实时API:flyai@error@${ts}] ${label} itemList malformed (${malformedCount}/${total} items failed typed validation; valid siblings discarded)`,
|
|
248
|
+
error,
|
|
215
249
|
};
|
|
216
250
|
}
|
|
251
|
+
/** 机/火条目 → FlyaiOption;非空列表含任一不完整条目即整体 malformed */
|
|
217
252
|
function parseTransportItems(items) {
|
|
218
253
|
const options = [];
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (!
|
|
222
|
-
|
|
223
|
-
|
|
254
|
+
let malformedCount = 0;
|
|
255
|
+
for (const raw of items) {
|
|
256
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
257
|
+
malformedCount += 1;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
const it = raw;
|
|
261
|
+
const journey = Array.isArray(it.journeys) ? it.journeys[0] : undefined;
|
|
262
|
+
const seg = journey && typeof journey === 'object' && !Array.isArray(journey) && Array.isArray(journey.segments)
|
|
263
|
+
? journey.segments[0]
|
|
264
|
+
: undefined;
|
|
265
|
+
if (!seg || typeof seg !== 'object' || Array.isArray(seg)) {
|
|
266
|
+
malformedCount += 1;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const no = nonEmptyText(seg.marketingTransportNo);
|
|
270
|
+
const name = optionalText(seg.marketingTransportName);
|
|
271
|
+
const depDateTime = nonEmptyText(seg.depDateTime);
|
|
272
|
+
const arrDateTime = nonEmptyText(seg.arrDateTime);
|
|
273
|
+
const depStation = nonEmptyText(seg.depStationName);
|
|
274
|
+
const arrStation = nonEmptyText(seg.arrStationName);
|
|
275
|
+
const durationMin = positiveFiniteDuration(seg.duration);
|
|
276
|
+
const rawPriceValue = it.adultPrice !== undefined
|
|
277
|
+
? it.adultPrice
|
|
278
|
+
: it.ticketPrice !== undefined
|
|
279
|
+
? it.ticketPrice
|
|
280
|
+
: it.price;
|
|
281
|
+
const parsedPrice = parseTransportPrice(rawPriceValue);
|
|
282
|
+
const seatClass = optionalText(seg.seatClassName);
|
|
283
|
+
const jumpUrl = optionalText(it.jumpUrl);
|
|
284
|
+
if (!no || !depDateTime || !arrDateTime || !depStation || !arrStation || durationMin === undefined || !parsedPrice
|
|
285
|
+
|| (seg.marketingTransportName !== undefined && name === undefined)
|
|
286
|
+
|| (seg.seatClassName !== undefined && seatClass === undefined)
|
|
287
|
+
|| (it.jumpUrl !== undefined && jumpUrl === undefined)) {
|
|
288
|
+
malformedCount += 1;
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
224
291
|
options.push({
|
|
225
|
-
no
|
|
226
|
-
name:
|
|
227
|
-
depDateTime
|
|
228
|
-
arrDateTime
|
|
229
|
-
depStation
|
|
230
|
-
arrStation
|
|
231
|
-
durationMin
|
|
232
|
-
price:
|
|
233
|
-
priceRaw:
|
|
234
|
-
seatClass:
|
|
235
|
-
jumpUrl:
|
|
292
|
+
no,
|
|
293
|
+
name: name ?? '',
|
|
294
|
+
depDateTime,
|
|
295
|
+
arrDateTime,
|
|
296
|
+
depStation,
|
|
297
|
+
arrStation,
|
|
298
|
+
durationMin,
|
|
299
|
+
price: parsedPrice.price,
|
|
300
|
+
priceRaw: parsedPrice.priceRaw,
|
|
301
|
+
seatClass: seatClass ?? undefined,
|
|
302
|
+
jumpUrl: jumpUrl ?? undefined,
|
|
236
303
|
});
|
|
237
304
|
}
|
|
238
|
-
return options;
|
|
305
|
+
return { options, malformedCount };
|
|
239
306
|
}
|
|
307
|
+
/** 酒店条目 → FlyaiHotelOption;非空列表含任一不完整条目即整体 malformed */
|
|
240
308
|
function parseHotelItems(items) {
|
|
241
309
|
const hotels = [];
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
310
|
+
let malformedCount = 0;
|
|
311
|
+
for (const raw of items) {
|
|
312
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
313
|
+
malformedCount += 1;
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
const it = raw;
|
|
317
|
+
const name = nonEmptyText(it.name);
|
|
318
|
+
const star = optionalText(it.star);
|
|
319
|
+
const rawPrice = optionalText(it.price);
|
|
320
|
+
const rate = optionalText(it.rate, true);
|
|
321
|
+
const address = optionalText(it.address);
|
|
322
|
+
const poi = optionalText(it.interestsPoi);
|
|
323
|
+
const hotelId = optionalText(it.shId);
|
|
324
|
+
const jumpUrl = optionalText(it.detailUrl);
|
|
325
|
+
if (!name
|
|
326
|
+
|| (it.star !== undefined && star === undefined)
|
|
327
|
+
|| (it.price !== undefined && rawPrice === undefined)
|
|
328
|
+
|| (it.rate !== undefined && rate === undefined)
|
|
329
|
+
|| (it.address !== undefined && address === undefined)
|
|
330
|
+
|| (it.interestsPoi !== undefined && poi === undefined)
|
|
331
|
+
|| (it.shId !== undefined && hotelId === undefined)
|
|
332
|
+
|| (it.detailUrl !== undefined && jumpUrl === undefined)) {
|
|
333
|
+
malformedCount += 1;
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
// 打码价如 "¥7xx" 绝不能截成数字 7(会伪装成真价)——仅全数字串才落 price
|
|
337
|
+
const bare = (rawPrice ?? '').replace(/^[¥]/, '');
|
|
246
338
|
const numericPrice = Number(bare);
|
|
247
339
|
hotels.push({
|
|
248
|
-
name
|
|
249
|
-
star:
|
|
340
|
+
name,
|
|
341
|
+
star: star ?? undefined,
|
|
250
342
|
price: /^\d+(\.\d+)?$/.test(bare) && numericPrice > 0 ? numericPrice : 0,
|
|
251
343
|
priceRaw: rawPrice || undefined,
|
|
252
|
-
rate:
|
|
253
|
-
address:
|
|
254
|
-
poi:
|
|
255
|
-
hotelId:
|
|
256
|
-
jumpUrl:
|
|
344
|
+
rate: rate ?? undefined,
|
|
345
|
+
address: address ?? undefined,
|
|
346
|
+
poi: poi ?? undefined,
|
|
347
|
+
hotelId: hotelId ?? undefined,
|
|
348
|
+
jumpUrl: jumpUrl ?? undefined,
|
|
257
349
|
});
|
|
258
350
|
}
|
|
259
|
-
return hotels;
|
|
351
|
+
return { options: hotels, malformedCount };
|
|
260
352
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
//# sourceURL=ts/capabilities/flyai.ts
|