@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
package/dist/src/unified.js
CHANGED
|
@@ -1,104 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 统一行程模型 TS 版(与 py/gotry_feasibility/unified.py 对齐;Python 为 oracle)。
|
|
3
|
+
* 行程 = Segment 序列;选择单元是 SegmentOption(目的地或具体班次)。
|
|
4
|
+
* 本文件实现:类型 + 双旧输入适配 + 航班链形态求解(Z3)。
|
|
5
|
+
* 候选形态 TS 求解(枚举过滤)与 engine 等价对账在下一迁移段。
|
|
6
|
+
*/
|
|
1
7
|
import { checkConnectivity } from '../scripts/skeleton-check.js';
|
|
2
|
-
import { evaluateChoice, minToHhmm, hhmmToMin, requiredUsableHours, trueCostToDict, LATEST_ARRIVE_STAY_MIN } from './model.js';
|
|
8
|
+
import { evaluateChoice, minToHhmm, hhmmToMin, requiredUsableHours, trueCostToDict, LATEST_ARRIVE_STAY_MIN, doorToDoorFromMove, NonpositiveDurationError } from './model.js';
|
|
3
9
|
import { withZ3 } from './z3-shared.js';
|
|
4
10
|
import { t as i18nT } from './i18n.js';
|
|
11
|
+
import { resolveOffsetForLocalDate, describeResolveFailure, isKnownZoneLoose, readWallParts, wallPartsToIso } from './tz-resolver.js';
|
|
12
|
+
import { FLIGHT_PACK_VERSION } from './flight-pack-contract.js';
|
|
13
|
+
// Z3 运行时收敛到 z3-shared(单一 WASM 实例 + 单一 Context + 会话级互斥)——
|
|
14
|
+
// 此前模块级 z3Promise 三份并存是 WASM race/OOM 的根因(见 z3-shared.ts 头注)。
|
|
15
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
16
|
+
/** 航班数据包(data/flights_2026.json 形态)→ 段链 */
|
|
5
17
|
const SEGMENT_ROUTES = {
|
|
6
|
-
f1: 'HKG->HKT',
|
|
7
|
-
|
|
8
|
-
f3: 'BKK->KMG',
|
|
9
|
-
f4: 'KMG->SZX',
|
|
10
|
-
f5: 'SZX->DXB',
|
|
11
|
-
yn1: 'SZX->LJG',
|
|
12
|
-
yn2: 'LJG->DLU',
|
|
13
|
-
yn3: 'DLU->LJG',
|
|
14
|
-
yn4: 'LJG->SZX'
|
|
18
|
+
f1: 'HKG->HKT', f2: 'HKT->BKK', f3: 'BKK->KMG', f4: 'KMG->SZX', f5: 'SZX->DXB',
|
|
19
|
+
yn1: 'SZX->LJG', yn2: 'LJG->DLU', yn3: 'DLU->LJG', yn4: 'LJG->SZX',
|
|
15
20
|
};
|
|
16
21
|
function routeHint(id) {
|
|
17
22
|
return SEGMENT_ROUTES[id];
|
|
18
23
|
}
|
|
24
|
+
// ---- 适配器:旧候选用例(洱海形态:request + candidates)→ 单 choice 段 -------------
|
|
25
|
+
// 与 py unified.py segments_from_candidate 逐行对齐
|
|
19
26
|
export function segmentsFromCandidate(req, candidates) {
|
|
20
27
|
const motHard = req.motivation;
|
|
21
28
|
const wakeFloor = motHard.wakeFloorMin;
|
|
22
|
-
const options = candidates.map(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
nights: req.windowDays - 1,
|
|
46
|
-
stayCnyPerNight: c.stayCnyPerNight,
|
|
47
|
-
localDailyCny: c.localDailyCny
|
|
48
|
-
}
|
|
49
|
-
}));
|
|
29
|
+
const options = candidates.map(c => ({
|
|
30
|
+
id: c.id,
|
|
31
|
+
label: c.name,
|
|
32
|
+
date: c.date,
|
|
33
|
+
score: c.imageryMatch,
|
|
34
|
+
bestMonths: c.bestMonths,
|
|
35
|
+
minDays: c.minDaysForPurpose,
|
|
36
|
+
move: {
|
|
37
|
+
hub: c.hub,
|
|
38
|
+
services: [...c.servicesOut],
|
|
39
|
+
retServices: [...c.servicesRet],
|
|
40
|
+
transfers: [...c.destTransfers],
|
|
41
|
+
bufferMin: c.bufferOutMin,
|
|
42
|
+
bufferRetMin: c.bufferRetMin,
|
|
43
|
+
originTransferMin: 0, // 候选形态无独立接驳(并入 destTransfers)
|
|
44
|
+
destTransferMin: 0,
|
|
45
|
+
},
|
|
46
|
+
stay: {
|
|
47
|
+
nights: req.windowDays - 1,
|
|
48
|
+
stayCnyPerNight: c.stayCnyPerNight,
|
|
49
|
+
localDailyCny: c.localDailyCny,
|
|
50
|
+
},
|
|
51
|
+
}));
|
|
50
52
|
const escape = req.motivation.weights['escape_rest'] ?? 0;
|
|
51
53
|
return {
|
|
52
54
|
note: req.note,
|
|
53
|
-
segments: [
|
|
54
|
-
{
|
|
55
|
+
segments: [{
|
|
55
56
|
id: 'dest',
|
|
56
57
|
role: 'choice',
|
|
57
58
|
note: '目的地选择',
|
|
58
|
-
anchors: {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
options
|
|
62
|
-
}
|
|
63
|
-
],
|
|
59
|
+
anchors: { wakeFloorMin: wakeFloor },
|
|
60
|
+
options,
|
|
61
|
+
}],
|
|
64
62
|
budgetCny: req.budgetCny,
|
|
65
|
-
defaultWakeFloorMin: wakeFloor
|
|
63
|
+
defaultWakeFloorMin: wakeFloor,
|
|
66
64
|
};
|
|
67
65
|
}
|
|
68
66
|
export function parseFlightPackToSpec(pack) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
67
|
+
// Issue #343 v2:IANA path;v1(无 version 字段或 version===1)走原数值口径。
|
|
68
|
+
const version = pack['version'] === undefined ? 1 : Number(pack['version']);
|
|
69
|
+
if (version === 2)
|
|
70
|
+
return parseFlightPackToSpecV2(pack);
|
|
71
|
+
if (version !== 1) {
|
|
72
|
+
throw Object.assign(new Error(`parseFlightPackToSpec: unsupported pack version ${version}`), { code: 'unsupported_pack_version' });
|
|
73
|
+
}
|
|
74
|
+
return parseFlightPackToSpecV1(pack);
|
|
75
|
+
}
|
|
76
|
+
/** v1 数值口径(path 保留为 export,便于 §7-1/§6 直接复用与差分) */
|
|
77
|
+
export function parseFlightPackToSpecV1(pack) {
|
|
78
|
+
const legs = pack['legs'].map(l => ({
|
|
79
|
+
id: String(l['id']),
|
|
80
|
+
role: (l['services'].length === 1 ? 'fixed' : 'choice'),
|
|
81
|
+
note: l['note'] ? String(l['note']) : undefined,
|
|
82
|
+
date: l['date'] ? String(l['date']) : undefined,
|
|
83
|
+
anchors: { arriveByMin: l['arrive_by'] ? hhmmToMin(String(l['arrive_by'])) : undefined },
|
|
84
|
+
route: routeHint(l['id']),
|
|
85
|
+
options: l['services'].map(sv => ({
|
|
86
|
+
id: String(sv['id']),
|
|
87
|
+
label: `${String(sv['id'])} ${String(l['note'] ?? '').slice(0, 18)}`,
|
|
88
|
+
move: {
|
|
89
|
+
hub: String(l['hub'] ?? ''),
|
|
90
|
+
services: [{ id: String(sv['id']), depMin: hhmmToMin(String(sv['dep'])), arrMin: hhmmToMin(String(sv['arr'])), priceCny: Number(sv['price_cny']) }],
|
|
91
|
+
bufferMin: Number(l['buffer_min']),
|
|
92
|
+
originTransferMin: Number(l['origin_transfer_min']),
|
|
93
|
+
destTransferMin: Number(l['dest_transfer_min']),
|
|
94
|
+
redEye: Boolean(l['red_eye']),
|
|
95
|
+
redEyeDurationMin: Number(l['red_eye_duration_min'] ?? 0),
|
|
96
|
+
tzOffsetMin: Number(l['tz_offset_min'] ?? 0),
|
|
97
|
+
originTzOffsetMin: Number(l['origin_tz_offset_min'] ?? 480),
|
|
76
98
|
},
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
label: `${String(sv['id'])} ${String(l['note'] ?? '').slice(0, 18)}`,
|
|
81
|
-
move: {
|
|
82
|
-
hub: String(l['hub'] ?? ''),
|
|
83
|
-
services: [
|
|
84
|
-
{
|
|
85
|
-
id: String(sv['id']),
|
|
86
|
-
depMin: hhmmToMin(String(sv['dep'])),
|
|
87
|
-
arrMin: hhmmToMin(String(sv['arr'])),
|
|
88
|
-
priceCny: Number(sv['price_cny'])
|
|
89
|
-
}
|
|
90
|
-
],
|
|
91
|
-
bufferMin: Number(l['buffer_min']),
|
|
92
|
-
originTransferMin: Number(l['origin_transfer_min']),
|
|
93
|
-
destTransferMin: Number(l['dest_transfer_min']),
|
|
94
|
-
redEye: Boolean(l['red_eye']),
|
|
95
|
-
redEyeDurationMin: Number(l['red_eye_duration_min'] ?? 0),
|
|
96
|
-
tzOffsetMin: Number(l['tz_offset_min'] ?? 0),
|
|
97
|
-
originTzOffsetMin: Number(l['origin_tz_offset_min'] ?? 480)
|
|
98
|
-
}
|
|
99
|
-
}))
|
|
100
|
-
}));
|
|
101
|
-
const meta = pack['meta'] ?? {};
|
|
99
|
+
})),
|
|
100
|
+
}));
|
|
101
|
+
const meta = (pack['meta'] ?? {});
|
|
102
102
|
const ww = meta['work_window'];
|
|
103
103
|
const spec = {
|
|
104
104
|
segments: legs,
|
|
@@ -106,87 +106,262 @@ export function parseFlightPackToSpec(pack) {
|
|
|
106
106
|
homeTzOffsetMin: Number(ww['home_tz_offset_min']),
|
|
107
107
|
startMin: Number(ww['start_min']),
|
|
108
108
|
endMin: Number(ww['end_min']),
|
|
109
|
-
workdays: ww['workdays'] ?? [
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
2,
|
|
113
|
-
3,
|
|
114
|
-
4
|
|
115
|
-
]
|
|
116
|
-
} : undefined
|
|
109
|
+
workdays: ww['workdays'] ?? [0, 1, 2, 3, 4],
|
|
110
|
+
} : undefined,
|
|
111
|
+
[FLIGHT_PACK_VERSION]: 1,
|
|
117
112
|
};
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
113
|
+
// M-1:班次的星期标注挂到 Option(缺省=不受工作窗口约束)
|
|
114
|
+
for (const l of pack['legs']) {
|
|
115
|
+
const seg = spec.segments.find(s => s.id === l['id']);
|
|
116
|
+
for (const svc of l['services']) {
|
|
121
117
|
if (svc['weekday']) {
|
|
122
|
-
const opt = seg.options.find(
|
|
123
|
-
if (opt)
|
|
118
|
+
const opt = seg.options.find(o => o.id === svc['id']);
|
|
119
|
+
if (opt)
|
|
120
|
+
opt.depWeekday = String(svc['weekday']);
|
|
124
121
|
}
|
|
125
122
|
}
|
|
126
123
|
}
|
|
127
124
|
return spec;
|
|
128
125
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
126
|
+
/** Issue #343 v2:IANA zone + 当地日期解析;数值 tz_* 字段不可用(混合形态解析时拒收)。 */
|
|
127
|
+
export function parseFlightPackToSpecV2(pack) {
|
|
128
|
+
const legsRaw = pack['legs'];
|
|
129
|
+
if (!Array.isArray(legsRaw) || legsRaw.length === 0) {
|
|
130
|
+
throw Object.assign(new Error('parseFlightPackToSpecV2: legs missing/empty'), { code: 'v2_legs_missing' });
|
|
131
|
+
}
|
|
132
|
+
const meta = (pack['meta'] ?? {});
|
|
133
|
+
const ww = meta['work_window'];
|
|
134
|
+
const legs = [];
|
|
135
|
+
for (const l of legsRaw) {
|
|
136
|
+
// v2 不允许混合数值 tz_offset_min/origin_tz_offset_min(早 fail)
|
|
137
|
+
if ('tz_offset_min' in l || 'origin_tz_offset_min' in l) {
|
|
138
|
+
throw Object.assign(new Error(`parseFlightPackToSpecV2: leg ${l['id']} has legacy tz_offset_min/origin_tz_offset_min;v2 pack must use iana_*_zone + *_local_date`), { code: 'v2_legacy_offset_mixed' });
|
|
139
|
+
}
|
|
140
|
+
const legDate = l['date'] ? String(l['date']) : '';
|
|
141
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(legDate)) {
|
|
142
|
+
throw Object.assign(new Error(`parseFlightPackToSpecV2: leg ${l['id']} requires ISO date`), { code: 'v2_leg_date_missing' });
|
|
143
|
+
}
|
|
144
|
+
const ianaDepZone = String(l['iana_dep_zone'] ?? '');
|
|
145
|
+
const ianaArrZone = String(l['iana_arr_zone'] ?? '');
|
|
146
|
+
if (!ianaDepZone || !ianaArrZone) {
|
|
147
|
+
throw Object.assign(new Error(`parseFlightPackToSpecV2: leg ${l['id']} needs iana_dep_zone and iana_arr_zone`), { code: 'v2_zone_missing' });
|
|
148
|
+
}
|
|
149
|
+
const options = l['services'].map(sv => {
|
|
150
|
+
const svcDepDate = sv['dep_local_date'] ? String(sv['dep_local_date']) : legDate;
|
|
151
|
+
const svcArrDate = sv['arr_local_date'] ? String(sv['arr_local_date']) : legDate;
|
|
152
|
+
const depZone = sv['iana_dep_zone'] ? String(sv['iana_dep_zone']) : ianaDepZone;
|
|
153
|
+
const arrZone = sv['iana_arr_zone'] ? String(sv['iana_arr_zone']) : ianaArrZone;
|
|
154
|
+
const depR = resolveOffsetForLocalDate(depZone, svcDepDate, String(sv['dep']));
|
|
155
|
+
if (!depR.ok) {
|
|
156
|
+
throw Object.assign(new Error(`parseFlightPackToSpecV2: dep zone/date failure: ${describeResolveFailure(depR)}`), { code: 'v2_dep_resolve_failed' });
|
|
157
|
+
}
|
|
158
|
+
const arrR = resolveOffsetForLocalDate(arrZone, svcArrDate, String(sv['arr']));
|
|
159
|
+
if (!arrR.ok) {
|
|
160
|
+
throw Object.assign(new Error(`parseFlightPackToSpecV2: arr zone/date failure: ${describeResolveFailure(arrR)}`), { code: 'v2_arr_resolve_failed' });
|
|
161
|
+
}
|
|
162
|
+
// Issue #343 root 复核:d2d canonical 输入必须在 parse 边界校验严格 chronologically after,
|
|
163
|
+
// 否则把病态数据喂给 model.doorToDoorFromMove → throw NonpositiveDurationError → 被
|
|
164
|
+
// solveUnified WASM guard 误报为 wasm_runtime_error。
|
|
165
|
+
if (arrR.utcMs <= depR.utcMs) {
|
|
166
|
+
throw Object.assign(new Error(`parseFlightPackToSpecV2: service ${String(sv['id'])} arrival instant (${new Date(arrR.utcMs).toISOString()}) must be strictly after departure instant (${new Date(depR.utcMs).toISOString()})`), { code: 'v2_arr_not_after_dep' });
|
|
167
|
+
}
|
|
168
|
+
// tzOffsetMin = arr - dep(positive = 抵达地更靠东)
|
|
169
|
+
const tzOffsetMin = arrR.offsetMin - depR.offsetMin;
|
|
170
|
+
return {
|
|
171
|
+
id: String(sv['id']),
|
|
172
|
+
label: `${String(sv['id'])} ${String(l['note'] ?? '').slice(0, 18)}`,
|
|
173
|
+
date: svcDepDate,
|
|
174
|
+
move: {
|
|
175
|
+
hub: String(l['hub'] ?? ''),
|
|
176
|
+
services: [{
|
|
177
|
+
id: String(sv['id']),
|
|
178
|
+
depMin: hhmmToMin(String(sv['dep'])),
|
|
179
|
+
arrMin: hhmmToMin(String(sv['arr'])),
|
|
180
|
+
priceCny: Number(sv['price_cny']),
|
|
181
|
+
// Issue #343 v2:把"墙上时刻"投影成真实 UTC instant 留给 evaluate / workWindow 用;
|
|
182
|
+
// d2d 公式走 (arrUtcMs - depUtcMs)/60000,跨日 / DST / 反向日界线都正确。
|
|
183
|
+
depUtcMs: depR.utcMs,
|
|
184
|
+
arrUtcMs: arrR.utcMs,
|
|
185
|
+
}],
|
|
186
|
+
bufferMin: Number(l['buffer_min']),
|
|
187
|
+
originTransferMin: Number(l['origin_transfer_min']),
|
|
188
|
+
destTransferMin: Number(l['dest_transfer_min']),
|
|
189
|
+
redEye: Boolean(l['red_eye']),
|
|
190
|
+
redEyeDurationMin: Number(sv['red_eye_duration_min'] ?? l['red_eye_duration_min'] ?? 0),
|
|
191
|
+
groundRecoveryMin: l['ground_recovery_min'] ? Number(l['ground_recovery_min']) : undefined,
|
|
192
|
+
tzOffsetMin,
|
|
193
|
+
originTzOffsetMin: depR.offsetMin,
|
|
194
|
+
ianaDepZone: depZone,
|
|
195
|
+
ianaArrZone: arrZone,
|
|
196
|
+
},
|
|
197
|
+
depWeekday: weekdayFromYmd(svcDepDate),
|
|
198
|
+
};
|
|
199
|
+
});
|
|
200
|
+
const role = (l['services'].length === 1 ? 'fixed' : 'choice');
|
|
201
|
+
legs.push({
|
|
202
|
+
id: String(l['id']),
|
|
203
|
+
role,
|
|
204
|
+
note: l['note'] ? String(l['note']) : undefined,
|
|
205
|
+
date: legDate,
|
|
206
|
+
anchors: { arriveByMin: l['arrive_by'] ? hhmmToMin(String(l['arrive_by'])) : undefined },
|
|
207
|
+
route: routeHint(l['id']),
|
|
208
|
+
options,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
// work_window:home_zone 必需(v2 的 home 偏移走 IANA,数值字段不再权威);同时给两值报 reject。
|
|
212
|
+
let workWindow;
|
|
213
|
+
if (ww) {
|
|
214
|
+
const hasHomeZone = Boolean(ww['home_zone']);
|
|
215
|
+
const hasHomeNum = ww['home_tz_offset_min'] !== undefined;
|
|
216
|
+
if (hasHomeZone && hasHomeNum) {
|
|
217
|
+
throw Object.assign(new Error('parseFlightPackToSpecV2: work_window cannot combine home_zone and home_tz_offset_min'), { code: 'v2_ww_conflict' });
|
|
218
|
+
}
|
|
219
|
+
if (!hasHomeZone) {
|
|
220
|
+
throw Object.assign(new Error('parseFlightPackToSpecV2: work_window.home_zone is required for v2'), { code: 'v2_ww_home_zone_required' });
|
|
221
|
+
}
|
|
222
|
+
// 在 v2 parse 边界即时验证 home_zone 是已知 IANA zone(root counterexample #3:不许 fail-open)。
|
|
223
|
+
// 用 tz-resolver 的 isKnownZoneLoose:接受 Intl.DateTimeFormat 能识别的合法 alias(US/Pacific / ROK / Etc/GMT+8)。
|
|
224
|
+
const homeZone = String(ww['home_zone']);
|
|
225
|
+
if (!isKnownZoneLoose(homeZone)) {
|
|
226
|
+
throw Object.assign(new Error(`parseFlightPackToSpecV2: work_window.home_zone '${homeZone}' is not a known IANA zone`), { code: 'v2_home_zone_unknown' });
|
|
227
|
+
}
|
|
228
|
+
const startHHmm = ww['start'] ? String(ww['start']) : undefined;
|
|
229
|
+
const endHHmm = ww['end'] ? String(ww['end']) : undefined;
|
|
230
|
+
const startMin = ww['start_min'] !== undefined ? Number(ww['start_min']) : (startHHmm ? hhmmToMin(startHHmm) : hhmmToMin('10:00'));
|
|
231
|
+
const endMin = ww['end_min'] !== undefined ? Number(ww['end_min']) : (endHHmm ? hhmmToMin(endHHmm) : hhmmToMin('19:00'));
|
|
232
|
+
// numeric homeTzOffsetMin 仅占位:在 v2 工作窗口过滤时按 home_zone + dep_date 重算;预填 leg-date 占位 0
|
|
233
|
+
workWindow = {
|
|
234
|
+
homeTzOffsetMin: 0,
|
|
235
|
+
startMin,
|
|
236
|
+
endMin,
|
|
237
|
+
workdays: ww['workdays'] ?? [0, 1, 2, 3, 4],
|
|
238
|
+
homeZone,
|
|
239
|
+
startHHmm,
|
|
240
|
+
endHHmm,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
return { segments: legs, workWindow, [FLIGHT_PACK_VERSION]: 2 };
|
|
244
|
+
}
|
|
245
|
+
const WEEKDAY_IDX = { mon: 0, tue: 1, wed: 2, thu: 3, fri: 4, sat: 5, sun: 6 };
|
|
246
|
+
const WEEKDAY_CN = { mon: '一', tue: '二', wed: '三', thu: '四', fri: '五', sat: '六', sun: '日' };
|
|
247
|
+
/** YYYY-MM-DD → mon/tue/wed/thu/fri/sat/sun;v2 路径给 option 标 depWeekday 用。 */
|
|
248
|
+
export function weekdayFromYmd(ymd) {
|
|
249
|
+
const m = ymd.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
250
|
+
if (!m)
|
|
251
|
+
throw new Error(`weekdayFromYmd: bad ymd ${ymd}`);
|
|
252
|
+
const dow = new Date(Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3]))).getUTCDay();
|
|
253
|
+
return ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'][dow];
|
|
254
|
+
}
|
|
255
|
+
/** M-1:工作日的工作窗口内起飞 → 排除理由;否则 null(与 py _work_window_blocks 对齐)。
|
|
256
|
+
*
|
|
257
|
+
* Issue #343 v2:home zone 存在时,必须把"出发真实 UTC instant"投影到 home zone
|
|
258
|
+
* 取该 zone 的 date/weekday/HH:MM(root counterexample #2:Tokyo 06:30 ≈ LA 14:30 prev day);
|
|
259
|
+
* 不再用 dep-local date 直接当 home date。
|
|
260
|
+
*
|
|
261
|
+
* root 评审复核:`depUtcMs` 是已知确定的 instant,直接 `readWallParts(homeZone, depUtcMs)`
|
|
262
|
+
* 取墙时分量 — 不要把 projected wall time 再喂给 `resolveOffsetForLocalDate` 重做 ±14h 扫描,
|
|
263
|
+
* 否则秋回退日 01:30 会被错误判为 overlap_ambiguous 而 fail-open(已知 instant 永远唯一)。
|
|
264
|
+
*
|
|
265
|
+
* dep/arr 的"墙上本地输入"(leg-level `dep`/`arr` 字段)由 parseFlightPackToSpecV2 在
|
|
266
|
+
* 解析时已用 `resolveOffsetForLocalDate` 拒过 gap/overlap,所以 leg 进 solver 时
|
|
267
|
+
* 两个 instant 一定 strict-after 且偏移已知。
|
|
268
|
+
*
|
|
269
|
+
* home zone 未知时由 v2 parse 边界直接抛 v2_home_zone_unknown 拒绝;
|
|
270
|
+
* 这里兜底只在 v1 legacy 路径出现。
|
|
271
|
+
*/
|
|
147
272
|
function workWindowBlocks(spec, option) {
|
|
148
273
|
const ww = spec.workWindow;
|
|
149
|
-
if (!ww || !option.move || !option.depWeekday)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
274
|
+
if (!ww || !option.move || !option.depWeekday)
|
|
275
|
+
return null;
|
|
276
|
+
const svc = option.move.services[0];
|
|
277
|
+
const dep = svc.depMin;
|
|
278
|
+
const homeZone = ww.homeZone;
|
|
279
|
+
let homeOffsetMin = ww.homeTzOffsetMin;
|
|
280
|
+
let homeDepMin;
|
|
281
|
+
let homeDepWeekday;
|
|
282
|
+
if (homeZone) {
|
|
283
|
+
if (typeof svc.depUtcMs !== 'number') {
|
|
284
|
+
// v2 路径必然填了 depUtcMs;没填 = 数据病,按 red flag 排除该 option。
|
|
285
|
+
return i18nT('un.workwindow_unresolvable_reason', {
|
|
286
|
+
reason: `home zone ${homeZone} requires UTC dep instant (v2 path bug)`,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
// 已知 instant 直接投影到 home zone 的墙时 — 无歧义,不必也不应重走 ±14h 扫描。
|
|
290
|
+
const homeParts = readWallParts(homeZone, svc.depUtcMs);
|
|
291
|
+
const homeIso = wallPartsToIso(homeParts);
|
|
292
|
+
homeDepMin = hhmmToMin(homeIso.hhmm);
|
|
293
|
+
homeDepWeekday = weekdayFromYmd(homeIso.ymd);
|
|
294
|
+
}
|
|
295
|
+
const effectiveDep = homeDepMin ?? dep;
|
|
296
|
+
const effectiveWeekday = homeDepWeekday ?? option.depWeekday;
|
|
297
|
+
if (!((ww.workdays ?? [0, 1, 2, 3, 4]).includes(WEEKDAY_IDX[effectiveWeekday])))
|
|
298
|
+
return null;
|
|
299
|
+
// v2:startMin/endMin 与 homeDepMin 都是 home-zone 墙时分量(已 IANA 归一),
|
|
300
|
+
// 直接比较;不再做 originTzOffsetMin 偏移(root 复核该 shift 是 v1 数值残留,会让窗口错位)。
|
|
301
|
+
// v1 路径(无 homeZone, 走数值 homeTzOffsetMin)保留 shift 兼容旧 spec。
|
|
302
|
+
let start, end;
|
|
303
|
+
if (homeZone) {
|
|
304
|
+
start = ww.startMin;
|
|
305
|
+
end = ww.endMin;
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
const shift = (option.move.originTzOffsetMin ?? 480) - homeOffsetMin;
|
|
309
|
+
start = ww.startMin + shift;
|
|
310
|
+
end = ww.endMin + shift;
|
|
311
|
+
}
|
|
312
|
+
if (start <= effectiveDep && effectiveDep <= end) {
|
|
313
|
+
const hhmm = (m) => `${String(Math.floor(m / 60)).padStart(2, '0')}:${String(m % 60).padStart(2, '0')}`;
|
|
162
314
|
return i18nT('un.workwindow_reason', {
|
|
163
|
-
wd: WEEKDAY_CN[
|
|
164
|
-
dep: hhmm(dep),
|
|
165
|
-
start: hhmm(start),
|
|
166
|
-
end: hhmm(end)
|
|
315
|
+
wd: WEEKDAY_CN[effectiveWeekday] ?? '', dep: hhmm(effectiveDep), start: hhmm(start), end: hhmm(end),
|
|
167
316
|
});
|
|
168
317
|
}
|
|
169
318
|
return null;
|
|
170
319
|
}
|
|
320
|
+
/** D-5:时区感知的段核算(与 py unified._evaluate_option_move 对齐)。
|
|
321
|
+
*
|
|
322
|
+
* Issue #343 d2d 算术唯一权威入口 = model.ts 的 `doorToDoorFromMove`。
|
|
323
|
+
* v2 路径会传 Service.depUtcMs/arrUtcMs(由 parseFlightPackToSpecV2 用 tz-resolver 投出),
|
|
324
|
+
* 此时 d2d 用真实 UTC instant 差 — 跨日 / 反向日界线 / DST 切换都正确;v2 arr<=dep 抛
|
|
325
|
+
* NonpositiveDurationError → 该 option 当作不可行(由 solveUnifiedInner 转 unsat_core)。
|
|
326
|
+
* v1 数值口径无 UTC 字段,走老 (arrMin - depMin) - tzOffsetMin;字节不变。
|
|
327
|
+
*/
|
|
171
328
|
function evaluateOptionMove(segId, mv) {
|
|
172
329
|
const svc = mv.services[0];
|
|
330
|
+
let d2dMin;
|
|
331
|
+
try {
|
|
332
|
+
d2dMin = doorToDoorFromMove(svc, mv).doorToDoorMin;
|
|
333
|
+
}
|
|
334
|
+
catch (e) {
|
|
335
|
+
if (e instanceof NonpositiveDurationError) {
|
|
336
|
+
// d2d 病态输入 → 把这条 option 抛红 flag;这里走结构化 reject 而非吞错。
|
|
337
|
+
throw Object.assign(new Error(`evaluateOptionMove ${segId}/${svc.id}: ${e.message}`), {
|
|
338
|
+
code: e.code,
|
|
339
|
+
kind: e.kind,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
throw e;
|
|
343
|
+
}
|
|
173
344
|
const wake = svc.depMin - mv.bufferMin - mv.originTransferMin;
|
|
174
|
-
const trueFlight = svc.arrMin - svc.depMin - (mv.tzOffsetMin ?? 0);
|
|
175
|
-
const d2d = mv.bufferMin + mv.originTransferMin + trueFlight + mv.destTransferMin;
|
|
176
345
|
const wakeDisplay = wake < 0 ? `${minToHhmm(wake + 1440)}(前一日)` : minToHhmm(wake);
|
|
177
346
|
const arriveStay = svc.arrMin + mv.destTransferMin;
|
|
178
347
|
let energy;
|
|
179
348
|
if (mv.redEye && (mv.redEyeDurationMin ?? 0) > 0) {
|
|
180
349
|
const sleepH = ((mv.redEyeDurationMin ?? 0) - 60) / 60;
|
|
350
|
+
// D-6 校准:机上睡眠上限 75;落地接驳(destTransferMin)乘车补眠回血(1h≈+5%,上限 80)
|
|
181
351
|
const groundH = (mv.groundRecoveryMin ?? mv.destTransferMin ?? 0) / 60;
|
|
182
352
|
const recovery = Math.min(5, 5 * groundH);
|
|
183
353
|
energy = Math.max(30, Math.min(75 + recovery, 80, 30 + 8 * sleepH + recovery));
|
|
184
|
-
}
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
185
356
|
energy = 100 - 2 * 8;
|
|
186
|
-
if (wake < 5 * 60)
|
|
187
|
-
|
|
188
|
-
if (
|
|
189
|
-
|
|
357
|
+
if (wake < 5 * 60)
|
|
358
|
+
energy -= 30;
|
|
359
|
+
else if (wake < 6 * 60)
|
|
360
|
+
energy -= 25;
|
|
361
|
+
if (arriveStay > 21 * 60)
|
|
362
|
+
energy -= 10;
|
|
363
|
+
if (d2dMin > 6 * 60)
|
|
364
|
+
energy -= 10;
|
|
190
365
|
energy = Math.max(0, energy);
|
|
191
366
|
}
|
|
192
367
|
return {
|
|
@@ -195,157 +370,180 @@ function evaluateOptionMove(segId, mv) {
|
|
|
195
370
|
wake: wakeDisplay,
|
|
196
371
|
wakeMin: wake,
|
|
197
372
|
arrive_stay: minToHhmm(arriveStay),
|
|
198
|
-
door_to_door: `${Math.floor(
|
|
199
|
-
d2d_min:
|
|
373
|
+
door_to_door: `${Math.floor(d2dMin / 60)}h${String(d2dMin % 60).padStart(2, '0')}m`,
|
|
374
|
+
d2d_min: d2dMin,
|
|
200
375
|
energy_pct: Math.round(energy),
|
|
201
|
-
price_cny: svc.priceCny
|
|
376
|
+
price_cny: svc.priceCny,
|
|
202
377
|
};
|
|
203
378
|
}
|
|
379
|
+
function releaseZ3(resource) {
|
|
380
|
+
if (typeof resource?.release === 'function')
|
|
381
|
+
resource.release();
|
|
382
|
+
}
|
|
383
|
+
/** 航班链形态求解:按 Option 选择,锚点命名约束,core 剥竖线(D-2 修复) */
|
|
204
384
|
export async function solveUnified(spec) {
|
|
385
|
+
// WASM 防护:如果 z3-solver 加载或求解触发 memory access 错误,不让异常穿透到进程层把 dsh 杀掉。
|
|
386
|
+
// 候选形态走 solveChoiceSegment 不经过这里——这里是显式航班链路径,用户量较少。
|
|
205
387
|
try {
|
|
206
388
|
return await solveUnifiedInner(spec);
|
|
207
|
-
}
|
|
389
|
+
}
|
|
390
|
+
catch (e) {
|
|
208
391
|
console.error('[gotry] solveUnified failed (likely wasm thread race):', e.message?.slice(0, 200));
|
|
209
|
-
return {
|
|
210
|
-
feasible: false,
|
|
211
|
-
unsat_core: [
|
|
212
|
-
'wasm_runtime_error'
|
|
213
|
-
],
|
|
214
|
-
red_flags: [
|
|
215
|
-
'WASM 求解器异常,建议下次用候选形态(枚举)重试'
|
|
216
|
-
]
|
|
217
|
-
};
|
|
392
|
+
return { feasible: false, unsat_core: ['wasm_runtime_error'], red_flags: ['WASM 求解器异常,建议下次用候选形态(枚举)重试'] };
|
|
218
393
|
}
|
|
219
394
|
}
|
|
220
395
|
async function solveUnifiedInner(spec) {
|
|
396
|
+
// M-1:求解前的工作窗口确定性预过滤(与 py 对齐),排除理由入记录
|
|
397
|
+
// 骨架层(§7-1):三值语义标注——枢纽间否定只降权不排除(骨架滞后会错杀 EK329)
|
|
221
398
|
const skeletonNotes = [];
|
|
222
399
|
const exclusions = [];
|
|
223
|
-
for (const seg of spec.segments){
|
|
400
|
+
for (const seg of spec.segments) {
|
|
224
401
|
if (spec.skeletonHub) {
|
|
402
|
+
// 段级骨架查询:route 提示(如 "HKG->HKT")优先,否则跳过
|
|
225
403
|
const route = seg.route;
|
|
226
404
|
if (route) {
|
|
227
|
-
const [a, b] = route.split('->').map(
|
|
405
|
+
const [a, b] = route.split('->').map(s => s.trim());
|
|
228
406
|
const verdict = await checkConnectivity(a, b);
|
|
229
407
|
skeletonNotes.push(`${seg.id}: ${verdict.evidence}`);
|
|
230
408
|
}
|
|
231
409
|
}
|
|
232
|
-
const kept = seg.options.filter(
|
|
410
|
+
const kept = seg.options.filter(o => {
|
|
233
411
|
const reason = workWindowBlocks(spec, o);
|
|
234
|
-
if (reason)
|
|
235
|
-
segment: seg.id,
|
|
236
|
-
option: o.id,
|
|
237
|
-
reason
|
|
238
|
-
});
|
|
412
|
+
if (reason)
|
|
413
|
+
exclusions.push({ segment: seg.id, option: o.id, reason });
|
|
239
414
|
return !reason;
|
|
240
415
|
});
|
|
241
416
|
seg.options = kept;
|
|
242
417
|
if (kept.length === 0) {
|
|
243
|
-
return {
|
|
244
|
-
feasible: false,
|
|
245
|
-
unsat_core: [
|
|
246
|
-
`${seg.id}:work_window`
|
|
247
|
-
],
|
|
248
|
-
work_window_exclusions: exclusions
|
|
249
|
-
};
|
|
418
|
+
return { feasible: false, unsat_core: [`${seg.id}:work_window`], work_window_exclusions: exclusions };
|
|
250
419
|
}
|
|
251
420
|
}
|
|
252
|
-
|
|
421
|
+
// Z3 会话进互斥门(骨架/预过滤等网络段留在门外):solveUnifiedInner 的判定段
|
|
422
|
+
// 从 here 到 return 全部独占共享实例。
|
|
423
|
+
return withZ3('unified.solveUnifiedInner', async (z3) => {
|
|
253
424
|
const { Bool, If, Int, Solver, Sum } = z3;
|
|
254
425
|
const wakeFloor = spec.defaultWakeFloorMin ?? hhmmToMin('06:00');
|
|
255
426
|
const allSels = {};
|
|
256
427
|
const assertions = {};
|
|
257
428
|
const svcOf = {};
|
|
258
|
-
for (const seg of spec.segments){
|
|
259
|
-
const sels = seg.options.map((_, i)=>Bool.const(`${seg.id}_o${i}`));
|
|
429
|
+
for (const seg of spec.segments) {
|
|
430
|
+
const sels = seg.options.map((_, i) => Bool.const(`${seg.id}_o${i}`));
|
|
260
431
|
allSels[seg.id] = sels;
|
|
261
|
-
const svcs = seg.options.map(
|
|
432
|
+
const svcs = seg.options.map(o => o.move.services[0]);
|
|
262
433
|
svcOf[seg.id] = svcs;
|
|
263
|
-
const pick = (attr)=>Sum(...sels.map((s, i)=>If(s, Int.val(svcs[i][attr]), Int.val(0))));
|
|
434
|
+
const pick = (attr) => Sum(...sels.map((s, i) => If(s, Int.val(svcs[i][attr]), Int.val(0))));
|
|
264
435
|
const dep = pick('depMin'), arr = pick('arrMin');
|
|
265
436
|
const o0 = seg.options[0].move;
|
|
266
437
|
const wake = dep.sub(Int.val(o0.bufferMin + o0.originTransferMin));
|
|
267
438
|
const arriveStay = arr.add(Int.val(o0.destTransferMin));
|
|
268
|
-
if (seg.anchors?.arriveByMin !== undefined)
|
|
269
|
-
|
|
270
|
-
if (
|
|
439
|
+
if (seg.anchors?.arriveByMin !== undefined)
|
|
440
|
+
assertions[`${seg.id}:arrive_by`] = arriveStay.le(Int.val(seg.anchors.arriveByMin));
|
|
441
|
+
if (seg.anchors?.departAfterMin !== undefined)
|
|
442
|
+
assertions[`${seg.id}:depart_after`] = dep.ge(Int.val(seg.anchors.departAfterMin));
|
|
443
|
+
if (!seg.options.some(o => o.move?.redEye))
|
|
444
|
+
assertions[`${seg.id}:wake_floor`] = wake.ge(Int.val(wakeFloor));
|
|
271
445
|
}
|
|
272
|
-
const total = Sum(...spec.segments.flatMap(
|
|
273
|
-
if (spec.budgetCny !== undefined)
|
|
274
|
-
|
|
275
|
-
const
|
|
446
|
+
const total = Sum(...spec.segments.flatMap(seg => allSels[seg.id].map((s, i) => If(s, Int.val(svcOf[seg.id][i].priceCny), Int.val(0)))));
|
|
447
|
+
if (spec.budgetCny !== undefined)
|
|
448
|
+
assertions['total:budget'] = total.le(Int.val(spec.budgetCny));
|
|
449
|
+
const exactlyOne = (sels) => Sum(...sels.map((s) => If(s, Int.val(1), Int.val(0)))).eq(Int.val(1));
|
|
450
|
+
const coreOf = (s) => {
|
|
276
451
|
const v = s.unsatCore();
|
|
277
452
|
const out = [];
|
|
278
|
-
for(let i = 0; i < v.length(); i++)
|
|
453
|
+
for (let i = 0; i < v.length(); i++)
|
|
454
|
+
out.push(String(v.get(i)).replace(/^\|/, '').replace(/\|$/, ''));
|
|
279
455
|
return out;
|
|
280
456
|
};
|
|
281
457
|
const s = new Solver();
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
for(let i = 0; i < allSels[seg.id].length; i++){
|
|
288
|
-
const v = await maybeAwait(model.eval(allSels[seg.id][i], true));
|
|
289
|
-
if (String(v) !== 'true') continue;
|
|
290
|
-
const o = seg.options[i];
|
|
291
|
-
out.push({
|
|
292
|
-
leg: seg.id,
|
|
293
|
-
...evaluateOptionMove(seg.id, o.move)
|
|
294
|
-
});
|
|
295
|
-
break;
|
|
296
|
-
}
|
|
458
|
+
let sOpen = true;
|
|
459
|
+
const closeS = () => {
|
|
460
|
+
if (sOpen) {
|
|
461
|
+
releaseZ3(s);
|
|
462
|
+
sOpen = false;
|
|
297
463
|
}
|
|
298
|
-
return out;
|
|
299
464
|
};
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
465
|
+
try {
|
|
466
|
+
for (const sel of Object.values(allSels))
|
|
467
|
+
s.add(exactlyOne(sel));
|
|
468
|
+
for (const [name, expr] of Object.entries(assertions))
|
|
469
|
+
s.addAndTrack(expr, name);
|
|
470
|
+
const report = async (model) => {
|
|
471
|
+
const out = [];
|
|
472
|
+
for (const seg of spec.segments) {
|
|
473
|
+
for (let i = 0; i < allSels[seg.id].length; i++) {
|
|
474
|
+
const v = await maybeAwait(model.eval(allSels[seg.id][i], true));
|
|
475
|
+
if (String(v) !== 'true')
|
|
476
|
+
continue;
|
|
477
|
+
const o = seg.options[i];
|
|
478
|
+
out.push({ leg: seg.id, ...evaluateOptionMove(seg.id, o.move) });
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return out;
|
|
317
483
|
};
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const suggestions = [];
|
|
321
|
-
for (const name of core){
|
|
322
|
-
const s2 = new Solver();
|
|
323
|
-
for (const sel of Object.values(allSels))s2.add(exactlyOne(sel));
|
|
324
|
-
for (const [n2, expr] of Object.entries(assertions)){
|
|
325
|
-
if (n2 !== name) s2.addAndTrack(expr, n2);
|
|
484
|
+
async function maybeAwait(v) {
|
|
485
|
+
return v instanceof Promise ? await v : v;
|
|
326
486
|
}
|
|
327
|
-
if (String(await
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
487
|
+
if (String(await s.check()) !== 'unsat') {
|
|
488
|
+
const model = s.model();
|
|
489
|
+
try {
|
|
490
|
+
const legs = await report(model);
|
|
491
|
+
const money = legs.reduce((a, l) => a + l.price_cny, 0);
|
|
492
|
+
const redFlags = legs
|
|
493
|
+
.filter(l => spec.segments.find(sg => sg.id === l.leg)?.options.some(o => o.move?.redEye) && l.energy_pct < 50)
|
|
494
|
+
.map(l => i18nT('un.redflag_redeye', { leg: l.leg, pct: l.energy_pct }));
|
|
495
|
+
return { feasible: true, money_cny: money, legs, red_flags: redFlags, work_window_exclusions: exclusions, skeleton_notes: skeletonNotes.length ? skeletonNotes : undefined };
|
|
496
|
+
}
|
|
497
|
+
finally {
|
|
498
|
+
releaseZ3(model);
|
|
499
|
+
}
|
|
333
500
|
}
|
|
501
|
+
const core = coreOf(s).sort();
|
|
502
|
+
const suggestions = [];
|
|
503
|
+
closeS();
|
|
504
|
+
for (const name of core) {
|
|
505
|
+
const s2 = new Solver();
|
|
506
|
+
try {
|
|
507
|
+
for (const sel of Object.values(allSels))
|
|
508
|
+
s2.add(exactlyOne(sel));
|
|
509
|
+
for (const [n2, expr] of Object.entries(assertions)) {
|
|
510
|
+
if (n2 !== name)
|
|
511
|
+
s2.addAndTrack(expr, n2);
|
|
512
|
+
}
|
|
513
|
+
if (String(await s2.check()) !== 'unsat') {
|
|
514
|
+
const model = s2.model();
|
|
515
|
+
try {
|
|
516
|
+
const legs = await report(model);
|
|
517
|
+
suggestions.push({ relax: name, money_cny: legs.reduce((a, l) => a + l.price_cny, 0) });
|
|
518
|
+
}
|
|
519
|
+
finally {
|
|
520
|
+
releaseZ3(model);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
finally {
|
|
525
|
+
releaseZ3(s2);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return { feasible: false, unsat_core: core, suggestions };
|
|
529
|
+
}
|
|
530
|
+
finally {
|
|
531
|
+
closeS();
|
|
334
532
|
}
|
|
335
|
-
return {
|
|
336
|
-
feasible: false,
|
|
337
|
-
unsat_core: core,
|
|
338
|
-
suggestions
|
|
339
|
-
};
|
|
340
533
|
});
|
|
341
534
|
}
|
|
342
535
|
function checkTrueCost(t, spec) {
|
|
343
536
|
const fails = [];
|
|
344
|
-
if (t.wakeMin < spec.wakeFloorMin)
|
|
345
|
-
|
|
346
|
-
if (t.
|
|
347
|
-
|
|
348
|
-
if (t.
|
|
537
|
+
if (t.wakeMin < spec.wakeFloorMin)
|
|
538
|
+
fails.push('wake_floor');
|
|
539
|
+
if (t.energyArrivalPct < spec.minArrivalEnergyPct)
|
|
540
|
+
fails.push('energy_floor');
|
|
541
|
+
if (t.usableHours < spec.requiredUsableHours)
|
|
542
|
+
fails.push('usable_hours');
|
|
543
|
+
if (spec.budgetCny !== undefined && t.moneyCny > spec.budgetCny)
|
|
544
|
+
fails.push('budget');
|
|
545
|
+
if (t.arriveStayMin > spec.latestArriveStayMin)
|
|
546
|
+
fails.push('arrival_before_evening');
|
|
349
547
|
return fails;
|
|
350
548
|
}
|
|
351
549
|
function enumerateCombos(opt, cand, req, days) {
|
|
@@ -356,25 +554,15 @@ function enumerateCombos(opt, cand, req, days) {
|
|
|
356
554
|
minArrivalEnergyPct: req.motivation.minArrivalEnergyPct,
|
|
357
555
|
requiredUsableHours: requiredUsableHours(req.motivation),
|
|
358
556
|
budgetCny: req.budgetCny,
|
|
359
|
-
latestArriveStayMin: LATEST_ARRIVE_STAY_MIN
|
|
557
|
+
latestArriveStayMin: LATEST_ARRIVE_STAY_MIN,
|
|
360
558
|
};
|
|
361
|
-
for (const o of mv.services){
|
|
362
|
-
for (const tr of mv.transfers ?? []){
|
|
363
|
-
for (const r of mv.retServices ?? mv.services){
|
|
364
|
-
for (const trr of mv.transfers ?? []){
|
|
365
|
-
const ch = {
|
|
366
|
-
outService: o,
|
|
367
|
-
outTransfer: tr,
|
|
368
|
-
retService: r,
|
|
369
|
-
retTransfer: trr,
|
|
370
|
-
days
|
|
371
|
-
};
|
|
559
|
+
for (const o of mv.services) {
|
|
560
|
+
for (const tr of mv.transfers ?? []) {
|
|
561
|
+
for (const r of (mv.retServices ?? mv.services)) {
|
|
562
|
+
for (const trr of mv.transfers ?? []) {
|
|
563
|
+
const ch = { outService: o, outTransfer: tr, retService: r, retTransfer: trr, days };
|
|
372
564
|
const t = evaluateChoice(cand, req, ch);
|
|
373
|
-
out.push({
|
|
374
|
-
choice: ch,
|
|
375
|
-
cost: t,
|
|
376
|
-
fails: checkTrueCost(t, spec)
|
|
377
|
-
});
|
|
565
|
+
out.push({ choice: ch, cost: t, fails: checkTrueCost(t, spec) });
|
|
378
566
|
}
|
|
379
567
|
}
|
|
380
568
|
}
|
|
@@ -384,208 +572,143 @@ function enumerateCombos(opt, cand, req, days) {
|
|
|
384
572
|
function optionToCandidate(opt) {
|
|
385
573
|
const mv = opt.move, st = opt.stay;
|
|
386
574
|
return {
|
|
387
|
-
id: opt.id,
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
servicesOut: [
|
|
393
|
-
...mv.services
|
|
394
|
-
],
|
|
395
|
-
servicesRet: [
|
|
396
|
-
...mv.retServices ?? mv.services
|
|
397
|
-
],
|
|
398
|
-
destTransfers: [
|
|
399
|
-
...mv.transfers ?? []
|
|
400
|
-
],
|
|
401
|
-
stayCnyPerNight: st.stayCnyPerNight ?? 0,
|
|
402
|
-
localDailyCny: st.localDailyCny ?? 0,
|
|
575
|
+
id: opt.id, name: opt.label, hub: mv.hub,
|
|
576
|
+
bufferOutMin: mv.bufferMin, bufferRetMin: mv.bufferRetMin ?? mv.bufferMin,
|
|
577
|
+
servicesOut: [...mv.services], servicesRet: [...(mv.retServices ?? mv.services)],
|
|
578
|
+
destTransfers: [...(mv.transfers ?? [])],
|
|
579
|
+
stayCnyPerNight: st.stayCnyPerNight ?? 0, localDailyCny: st.localDailyCny ?? 0,
|
|
403
580
|
minDaysForPurpose: opt.minDays ?? 1,
|
|
404
|
-
imageryMatch: opt.score ?? 0,
|
|
405
|
-
bestMonths: opt.bestMonths ?? []
|
|
581
|
+
imageryMatch: opt.score ?? 0, bestMonths: opt.bestMonths ?? [],
|
|
406
582
|
};
|
|
407
583
|
}
|
|
584
|
+
/** 单 choice 段(目的地选项):逐 Option 枚举过滤,按 score 排序。与 py solve_choice_segment 等价。 */
|
|
408
585
|
export function solveChoiceSegment(spec, req) {
|
|
409
|
-
const seg = spec.segments.find(
|
|
410
|
-
if (!seg)
|
|
586
|
+
const seg = spec.segments.find(s => s.role === 'choice' && s.options.length > 0 && s.options[0].stay);
|
|
587
|
+
if (!seg)
|
|
588
|
+
throw new Error('solveChoiceSegment: no choice segment with stay found');
|
|
411
589
|
const windowDays = req.windowDays;
|
|
412
590
|
const verdicts = [];
|
|
413
|
-
for (const opt of seg.options){
|
|
591
|
+
for (const opt of seg.options) {
|
|
414
592
|
const cand = optionToCandidate(opt);
|
|
415
593
|
const minDays = cand.minDaysForPurpose;
|
|
416
594
|
const durationOk = windowDays >= minDays;
|
|
417
595
|
const combos = durationOk ? enumerateCombos(opt, cand, req, windowDays) : [];
|
|
418
|
-
const good = combos.filter(
|
|
596
|
+
const good = combos.filter(c => c.fails.length === 0);
|
|
419
597
|
if (good.length > 0) {
|
|
420
|
-
const best = good.reduce((a, b)=>a.cost.moneyCny <= b.cost.moneyCny ? a : b);
|
|
598
|
+
const best = good.reduce((a, b) => a.cost.moneyCny <= b.cost.moneyCny ? a : b);
|
|
421
599
|
verdicts.push({
|
|
422
|
-
candidate_id: opt.id,
|
|
423
|
-
name: opt.label,
|
|
424
|
-
feasible: true,
|
|
425
|
-
imagery_match: opt.score ?? 0,
|
|
600
|
+
candidate_id: opt.id, name: opt.label, feasible: true, imagery_match: opt.score ?? 0,
|
|
426
601
|
chosen: {
|
|
427
|
-
out_service: best.choice.outService.id,
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
ret_transfer: best.choice.retTransfer.mode,
|
|
431
|
-
days: best.choice.days
|
|
602
|
+
out_service: best.choice.outService.id, out_transfer: best.choice.outTransfer.mode,
|
|
603
|
+
ret_service: best.choice.retService.id, ret_transfer: best.choice.retTransfer.mode,
|
|
604
|
+
days: best.choice.days,
|
|
432
605
|
},
|
|
433
|
-
true_cost: trueCostToDict(best.cost)
|
|
606
|
+
true_cost: trueCostToDict(best.cost),
|
|
434
607
|
});
|
|
435
608
|
continue;
|
|
436
609
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
610
|
+
// 不可行:归因 + duration 换长口径的 wish pool
|
|
611
|
+
const blocked = durationOk
|
|
612
|
+
? Array.from(new Set(combos.flatMap(c => c.fails))).sort()
|
|
613
|
+
: ['duration'];
|
|
440
614
|
const suggestions = [];
|
|
441
|
-
for (const name of blocked){
|
|
442
|
-
const opened = combos.filter(
|
|
615
|
+
for (const name of blocked) {
|
|
616
|
+
const opened = combos.filter(c => !c.fails.includes(name));
|
|
443
617
|
if (opened.length > 0) {
|
|
444
|
-
const cheapest = opened.reduce((a, b)=>a.cost.moneyCny <= b.cost.moneyCny ? a : b);
|
|
445
|
-
suggestions.push({
|
|
446
|
-
relax: name,
|
|
447
|
-
resulting_money_cny: cheapest.cost.moneyCny
|
|
448
|
-
});
|
|
618
|
+
const cheapest = opened.reduce((a, b) => a.cost.moneyCny <= b.cost.moneyCny ? a : b);
|
|
619
|
+
suggestions.push({ relax: name, resulting_money_cny: cheapest.cost.moneyCny });
|
|
449
620
|
}
|
|
450
621
|
}
|
|
451
622
|
let wish = null;
|
|
452
623
|
if (!durationOk) {
|
|
453
624
|
const longCombos = enumerateCombos(opt, cand, req, minDays);
|
|
454
|
-
const budgetDropped = longCombos.filter(
|
|
455
|
-
const conds = {
|
|
456
|
-
days: minDays
|
|
457
|
-
};
|
|
625
|
+
const budgetDropped = longCombos.filter(c => c.fails.length === 0 || (c.fails.length === 1 && c.fails[0] === 'budget'));
|
|
626
|
+
const conds = { days: minDays };
|
|
458
627
|
if (budgetDropped.length > 0) {
|
|
459
|
-
conds['budget_cny'] = Math.min(...budgetDropped.map(
|
|
628
|
+
conds['budget_cny'] = Math.min(...budgetDropped.map(c => c.cost.moneyCny));
|
|
460
629
|
}
|
|
461
|
-
if (opt.bestMonths?.length)
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
conditions: conds,
|
|
465
|
-
reason: i18nT('sg.wish_reason_window', {
|
|
466
|
-
days: windowDays,
|
|
467
|
-
minDays
|
|
468
|
-
})
|
|
469
|
-
};
|
|
630
|
+
if (opt.bestMonths?.length)
|
|
631
|
+
conds['best_months'] = opt.bestMonths;
|
|
632
|
+
wish = { name: opt.label, conditions: conds, reason: i18nT('sg.wish_reason_window', { days: windowDays, minDays }) };
|
|
470
633
|
}
|
|
471
634
|
verdicts.push({
|
|
472
|
-
candidate_id: opt.id,
|
|
473
|
-
|
|
474
|
-
feasible: false,
|
|
475
|
-
imagery_match: opt.score ?? 0,
|
|
476
|
-
unsat_core: blocked,
|
|
477
|
-
suggestions,
|
|
478
|
-
wish_pool: wish
|
|
635
|
+
candidate_id: opt.id, name: opt.label, feasible: false, imagery_match: opt.score ?? 0,
|
|
636
|
+
unsat_core: blocked, suggestions, wish_pool: wish,
|
|
479
637
|
});
|
|
480
638
|
}
|
|
481
|
-
const feasible = verdicts.filter(
|
|
639
|
+
const feasible = verdicts.filter(v => v.feasible).sort((a, b) => b.imagery_match - a.imagery_match);
|
|
482
640
|
return {
|
|
483
641
|
verdicts,
|
|
484
642
|
recommended: feasible.length > 0 ? feasible[0]['candidate_id'] : null,
|
|
485
|
-
answer_md: renderCandidateMarkdown(req, verdicts, feasible.length > 0 ? String(feasible[0]['candidate_id']) : null)
|
|
643
|
+
answer_md: renderCandidateMarkdown(req, verdicts, feasible.length > 0 ? String(feasible[0]['candidate_id']) : null),
|
|
486
644
|
};
|
|
487
645
|
}
|
|
488
646
|
function renderCandidateMarkdown(req, verdicts, recommended) {
|
|
489
|
-
const feasible = verdicts.filter(
|
|
490
|
-
const parked = verdicts.filter(
|
|
647
|
+
const feasible = verdicts.filter(v => v['feasible']).sort((a, b) => b['imagery_match'] - a['imagery_match']);
|
|
648
|
+
const parked = verdicts.filter(v => !v['feasible']);
|
|
491
649
|
const lines = [];
|
|
492
|
-
lines.push(i18nT('md.header', {
|
|
493
|
-
note: req.note
|
|
494
|
-
}));
|
|
650
|
+
lines.push(i18nT('md.header', { note: req.note }));
|
|
495
651
|
lines.push(i18nT('md.constraints', {
|
|
496
|
-
days: req.windowDays,
|
|
497
|
-
|
|
498
|
-
hours: requiredUsableHours(req.motivation).toFixed(1)
|
|
652
|
+
days: req.windowDays, budget: req.budgetCny,
|
|
653
|
+
hours: requiredUsableHours(req.motivation).toFixed(1),
|
|
499
654
|
}));
|
|
500
655
|
lines.push('');
|
|
501
|
-
for (const v of parked){
|
|
656
|
+
for (const v of parked) {
|
|
502
657
|
const core = v['unsat_core'] ?? [];
|
|
503
|
-
lines.push(i18nT('md.infeasible', {
|
|
504
|
-
name: String(v['name']),
|
|
505
|
-
core: core.join('、')
|
|
506
|
-
}));
|
|
658
|
+
lines.push(i18nT('md.infeasible', { name: String(v['name']), core: core.join('、') }));
|
|
507
659
|
const sug = (v['suggestions'] ?? [])[0];
|
|
508
|
-
if (sug)
|
|
509
|
-
relax: String(sug['relax']),
|
|
510
|
-
money: String(sug['resulting_money_cny'] ?? '')
|
|
511
|
-
}));
|
|
660
|
+
if (sug)
|
|
661
|
+
lines.push(i18nT('md.relax_one', { relax: String(sug['relax']), money: String(sug['resulting_money_cny'] ?? '') }));
|
|
512
662
|
const wish = v['wish_pool'];
|
|
513
663
|
if (wish) {
|
|
514
664
|
const conds = wish['conditions'];
|
|
515
|
-
const budgetNote = conds['budget_cny'] ? i18nT('md.wish_budget', {
|
|
516
|
-
cny: String(conds['budget_cny'])
|
|
517
|
-
}) : '';
|
|
665
|
+
const budgetNote = conds['budget_cny'] ? i18nT('md.wish_budget', { cny: String(conds['budget_cny']) }) : '';
|
|
518
666
|
const months = conds['best_months'];
|
|
519
|
-
const season = months ? i18nT('md.wish_season', {
|
|
520
|
-
|
|
521
|
-
}) : '';
|
|
522
|
-
lines.push(i18nT('md.wish_pool', {
|
|
523
|
-
days: String(conds['days']),
|
|
524
|
-
budgetNote,
|
|
525
|
-
season
|
|
526
|
-
}));
|
|
667
|
+
const season = months ? i18nT('md.wish_season', { months: String(months) }) : '';
|
|
668
|
+
lines.push(i18nT('md.wish_pool', { days: String(conds['days']), budgetNote, season }));
|
|
527
669
|
}
|
|
528
670
|
}
|
|
529
671
|
lines.push('');
|
|
530
|
-
for (const v of feasible){
|
|
672
|
+
for (const v of feasible) {
|
|
531
673
|
const ch = v['chosen'];
|
|
532
674
|
const tc = v['true_cost'];
|
|
533
675
|
lines.push(i18nT('md.feasible_unified', {
|
|
534
|
-
name: String(v['name']),
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
wake: String(tc['wake']),
|
|
538
|
-
energy: String(tc['energy_arrival_pct']),
|
|
539
|
-
d2d: String(tc['door_to_door_out']),
|
|
540
|
-
hours: String(tc['usable_hours']),
|
|
541
|
-
money: String(tc['money_cny'])
|
|
676
|
+
name: String(v['name']), svc: String(ch['out_service']), mode: String(ch['out_transfer']),
|
|
677
|
+
wake: String(tc['wake']), energy: String(tc['energy_arrival_pct']), d2d: String(tc['door_to_door_out']),
|
|
678
|
+
hours: String(tc['usable_hours']), money: String(tc['money_cny']),
|
|
542
679
|
}));
|
|
543
680
|
}
|
|
544
681
|
if (feasible.length) {
|
|
545
682
|
lines.push('');
|
|
546
683
|
const best = feasible[0];
|
|
547
684
|
const alt = feasible[1];
|
|
548
|
-
lines.push(i18nT('md.recommend', {
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
money: String(alt['true_cost']['money_cny']),
|
|
554
|
-
match: (alt['imagery_match'] * 100).toFixed(0)
|
|
555
|
-
}) : ''));
|
|
685
|
+
lines.push(i18nT('md.recommend', { best: String(best['name']), match: (best['imagery_match'] * 100).toFixed(0) })
|
|
686
|
+
+ (alt ? i18nT('md.alt', {
|
|
687
|
+
name: String(alt['name']), money: String(alt['true_cost']['money_cny']),
|
|
688
|
+
match: (alt['imagery_match'] * 100).toFixed(0),
|
|
689
|
+
}) : ''));
|
|
556
690
|
}
|
|
557
691
|
lines.push('');
|
|
558
692
|
lines.push(i18nT('md.decide_two'));
|
|
559
693
|
if (feasible.length >= 2) {
|
|
560
|
-
lines.push(i18nT('md.q_choice', {
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
}));
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
name: String(feasible[0]['name'])
|
|
567
|
-
}));
|
|
568
|
-
} else {
|
|
694
|
+
lines.push(i18nT('md.q_choice', { a: String(feasible[0]['name']), b: String(feasible[1]['name']) }));
|
|
695
|
+
}
|
|
696
|
+
else if (feasible.length === 1) {
|
|
697
|
+
lines.push(i18nT('md.q_single', { name: String(feasible[0]['name']) }));
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
569
700
|
lines.push(i18nT('md.q_none'));
|
|
570
701
|
}
|
|
571
702
|
const p0 = parked[0];
|
|
572
703
|
const p0wish = p0?.['wish_pool'];
|
|
573
704
|
if (p0wish) {
|
|
574
705
|
const conds = p0wish['conditions'];
|
|
575
|
-
lines.push(i18nT('md.q_wish', {
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
}));
|
|
579
|
-
} else if (feasible.length) {
|
|
706
|
+
lines.push(i18nT('md.q_wish', { name: String(p0['name']), days: String(conds['days']) }));
|
|
707
|
+
}
|
|
708
|
+
else if (feasible.length) {
|
|
580
709
|
const ch = feasible[0]['chosen'];
|
|
581
|
-
lines.push(i18nT('md.q_depart', {
|
|
582
|
-
id: String(ch['out_service']),
|
|
583
|
-
dep: ''
|
|
584
|
-
}));
|
|
710
|
+
lines.push(i18nT('md.q_depart', { id: String(ch['out_service']), dep: '' }));
|
|
585
711
|
}
|
|
586
712
|
return lines.join('\n');
|
|
587
713
|
}
|
|
588
714
|
export { minToHhmm };
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
//# sourceURL=ts/src/unified.ts
|