@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/ts/src/loop.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import type { InterviewQuestion, TripState, TravelerProfile, Turn, CalendarState, SolveResult } from './contracts.ts'
|
|
10
10
|
import type { JourneySpecTS } from './unified.ts'
|
|
11
|
+
import { buildTimeAnchor, isRealIsoDate, planningWindowBounds, parsePlanningWindow, type PlanningWindow, type TimeAnchor } from './time-anchor.ts'
|
|
11
12
|
import type { TravelSlotExtraction } from './travel-slots.ts'
|
|
12
13
|
import { anythingSearch } from '../capabilities/anything.ts'
|
|
13
14
|
|
|
@@ -95,6 +96,98 @@ export function validateSpec(spec: JourneySpecTS): string | null {
|
|
|
95
96
|
return null
|
|
96
97
|
}
|
|
97
98
|
|
|
99
|
+
export interface PlanningWindowRejection {
|
|
100
|
+
segmentId: string
|
|
101
|
+
optionId: string
|
|
102
|
+
date: string
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface PlanningWindowCheck {
|
|
106
|
+
spec: JourneySpecTS
|
|
107
|
+
rejected: PlanningWindowRejection[]
|
|
108
|
+
error?: string
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface PlanningWindowOptions {
|
|
112
|
+
/** Registered feasibility tool opts into the dated-candidate default future floor. */
|
|
113
|
+
defaultDatedFuture?: boolean
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isoDateInBounds(date: string, bounds: { start: string; end: string }): boolean {
|
|
117
|
+
const m = date.match(/^(\d{4})-(\d{2})-(\d{2})$/)
|
|
118
|
+
if (!m || !isRealIsoDate(Number(m[1]), Number(m[2]), Number(m[3]))) return false
|
|
119
|
+
return date >= bounds.start && date <= bounds.end
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function hasDatedCandidates(spec: JourneySpecTS): boolean {
|
|
123
|
+
return spec.segments.some(segment => segment.date !== undefined || segment.options.some(option => option.date !== undefined))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Enforce a named-year future window at the planner boundary. Historical
|
|
128
|
+
* lookup/backtesting remains an explicit bypass; an expired named year fails
|
|
129
|
+
* instead of being silently rolled into a different year.
|
|
130
|
+
*/
|
|
131
|
+
export function applyPlanningWindow(
|
|
132
|
+
spec: JourneySpecTS,
|
|
133
|
+
window: PlanningWindow | null,
|
|
134
|
+
anchor: TimeAnchor,
|
|
135
|
+
options: PlanningWindowOptions = {},
|
|
136
|
+
): PlanningWindowCheck {
|
|
137
|
+
// Dated recommendations are future-oriented even without optional named-year context.
|
|
138
|
+
// Fully dateless legacy calculations remain untouched; historical bypass is explicit.
|
|
139
|
+
if (window?.intent === 'historical') return { spec, rejected: [] }
|
|
140
|
+
const implicitFutureFloor = !window && options.defaultDatedFuture === true && hasDatedCandidates(spec)
|
|
141
|
+
if (!window && !implicitFutureFloor) return { spec, rejected: [] }
|
|
142
|
+
|
|
143
|
+
const currentYear = Number(anchor.today.slice(0, 4))
|
|
144
|
+
if (window && window.requestedYear < currentYear) {
|
|
145
|
+
return {
|
|
146
|
+
spec,
|
|
147
|
+
rejected: [],
|
|
148
|
+
error: `${window.requestedYear} 年已结束(当前规划参考日 ${anchor.today}),不会自动改用其他年份;如需历史查询请明确说明“历史/回测”。`,
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const bounds = window
|
|
153
|
+
? planningWindowBounds(window, anchor)
|
|
154
|
+
: { start: anchor.today, end: '9999-12-31' }
|
|
155
|
+
if (!bounds) return { spec, rejected: [], error: `无法建立 ${window!.requestedYear} 年的未来规划窗口。` }
|
|
156
|
+
|
|
157
|
+
const rejected: PlanningWindowRejection[] = []
|
|
158
|
+
const guardedSegments = spec.segments.map(segment => ({
|
|
159
|
+
...segment,
|
|
160
|
+
options: segment.options.filter(option => {
|
|
161
|
+
const date = option.date ?? segment.date
|
|
162
|
+
if (date && isoDateInBounds(date, bounds)) return true
|
|
163
|
+
rejected.push({ segmentId: segment.id, optionId: option.id, date: date ?? '未提供明确日期' })
|
|
164
|
+
return false
|
|
165
|
+
}),
|
|
166
|
+
}))
|
|
167
|
+
|
|
168
|
+
if (guardedSegments.some(segment => segment.options.length === 0)) {
|
|
169
|
+
return {
|
|
170
|
+
spec: { ...spec, segments: guardedSegments },
|
|
171
|
+
rejected,
|
|
172
|
+
error: implicitFutureFloor
|
|
173
|
+
? `带日期的未来推荐必须不早于当前规划参考日 ${bounds.start};没有带明确日期且在未来窗口内的可接受方案。`
|
|
174
|
+
: `${window!.requestedYear} 年未来规划窗口为 ${bounds.start} 至 ${bounds.end};没有带明确日期且落在窗口内的可接受方案。`,
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return { spec: { ...spec, segments: guardedSegments }, rejected }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function latestPlanningWindow(history: Turn[], userMsg: string, anchor: TimeAnchor): PlanningWindow | null {
|
|
181
|
+
const turns = [...history, { role: 'user' as const, text: userMsg }]
|
|
182
|
+
for (let i = turns.length - 1; i >= 0; i--) {
|
|
183
|
+
const turn = turns[i]
|
|
184
|
+
if (turn?.role !== 'user') continue
|
|
185
|
+
const parsed = parsePlanningWindow(turn.text, anchor)
|
|
186
|
+
if (parsed) return parsed
|
|
187
|
+
}
|
|
188
|
+
return null
|
|
189
|
+
}
|
|
190
|
+
|
|
98
191
|
/** 求解端口:S3 起由 unified 求解器实现;循环只认此签名(确定性责任)。 */
|
|
99
192
|
export type SolvePort = (spec: JourneySpecTS) => Promise<SolveResult>
|
|
100
193
|
|
|
@@ -206,15 +299,15 @@ export function renderSolve(state: TripState): string {
|
|
|
206
299
|
lines.push(`- ${l['leg']} ${l['service']}:${l['dep']} 起飞,${l['wake']} 出发,${l['arrive_stay']} 到,`
|
|
207
300
|
+ `门到门 ${l['door_to_door']},落地精力 ${l['energy_pct']}%,¥${l['price_cny']}`)
|
|
208
301
|
}
|
|
209
|
-
for (const e of s.work_window_exclusions ?? []) {
|
|
210
|
-
lines.push(`- 已排除 ${e.option}(工作窗口):${e.reason}`)
|
|
211
|
-
}
|
|
212
302
|
for (const n of s.skeleton_notes ?? []) lines.push(`- ${n}`)
|
|
213
303
|
for (const f of s.red_flags ?? []) lines.push(`- ⚠️ ${f}`)
|
|
214
304
|
} else {
|
|
215
305
|
lines.push(`**当前约束下不可行——冲突:${(s.unsat_core ?? []).join('、')}`)
|
|
216
306
|
for (const sg of s.suggestions ?? []) lines.push(`- 放宽「${sg.relax}」可解(约 ¥${sg.money_cny})`)
|
|
217
307
|
}
|
|
308
|
+
for (const e of s.work_window_exclusions ?? []) {
|
|
309
|
+
lines.push(`- 已排除 ${e.option}(工作窗口):${e.reason}`)
|
|
310
|
+
}
|
|
218
311
|
if (state.gates.length) {
|
|
219
312
|
lines.push('', '**待你决定(选择题)**')
|
|
220
313
|
for (const g of state.gates) {
|
|
@@ -231,7 +324,9 @@ export async function runTurn(
|
|
|
231
324
|
llm: LlmPort,
|
|
232
325
|
history: Turn[] = [],
|
|
233
326
|
solve: SolvePort | null = null,
|
|
327
|
+
now: Date = new Date(),
|
|
234
328
|
): Promise<{ reply: string; state: TripState }> {
|
|
329
|
+
const anchor = buildTimeAnchor(now)
|
|
235
330
|
const facts = await llm.extractFacts([...history, { role: 'user', text: userMsg }], state)
|
|
236
331
|
|
|
237
332
|
const conflicts: string[] = []
|
|
@@ -287,7 +382,22 @@ export async function runTurn(
|
|
|
287
382
|
|
|
288
383
|
// 约束齐备(无阻塞问题)→ 翻译 spec → **校验闸 + 日期一致性闸** → 求解 → 渲染
|
|
289
384
|
if (blocking.length === 0 && solve) {
|
|
290
|
-
const
|
|
385
|
+
const extractedSpec = await llm.extractSpec([...history, { role: 'user', text: userMsg }], state)
|
|
386
|
+
const planning = latestPlanningWindow(history, userMsg, anchor)
|
|
387
|
+
const planningCheck = extractedSpec ? applyPlanningWindow(extractedSpec, planning, anchor) : null
|
|
388
|
+
if (planningCheck?.error) {
|
|
389
|
+
state.spec = undefined
|
|
390
|
+
state.solve = undefined
|
|
391
|
+
parts.push(`⚠️ ${planningCheck.error}`)
|
|
392
|
+
return { reply: parts.join('\n\n'), state }
|
|
393
|
+
}
|
|
394
|
+
if (planningCheck?.rejected.length) {
|
|
395
|
+
const rejected = planningCheck.rejected
|
|
396
|
+
.map(r => `${r.segmentId}/${r.optionId}(${r.date})`)
|
|
397
|
+
.join('、')
|
|
398
|
+
parts.push(`已排除规划窗口外的候选:${rejected}`)
|
|
399
|
+
}
|
|
400
|
+
const spec = planningCheck?.spec ?? extractedSpec
|
|
291
401
|
// 场景路由:erhai 候选标记 → 候选求解(洱海金标准的引擎判定)——纯 TS unify 路径
|
|
292
402
|
if (spec && (spec as unknown as { note?: string }).note === 'erhai-candidates') {
|
|
293
403
|
const { readFile } = await import('node:fs/promises')
|
|
@@ -311,11 +421,10 @@ export async function runTurn(
|
|
|
311
421
|
// D-10 切片 C:spec↔槽位日期一致性闸(ADR-10 翻译≠造数的执行点)。
|
|
312
422
|
// LLM 翻译的 spec 日期与代码层换算的槽位日期分歧时不求解、不猜,追问确认——
|
|
313
423
|
// 日期是求解关键输入,错日期 = 错判决。槽位缺失/unresolved 不参与(不造假阳性)。
|
|
314
|
-
const ext = await llm.extractSlots([...history, { role: 'user', text: userMsg }])
|
|
424
|
+
const ext = await llm.extractSlots([...history, { role: 'user', text: userMsg }], now)
|
|
315
425
|
if (ext) {
|
|
316
426
|
const { resolveSlots, specDateMismatches } = await import('./slot-spec.ts')
|
|
317
|
-
const
|
|
318
|
-
const mismatches = specDateMismatches(spec, resolveSlots(ext, buildTimeAnchor()))
|
|
427
|
+
const mismatches = specDateMismatches(spec, resolveSlots(ext, anchor))
|
|
319
428
|
if (mismatches.length > 0) {
|
|
320
429
|
const lines = mismatches.map(m => `- ${m.specAt}:翻译说 ${m.specDate},你的原话换算是 ${m.slotDate}(${m.field})`)
|
|
321
430
|
parts.push(`⚠️ 日期分歧,确认后我再算(以你为准):\n${lines.join('\n')}`)
|
package/ts/src/memory-capture.ts
CHANGED
|
@@ -9,31 +9,143 @@
|
|
|
9
9
|
* ① 追加不删史(evidence 永不移除,P0 反幻觉:历史依据不可篡改)
|
|
10
10
|
* ② 幂等(同 evidence 不重复入池)
|
|
11
11
|
* ③ 结构守卫(weights 存在性校验、空补丁拒绝、权重归一)
|
|
12
|
+
*
|
|
13
|
+
* 额外承载(issue #338):用户显式声明的常驻城市/默认出发地——typed
|
|
14
|
+
* homeCity 字段 + 纯函数 resolveDefaultOrigin(explicit origin, profile)。
|
|
15
|
+
* - 当前轮显式出发地永远赢(explicit origin > home city > missing)
|
|
16
|
+
* - homeCity 变更必须伴新 evidence(P0 与 weights/hard 同向);单条 evidence
|
|
17
|
+
* 可作为省略 binding 的兼容形态,多条 evidence 必须显式绑定
|
|
18
|
+
* - 该契约不触碰算术/求解——只读字段,不进 feasibility/evaluate 路径
|
|
19
|
+
*
|
|
20
|
+
* 持久化语义(issue #338):home-city 是 typed self-contained preference
|
|
21
|
+
* 记录 `{ value, evidence, updated_at }`,与全局 evidence 列表解耦——
|
|
22
|
+
* 一次不相关的动机补丁不会改写它的「证据指针」「更新时间」「当前值」。
|
|
23
|
+
* `updated_at` 由调用方在事件边界注入(账本事件行的 ts),mergeProfile
|
|
24
|
+
* 自身不在 fold 路径里再读时钟——保证 rebuildProjections() 与直读投影
|
|
25
|
+
* 永不分叉。
|
|
12
26
|
*/
|
|
13
27
|
|
|
28
|
+
export interface HomeCityPreference {
|
|
29
|
+
/** string=当前默认城市,null=已显式清除(审计一行留痕) */
|
|
30
|
+
value: string | null
|
|
31
|
+
/** 锁定到本次 home-city 操作的用户原话指针(append-only evidence 池里的那条) */
|
|
32
|
+
evidence: string
|
|
33
|
+
/** 账本事件行的 ts(非折叠期再读时钟) */
|
|
34
|
+
updated_at: string
|
|
35
|
+
}
|
|
36
|
+
|
|
14
37
|
export interface ProfilePatch {
|
|
15
38
|
weights?: Record<string, number>
|
|
16
39
|
/** 新增依据(用户原话或工具证据);只增不改不删 */
|
|
17
40
|
evidence?: string[]
|
|
18
41
|
hard?: Record<string, unknown>
|
|
42
|
+
/**
|
|
43
|
+
* 持久化用户常驻城市/默认出发地(issue #338)。
|
|
44
|
+
* - string = 设置默认城市(用户原话指明常驻)
|
|
45
|
+
* - null = 显式清除(审计一行留痕,用于遗忘/迁居)
|
|
46
|
+
* - undefined = 字段未传,保持当前值不动
|
|
47
|
+
* 改值须伴至少一条新 evidence(同 weights/hard 的 P0 守卫);恰好一条非空
|
|
48
|
+
* evidence 时可省略 homeCityEvidence,多条 evidence 必须显式绑定。
|
|
49
|
+
* 进入合并前会 trim;trim 后空白 → 视为「未传」,不触发变更、不落事件。
|
|
50
|
+
*/
|
|
51
|
+
homeCity?: string | null
|
|
52
|
+
/**
|
|
53
|
+
* homeCity 在本次 profile.evidence 中的 exact 用户原话绑定。
|
|
54
|
+
* 显式传入时必须非空、逐字出现在 evidence 且相对当前画像为新值;
|
|
55
|
+
* 未传时仅允许 evidence 恰好一条且非空,由该条提供 unambiguous binding;
|
|
56
|
+
* 唯一例外是同 value/clear state + 同 typed evidence 的重复调用(幂等)。
|
|
57
|
+
*/
|
|
58
|
+
homeCityEvidence?: string
|
|
19
59
|
}
|
|
20
60
|
|
|
21
61
|
export interface MergedProfile {
|
|
22
62
|
weights: Record<string, number>
|
|
23
63
|
evidence: string[]
|
|
24
64
|
hard: Record<string, unknown>
|
|
65
|
+
/** 当前持久默认城市。undefined 从未保存;null 显式清除;string 当前默认。 */
|
|
66
|
+
homeCity?: string | null
|
|
67
|
+
/**
|
|
68
|
+
* home-city 自含记录(issue #338):value + 锁定证据 + 更新时间三件套,
|
|
69
|
+
* 与全局 evidence 列表解耦。缺少或不满足完整 typed contract 的画像按
|
|
70
|
+
* missing 处理;raw homeCity 不会成为默认值或证据来源。
|
|
71
|
+
*/
|
|
72
|
+
homeCityPreference?: HomeCityPreference
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const ISO_LIKE_TIMESTAMP = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d{1,3})?(Z|[+-]\d{2}:\d{2})$/
|
|
76
|
+
|
|
77
|
+
function isNonBlankString(value: unknown): value is string {
|
|
78
|
+
return typeof value === 'string' && value.trim().length > 0
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isValidIsoLikeTimestamp(value: unknown): value is string {
|
|
82
|
+
if (typeof value !== 'string') return false
|
|
83
|
+
const match = ISO_LIKE_TIMESTAMP.exec(value)
|
|
84
|
+
if (!match) return false
|
|
85
|
+
const year = Number(match[1])
|
|
86
|
+
const month = Number(match[2])
|
|
87
|
+
const day = Number(match[3])
|
|
88
|
+
const hour = Number(match[4])
|
|
89
|
+
const minute = Number(match[5])
|
|
90
|
+
const second = Number(match[6])
|
|
91
|
+
const zone = match[7]!
|
|
92
|
+
const daysInMonth = month === 2
|
|
93
|
+
? (year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28)
|
|
94
|
+
: [31, 0, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1]
|
|
95
|
+
if (!daysInMonth || day < 1 || day > daysInMonth) return false
|
|
96
|
+
// ISO permits 24:00:00 as the end-of-day spelling; no other 24:xx value.
|
|
97
|
+
if (hour > 23 && !(hour === 24 && minute === 0 && second === 0)) return false
|
|
98
|
+
if (minute > 59 || second > 59) return false
|
|
99
|
+
if (zone !== 'Z' && (Number(zone.slice(1, 3)) > 23 || Number(zone.slice(4, 6)) > 59)) return false
|
|
100
|
+
return Number.isFinite(Date.parse(value))
|
|
25
101
|
}
|
|
26
102
|
|
|
27
|
-
/**
|
|
103
|
+
/**
|
|
104
|
+
* Typed home-city preference 的唯一有效性判定。
|
|
105
|
+
* 不读取、不修写 raw profile;raw homeCity 若存在只作镜像一致性校验。
|
|
106
|
+
*/
|
|
107
|
+
export function isValidHomeCityPreference(profile: {
|
|
108
|
+
homeCity?: unknown
|
|
109
|
+
evidence?: unknown
|
|
110
|
+
homeCityPreference?: unknown
|
|
111
|
+
} | null): boolean {
|
|
112
|
+
const preference = profile?.homeCityPreference
|
|
113
|
+
if (!preference || typeof preference !== 'object' || Array.isArray(preference)) return false
|
|
114
|
+
const candidate = preference as Partial<HomeCityPreference>
|
|
115
|
+
if (!(candidate.value === null || isNonBlankString(candidate.value))) return false
|
|
116
|
+
if (!isNonBlankString(candidate.evidence) || !isValidIsoLikeTimestamp(candidate.updated_at)) return false
|
|
117
|
+
if (!profile || !Array.isArray(profile.evidence) || !profile.evidence.some(entry => entry === candidate.evidence)) return false
|
|
118
|
+
if (profile && Object.prototype.hasOwnProperty.call(profile, 'homeCity') && profile.homeCity !== candidate.value) return false
|
|
119
|
+
return true
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 合并守门。纯函数;不读时钟——`now` 由调用方从事件边界(appendMotivationPatch
|
|
124
|
+
* 写时 ISO 或 foldEvent 重放时 row.ts)注入,保证写路径与重建路径时间戳一致。
|
|
125
|
+
*
|
|
126
|
+
* 触发 homeCity 操作的同时,会原地维护 typed homeCityPreference:
|
|
127
|
+
* - valid new binding → 新 `{value, evidence: <homeCityEvidence exact string>, updated_at: now}`
|
|
128
|
+
* - unchanged → 沿用 base 的 homeCityPreference(若有);缺失 typed 字段保持缺失
|
|
129
|
+
* - missing/mismatched/stale binding → 返回 null,由账本跳过整次事件
|
|
130
|
+
*/
|
|
28
131
|
export function mergeProfile(
|
|
29
|
-
current: {
|
|
132
|
+
current: {
|
|
133
|
+
weights?: Record<string, number>
|
|
134
|
+
evidence?: string[]
|
|
135
|
+
hard?: Record<string, unknown>
|
|
136
|
+
homeCity?: string | null
|
|
137
|
+
homeCityPreference?: HomeCityPreference
|
|
138
|
+
} | null,
|
|
30
139
|
patch: ProfilePatch | null,
|
|
140
|
+
now: string,
|
|
31
141
|
): MergedProfile | null {
|
|
32
142
|
if (!patch) return null
|
|
33
143
|
const base = {
|
|
34
144
|
weights: (current?.weights ?? {}) as Record<string, number>,
|
|
35
145
|
evidence: (current?.evidence ?? []) as string[],
|
|
36
146
|
hard: (current?.hard ?? {}) as Record<string, unknown>,
|
|
147
|
+
homeCity: (current?.homeCity === undefined ? undefined : current.homeCity) as string | null | undefined,
|
|
148
|
+
homeCityPreference: current?.homeCityPreference as HomeCityPreference | undefined,
|
|
37
149
|
} // 首次保存:无档案即空档案
|
|
38
150
|
const newEvidence = (patch.evidence ?? []).filter(Boolean).filter(e => !base.evidence.includes(e))
|
|
39
151
|
const patchWeights = patch.weights
|
|
@@ -41,7 +153,46 @@ export function mergeProfile(
|
|
|
41
153
|
JSON.stringify(patchWeights) !== JSON.stringify(base.weights)
|
|
42
154
|
const hardChanged = !!patch.hard && Object.keys(patch.hard).length > 0 &&
|
|
43
155
|
JSON.stringify(patch.hard) !== JSON.stringify(base.hard)
|
|
44
|
-
|
|
156
|
+
// homeCity 变更判定:
|
|
157
|
+
// patch.homeCity === undefined → 字段未传,不视为变更
|
|
158
|
+
// string 先 trim;trim 后空白 → 视作「未传」(避免把空白值当清除语义或无意义值写入)
|
|
159
|
+
// trim 后与 base.homeCity 仍不等 → 视为变更(包括 string↔null、null↔undefined 等差异)
|
|
160
|
+
let patchedHomeCityInput: string | null | undefined = patch.homeCity
|
|
161
|
+
if (typeof patchedHomeCityInput === 'string') {
|
|
162
|
+
const trimmed = patchedHomeCityInput.trim()
|
|
163
|
+
patchedHomeCityInput = trimmed.length === 0 ? undefined : trimmed
|
|
164
|
+
}
|
|
165
|
+
const homeCityTouched = patchedHomeCityInput !== undefined
|
|
166
|
+
const homeCityChanged = homeCityTouched && patchedHomeCityInput !== base.homeCity
|
|
167
|
+
|
|
168
|
+
if (!homeCityTouched && patch.homeCityEvidence !== undefined) return null
|
|
169
|
+
|
|
170
|
+
// homeCity 的语义绑定必须在 profile patch 边界明确给出,不从自然语言或
|
|
171
|
+
// evidence 数组位置推断。唯一兼容例外:本次只有一条非空 evidence 时,
|
|
172
|
+
// 该条本身就是 unambiguous binding;多条 evidence 仍必须显式命名。
|
|
173
|
+
let homeCityEvidence: string | undefined
|
|
174
|
+
let homeCityEvidenceIsNew = false
|
|
175
|
+
if (homeCityTouched) {
|
|
176
|
+
const evidenceEntries = patch.evidence ?? []
|
|
177
|
+
const candidate = patch.homeCityEvidence !== undefined
|
|
178
|
+
? patch.homeCityEvidence
|
|
179
|
+
: evidenceEntries.length === 1 && isNonBlankString(evidenceEntries[0])
|
|
180
|
+
? evidenceEntries[0]
|
|
181
|
+
: undefined
|
|
182
|
+
const appearsInCall = isNonBlankString(candidate) && evidenceEntries.some(e => e === candidate)
|
|
183
|
+
homeCityEvidenceIsNew = appearsInCall && !base.evidence.includes(candidate)
|
|
184
|
+
const exactDuplicate = appearsInCall && !homeCityEvidenceIsNew && !homeCityChanged
|
|
185
|
+
&& base.homeCityPreference?.value === patchedHomeCityInput
|
|
186
|
+
&& base.homeCityPreference?.evidence === candidate
|
|
187
|
+
if (!appearsInCall || (!homeCityEvidenceIsNew && !exactDuplicate)) return null
|
|
188
|
+
homeCityEvidence = candidate
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// A same-city/clear operation with a new bound quote is a real preference
|
|
192
|
+
// update even though the value itself did not change. The exact duplicate
|
|
193
|
+
// above remains a no-op.
|
|
194
|
+
const homeCityPreferenceChanged = homeCityTouched && homeCityEvidenceIsNew
|
|
195
|
+
if (newEvidence.length === 0 && !weightsChanged && !hardChanged && !homeCityChanged && !homeCityPreferenceChanged) return null
|
|
45
196
|
|
|
46
197
|
// 权重变更必须伴至少一条新 evidence(P0:改画像要有依据),否则拒该部分
|
|
47
198
|
let weights = { ...base.weights }
|
|
@@ -51,9 +202,82 @@ export function mergeProfile(
|
|
|
51
202
|
if (sum > 0) for (const k of Object.keys(weights)) weights[k] = Math.round((weights[k]! / sum) * 100) / 100
|
|
52
203
|
}
|
|
53
204
|
|
|
205
|
+
// homeCity 操作只接受上面验证过的 exact homeCityEvidence 绑定;无关 evidence
|
|
206
|
+
// 可以继续追加到全局池,但绝不会成为 homeCityPreference 的语义指针。
|
|
207
|
+
let homeCity: string | null | undefined = base.homeCity
|
|
208
|
+
let homeCityPreference: HomeCityPreference | undefined = base.homeCityPreference
|
|
209
|
+
if (homeCityPreferenceChanged) {
|
|
210
|
+
homeCity = patchedHomeCityInput ?? null
|
|
211
|
+
if (homeCityEvidence !== undefined) {
|
|
212
|
+
homeCityPreference = { value: homeCity, evidence: homeCityEvidence, updated_at: now }
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
54
216
|
return {
|
|
55
217
|
weights,
|
|
56
218
|
evidence: [...base.evidence, ...newEvidence],
|
|
57
219
|
hard: hardChanged ? { ...base.hard, ...(patch.hard ?? {}) } : { ...base.hard },
|
|
220
|
+
homeCity,
|
|
221
|
+
homeCityPreference,
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* 默认出发地解析契约(issue #338)。
|
|
227
|
+
*
|
|
228
|
+
* 纯函数,不读文件、不查 IP/浏览器位置/语言/时区/历史行程;不构造新 evaluate/solve 路径。
|
|
229
|
+
* 调用方负责传入当轮上下文(currentTripOrigin)与持久画像(profile)。
|
|
230
|
+
*
|
|
231
|
+
* 三态判决:
|
|
232
|
+
* - explicit 当轮显式出发地(用户当轮输入);永远赢,不被持久默认覆盖
|
|
233
|
+
* - home_default 持久化 homeCity;仅在当轮未指定时填补缺省(用于软排序/兜底)
|
|
234
|
+
* - missing 二者皆空或显式已清除;调用方须向用户澄清而非自行猜测
|
|
235
|
+
*
|
|
236
|
+
* 该契约的硬约束:
|
|
237
|
+
* - 不删除候选、不改变可行性判定、不进 evaluate/solve 路径
|
|
238
|
+
* - 「只进软排序/补缺」语义由调用方在排序/回退层使用本结果
|
|
239
|
+
*
|
|
240
|
+
* 证据来源:只接受 self-contained typed preference(issue #338)的完整合同。
|
|
241
|
+
* 缺字段、镜像不一致或字段 malformed 的画像一律 missing,不从 raw homeCity
|
|
242
|
+
* 或全局 evidence 推断默认值/原话。
|
|
243
|
+
*/
|
|
244
|
+
export interface DefaultOriginResolution {
|
|
245
|
+
/** 解析后的出发地;null = 调用方应澄清 */
|
|
246
|
+
origin: string | null
|
|
247
|
+
/** 取值来源 */
|
|
248
|
+
source: 'explicit' | 'home_default' | 'missing'
|
|
249
|
+
/** 当前持久 homeCity(原值,便于 UI/日志/审计展示) */
|
|
250
|
+
homeCity: string | null | undefined
|
|
251
|
+
/** 锁定到本次 home-city 操作的用户原话指针;仅 valid home_default 填 */
|
|
252
|
+
homeCityEvidence?: string[]
|
|
253
|
+
/** 是否来自完整 typed preference 路径(便于 audit/UI 区分) */
|
|
254
|
+
homeCityPreferenceSource: 'typed' | 'none'
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function resolveDefaultOrigin(
|
|
258
|
+
currentTripOrigin: string | null | undefined,
|
|
259
|
+
profile: {
|
|
260
|
+
homeCity?: string | null
|
|
261
|
+
evidence?: string[]
|
|
262
|
+
homeCityPreference?: HomeCityPreference
|
|
263
|
+
} | null,
|
|
264
|
+
): DefaultOriginResolution {
|
|
265
|
+
const trimmed = typeof currentTripOrigin === 'string' ? currentTripOrigin.trim() : ''
|
|
266
|
+
const validTypedPreference = isValidHomeCityPreference(profile)
|
|
267
|
+
const preference = validTypedPreference ? profile?.homeCityPreference as HomeCityPreference : undefined
|
|
268
|
+
const typedValue = preference?.value
|
|
269
|
+
const homeCity = validTypedPreference ? typedValue : undefined
|
|
270
|
+
if (trimmed) {
|
|
271
|
+
return { origin: trimmed, source: 'explicit', homeCity, homeCityPreferenceSource: 'none' }
|
|
272
|
+
}
|
|
273
|
+
if (validTypedPreference && typeof typedValue === 'string') {
|
|
274
|
+
return {
|
|
275
|
+
origin: typedValue.trim(),
|
|
276
|
+
source: 'home_default',
|
|
277
|
+
homeCity,
|
|
278
|
+
homeCityEvidence: [preference!.evidence],
|
|
279
|
+
homeCityPreferenceSource: 'typed',
|
|
280
|
+
}
|
|
58
281
|
}
|
|
282
|
+
return { origin: null, source: 'missing', homeCity, homeCityPreferenceSource: 'none' }
|
|
59
283
|
}
|
package/ts/src/mock-llm.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { InterviewQuestion, TravelerProfile, TripState, Turn, CalendarState
|
|
|
9
9
|
import type { JourneySpecTS } from './unified.ts'
|
|
10
10
|
import type { TravelSlotExtraction } from './travel-slots.ts'
|
|
11
11
|
import { parseFlightPackToSpec } from './unified.ts'
|
|
12
|
+
import { mergeProfileWorkWindow } from './flight-pack-adapter.ts'
|
|
12
13
|
|
|
13
14
|
interface ScriptStep {
|
|
14
15
|
/** 命中条件:用户消息包含此关键词(第一条匹配生效) */
|
|
@@ -79,14 +80,9 @@ export function createMockLlm(flightPackPath?: string, slotScript: SlotScriptSte
|
|
|
79
80
|
const { readFile } = await import('node:fs/promises')
|
|
80
81
|
const pack = JSON.parse(await readFile(flightPackPath, 'utf-8'))
|
|
81
82
|
const spec = parseFlightPackToSpec(pack)
|
|
82
|
-
spec.workWindow
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
endMin: state.profile.workWindow.endMin,
|
|
86
|
-
workdays: state.profile.workWindow.workdays,
|
|
87
|
-
}
|
|
88
|
-
spec.budgetCny = 9000
|
|
89
|
-
return spec
|
|
83
|
+
const merged = mergeProfileWorkWindow(spec, state.profile.workWindow)
|
|
84
|
+
merged.budgetCny = 9000
|
|
85
|
+
return merged
|
|
90
86
|
},
|
|
91
87
|
async extractSlots(history: Turn[]): Promise<TravelSlotExtraction | null> {
|
|
92
88
|
const last = history[history.length - 1]?.text ?? ''
|
package/ts/src/model.ts
CHANGED
|
@@ -19,6 +19,16 @@ export interface TransferMode {
|
|
|
19
19
|
mode: string
|
|
20
20
|
minutes: number
|
|
21
21
|
priceCny: number
|
|
22
|
+
/**
|
|
23
|
+
* Per-direction overrides supplied by the ground-transfer capability when a
|
|
24
|
+
* driving route fact successfully resolves for outbound (A→B) and/or return
|
|
25
|
+
* (B→A). `minutesOut` feeds the outbound arrival stay minute;
|
|
26
|
+
* `minutesRet` feeds the return departure-from-stay minute. When absent
|
|
27
|
+
* the solver falls back to the symmetric `minutes` field, preserving
|
|
28
|
+
* byte-for-byte behavior for static and symmetric inputs.
|
|
29
|
+
*/
|
|
30
|
+
minutesOut?: number
|
|
31
|
+
minutesRet?: number
|
|
22
32
|
}
|
|
23
33
|
|
|
24
34
|
export interface Service {
|
|
@@ -26,6 +36,14 @@ export interface Service {
|
|
|
26
36
|
depMin: number
|
|
27
37
|
arrMin: number
|
|
28
38
|
priceCny: number
|
|
39
|
+
/**
|
|
40
|
+
* Issue #343 v2 路径填充(可选):service 出发/到达对应的真实 UTC instant(epoch ms)。
|
|
41
|
+
* 同一 service 在 arrMin - depMin ≤ 真实飞行时长 跨日 / DST 切换场景下需要这两个字段
|
|
42
|
+
* 才能算出正确的 (arrUtcMs - depUtcMs) / 60000。v1 数值路径不填,由 evaluateChoice/Segment 走
|
|
43
|
+
* 旧 `(arrMin - depMin) - tzOffsetMin` 公式,字节不变。
|
|
44
|
+
*/
|
|
45
|
+
depUtcMs?: number
|
|
46
|
+
arrUtcMs?: number
|
|
29
47
|
}
|
|
30
48
|
|
|
31
49
|
export interface HubAccess {
|
|
@@ -46,6 +64,8 @@ export interface MotivationProfile {
|
|
|
46
64
|
export interface Candidate {
|
|
47
65
|
id: string
|
|
48
66
|
name: string
|
|
67
|
+
/** 可选的明确出行日;未提供时保持旧版无日期可行性计算语义 */
|
|
68
|
+
date?: string
|
|
49
69
|
hub: string
|
|
50
70
|
bufferOutMin: number
|
|
51
71
|
bufferRetMin: number
|
|
@@ -147,6 +167,7 @@ export function parseCandidate(d: Record<string, unknown>): Candidate {
|
|
|
147
167
|
return {
|
|
148
168
|
id: String(d['id']),
|
|
149
169
|
name: String(d['name']),
|
|
170
|
+
date: typeof d['date'] === 'string' ? d['date'] : undefined,
|
|
150
171
|
hub: String(d['hub']),
|
|
151
172
|
bufferOutMin: Number(d['buffer_out_min'] ?? 60),
|
|
152
173
|
bufferRetMin: Number(d['buffer_ret_min'] ?? 60),
|
|
@@ -161,11 +182,76 @@ export function parseCandidate(d: Record<string, unknown>): Candidate {
|
|
|
161
182
|
}
|
|
162
183
|
}
|
|
163
184
|
|
|
185
|
+
/** Issue #343 d2d canonical arithmetic。 */
|
|
186
|
+
export class NonpositiveDurationError extends Error {
|
|
187
|
+
/** 'arr_not_after_dep' | 'legacy_negative_offset' */
|
|
188
|
+
readonly kind: 'arr_not_after_dep' | 'legacy_negative_offset'
|
|
189
|
+
readonly code = 'nonpositive_duration'
|
|
190
|
+
constructor(kind: 'arr_not_after_dep' | 'legacy_negative_offset', message: string) {
|
|
191
|
+
super(message)
|
|
192
|
+
this.kind = kind
|
|
193
|
+
this.name = 'NonpositiveDurationError'
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* doorToDoorFromMove 所需的最小 move 字段。本模型层不耦合 unified.ts 的 MoveSpecTS(避免循环)。
|
|
199
|
+
* unified.ts evaluateOptionMove 必须用这个 helper,而不是再写一份 (arrUtcMs - depUtcMs)/60000。
|
|
200
|
+
*/
|
|
201
|
+
export interface D2DMoveView {
|
|
202
|
+
bufferMin: number
|
|
203
|
+
originTransferMin: number
|
|
204
|
+
destTransferMin: number
|
|
205
|
+
/** 仅 v1 路径用;v2 路径无需。 */
|
|
206
|
+
tzOffsetMin?: number
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export interface DoorToDoorParts {
|
|
210
|
+
/** 真实飞行时长(分);v2 = (arrUtcMs - depUtcMs)/60000,v1 = (arrMin - depMin) - tzOffsetMin */
|
|
211
|
+
trueFlightMin: number
|
|
212
|
+
/** 门到门(分) = buffer + originTransfer + trueFlight + destTransfer */
|
|
213
|
+
doorToDoorMin: number
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* 单段门到门时长(分)。v2 路径用真实 UTC instant(跨日 / 反向日界线 / DST 切换正确),
|
|
218
|
+
* 且强制 `arrUtcMs > depUtcMs` — parse 边界也会再做一次防御性 throw。
|
|
219
|
+
* v1 路径用墙上分钟差 - tzOffsetMin(原数值口径,字节不变,即使结果为负也照旧返回)。
|
|
220
|
+
*
|
|
221
|
+
* v2:arrUtcMs 必须严格大于 depUtcMs;否则抛 NonpositiveDurationError(arr_not_after_dep)。
|
|
222
|
+
* v1:沿用 legacy 公式,字节不变;不抛错。
|
|
223
|
+
*
|
|
224
|
+
* 调用方(unified.ts evaluateOptionMove)只负责拿到 doorToDoorMin 并包装 LegReport。
|
|
225
|
+
*/
|
|
226
|
+
export function doorToDoorFromMove(svc: Service, mv: D2DMoveView): DoorToDoorParts {
|
|
227
|
+
if (typeof svc.depUtcMs === 'number' && typeof svc.arrUtcMs === 'number') {
|
|
228
|
+
if (svc.arrUtcMs <= svc.depUtcMs) {
|
|
229
|
+
throw new NonpositiveDurationError(
|
|
230
|
+
'arr_not_after_dep',
|
|
231
|
+
`service ${svc.id}: arrUtcMs (${svc.arrUtcMs}) must be strictly greater than depUtcMs (${svc.depUtcMs})`,
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
const trueFlightMin = (svc.arrUtcMs - svc.depUtcMs) / 60_000
|
|
235
|
+
const doorToDoorMin = mv.bufferMin + mv.originTransferMin + trueFlightMin + mv.destTransferMin
|
|
236
|
+
return { trueFlightMin, doorToDoorMin }
|
|
237
|
+
}
|
|
238
|
+
// v1 legacy 字节不变:不校验正负,沿用原公式。
|
|
239
|
+
const trueFlightMin = (svc.arrMin - svc.depMin) - (mv.tzOffsetMin ?? 0)
|
|
240
|
+
const doorToDoorMin = mv.bufferMin + mv.originTransferMin + trueFlightMin + mv.destTransferMin
|
|
241
|
+
return { trueFlightMin, doorToDoorMin }
|
|
242
|
+
}
|
|
243
|
+
|
|
164
244
|
export function evaluateChoice(cand: Candidate, req: TravelRequest, ch: Choice): TrueCost {
|
|
165
245
|
/** 门到门全成本核算(与 Python evaluate_choice 完全一致)。 */
|
|
166
246
|
const access = req.homeHubAccess[cand.hub]
|
|
247
|
+
const outTransferMin = typeof ch.outTransfer.minutesOut === 'number'
|
|
248
|
+
? ch.outTransfer.minutesOut
|
|
249
|
+
: ch.outTransfer.minutes
|
|
250
|
+
const retTransferMin = typeof ch.retTransfer.minutesRet === 'number'
|
|
251
|
+
? ch.retTransfer.minutesRet
|
|
252
|
+
: ch.retTransfer.minutes
|
|
167
253
|
const wake = ch.outService.depMin - cand.bufferOutMin - access.toHubMin
|
|
168
|
-
const arriveStay = ch.outService.arrMin +
|
|
254
|
+
const arriveStay = ch.outService.arrMin + outTransferMin
|
|
169
255
|
const d2dOut = arriveStay - wake
|
|
170
256
|
|
|
171
257
|
let energy = 100
|
|
@@ -179,7 +265,7 @@ export function evaluateChoice(cand: Candidate, req: TravelRequest, ch: Choice):
|
|
|
179
265
|
|
|
180
266
|
const day1Raw = Math.max(0, DAY_END_MIN - arriveStay) / 60
|
|
181
267
|
const day1 = day1Raw * (0.5 + energy / 200)
|
|
182
|
-
const leaveStayRet = ch.retService.depMin - cand.bufferRetMin -
|
|
268
|
+
const leaveStayRet = ch.retService.depMin - cand.bufferRetMin - retTransferMin
|
|
183
269
|
const day2 = Math.max(0, leaveStayRet - DAY2_START_MIN) / 60 * DAY2_QUALITY
|
|
184
270
|
const midDays = Math.max(0, ch.days - 2)
|
|
185
271
|
const usable = day1 + day2 + midDays * 8.0 * DAY2_QUALITY
|