@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/index.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - gotry_wish_pool_add 「下一次出发」清单(憧憬不被拒绝)
|
|
8
8
|
*
|
|
9
9
|
* 插件形态遵循 dsh 约定(name/inject/Config/apply + ctx.tools.register(defineTool(...))),
|
|
10
|
-
* 对齐 @deepseek-ai/dsh-tools@0.1.
|
|
10
|
+
* 对齐 @deepseek-ai/dsh-tools@0.1.5-rc.1 的契约:
|
|
11
11
|
* render 位于 output 对象内,参数属性是 ValueSchemaSpec(支持 type:'json')。
|
|
12
12
|
*
|
|
13
13
|
* @module @gotry/plugin
|
|
@@ -17,7 +17,7 @@ import { join } from 'node:path'
|
|
|
17
17
|
|
|
18
18
|
import type { Context } from '@deepseek-ai/cordis'
|
|
19
19
|
import z from '@deepseek-ai/schemastery'
|
|
20
|
-
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
20
|
+
import { defineTool, type ToolResult, type ToolRunContext } from '@deepseek-ai/dsh-tools'
|
|
21
21
|
import { ensureStateDir, recordLatency } from './bridge.ts'
|
|
22
22
|
import { segmentsFromCandidate, solveChoiceSegment } from './unified.ts'
|
|
23
23
|
import { checkConnectivity } from '../scripts/skeleton-check.ts'
|
|
@@ -28,20 +28,23 @@ import { projectUtility } from './memory-utility.ts'
|
|
|
28
28
|
import { pickNudgeWish, type WishPoolEntry } from './wish-pool.ts'
|
|
29
29
|
import { resolveTimelineDate } from './travel-timeline.ts'
|
|
30
30
|
import { ensureLedger, readCompanionsWithFallback, readMotivationWithFallback, readTripsWithFallback, readWishPoolWithFallback } from './state-ledger.ts'
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
31
|
+
import { isValidHomeCityPreference, resolveDefaultOrigin, type MergedProfile, type ProfilePatch } from './memory-capture.ts'
|
|
32
|
+
import { applyPlanningWindow } from './loop.ts'
|
|
33
|
+
import { buildTimeAnchor, type PlanningWindow } from './time-anchor.ts'
|
|
34
|
+
import { evaluateHotelStayDates } from './hotel-date-gate.ts'
|
|
33
35
|
import { wmoLabel } from '../capabilities/weather.ts'
|
|
34
36
|
// D-23 收编(issue #115):anything/web/github/video/agent_reach/session_login 六渠道改经
|
|
35
37
|
// 效应注册表(退避/熔断/declined 面统一);能力层直接 import 仅留类型位(reach/reachStatus)
|
|
36
38
|
import { reach, reachStatus } from '../capabilities/agent-reach.ts'
|
|
37
|
-
import { runDoctorChecks, renderDoctorReportMd } from '../capabilities/doctor.ts'
|
|
39
|
+
import { runDoctorChecks, renderDoctorReportMd, runDoctorRepair, createRepairApprovalGate, type DoctorRepairOptions } from '../capabilities/doctor.ts'
|
|
38
40
|
import { EXTENSION_STORE_URL } from '../capabilities/session/extension-bridge.ts'
|
|
39
|
-
import { createConsentGate, approvalFromContext } from '../capabilities/session-consent.ts'
|
|
41
|
+
import { createConsentGate, approvalFromContext, resolveSessionSearchKind } from '../capabilities/session-consent.ts'
|
|
40
42
|
import { installModelOverride } from '../capabilities/model-override.ts'
|
|
41
43
|
import { listArtifacts, readArtifact } from '../capabilities/artifacts.ts'
|
|
42
44
|
import { interpretEffect, declinedObservation } from '../capabilities/effect.ts'
|
|
43
45
|
import { appendFacts, loadFactRegistry } from '../capabilities/fact-log.ts'
|
|
44
|
-
import { factsFromFlyai, factsFromHotel, factsFromSession } from './bookable-facts.ts'
|
|
46
|
+
import { factsFromFlyai, factsFromHotel, factsFromSession, factsFromSessionTrain } from './bookable-facts.ts'
|
|
47
|
+
import { hasRecognizedAvailableSeat } from '../capabilities/session/adapters/rail-12306.ts'
|
|
45
48
|
import { gateArtifact, type AirlineAirportMap } from './artifact-gate.ts'
|
|
46
49
|
import { installTurnDeadline, listTurnHandoffTickets } from './turn-deadline.ts'
|
|
47
50
|
import { noteChannelVerdict, recordChannelEvent, readLatestChannelEvents } from '../capabilities/channel-health.ts'
|
|
@@ -49,6 +52,15 @@ import { routingAdvice, renderRoutingCard, toolRoutingHeadline, type ChannelInte
|
|
|
49
52
|
import { registerBenchmarkEnvironmentBridge, type BenchmarkSubprocessService } from './benchmark-environment-bridge.ts'
|
|
50
53
|
import { installBenchmarkToolIsolation } from './benchmark-tool-isolation.ts'
|
|
51
54
|
import { installBenchmarkAgentConformance } from './benchmark-agent-conformance.ts'
|
|
55
|
+
import { installSubagentJobIdGuard } from './subagent-job-id-guard.ts'
|
|
56
|
+
import {
|
|
57
|
+
createGroundTransferResolver,
|
|
58
|
+
createPublicMapDrivingRouteProvider,
|
|
59
|
+
exposeGroundTransferEvidence,
|
|
60
|
+
resolveGroundTransferPayload,
|
|
61
|
+
type GroundTransferProvider,
|
|
62
|
+
type GroundTransferResolution,
|
|
63
|
+
} from '../capabilities/ground-transfer.ts'
|
|
52
64
|
|
|
53
65
|
/** 航司→机场映射表(issue #46 冲突检测面;data/airline-airports.json,as_of 快照) */
|
|
54
66
|
let airlineAirportMapCache: AirlineAirportMap | null = null
|
|
@@ -71,6 +83,13 @@ export const name = 'gotry-tools'
|
|
|
71
83
|
// depend on a service that is only needed for explicit benchmark opt-in.
|
|
72
84
|
export const inject = ['tools', 'systemPrompt']
|
|
73
85
|
|
|
86
|
+
function sessionCwd(exec: unknown): string | undefined {
|
|
87
|
+
const candidate = exec as { agent?: { session?: { header?: { cwd?: unknown } } } } | null
|
|
88
|
+
return typeof candidate?.agent?.session?.header?.cwd === 'string'
|
|
89
|
+
? candidate.agent.session.header.cwd
|
|
90
|
+
: undefined
|
|
91
|
+
}
|
|
92
|
+
|
|
74
93
|
export interface Config {
|
|
75
94
|
/** 状态根目录(动机画像、wish pool、延迟日志) */
|
|
76
95
|
stateRoot: string
|
|
@@ -96,12 +115,31 @@ interface FeasibilityResult {
|
|
|
96
115
|
answer_md?: string
|
|
97
116
|
recommended?: string | null
|
|
98
117
|
verdicts?: Array<Record<string, unknown>>
|
|
118
|
+
ground_transfer?: GroundTransferResolution
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
interface FeasibilityValidationFailure extends Record<string, Json> {
|
|
122
|
+
ok: false
|
|
123
|
+
code: 'invalid_planning_context' | 'planning_window_rejected'
|
|
124
|
+
summary: string
|
|
125
|
+
rejected: Array<{ segmentId: string; optionId: string; date: string }>
|
|
99
126
|
}
|
|
100
127
|
|
|
101
128
|
interface MotivationProfileInput {
|
|
102
129
|
weights?: Record<string, number>
|
|
103
130
|
evidence?: string[]
|
|
104
131
|
hard?: Record<string, unknown>
|
|
132
|
+
/**
|
|
133
|
+
* 持久化用户常驻城市/默认出发地(issue #338)。
|
|
134
|
+
* - string = 设置默认城市(用户原话指明常驻)
|
|
135
|
+
* - null = 显式清除(审计一行留痕,用于遗忘/迁居)
|
|
136
|
+
* - undefined = 字段未传,保持当前值不动
|
|
137
|
+
* 改值须伴新 evidence(同 weights/hard 的 P0 守卫);evidence 恰好一条且非空时
|
|
138
|
+
* 可省略 homeCityEvidence,多条 evidence 必须显式绑定。
|
|
139
|
+
*/
|
|
140
|
+
homeCity?: string | null
|
|
141
|
+
/** homeCity 对应的本次用户原话;仅与 homeCity 一起传入。 */
|
|
142
|
+
homeCityEvidence?: string
|
|
105
143
|
}
|
|
106
144
|
|
|
107
145
|
interface WishPoolEntryInput {
|
|
@@ -130,6 +168,22 @@ function renderMotivationBrief(stateRoot: string): string {
|
|
|
130
168
|
if (weights.length) lines.push(`- 动机权重: ${weights.map(([k, v]) => `${k}=${v}`).join(', ')}(证据 ${p.evidence?.length ?? 0} 条)`)
|
|
131
169
|
const hard = Object.entries(p.hard ?? {})
|
|
132
170
|
if (hard.length) lines.push(`- 硬约束: ${hard.map(([k, v]) => `${k}=${String(v)}`).join(', ')}`)
|
|
171
|
+
// 持久默认城市(issue #338):生产 brief 复用纯 resolver 的 precedence
|
|
172
|
+
// 契约;这里仍只负责读回渲染,不把 resolver 接入 feasibility/evaluate。
|
|
173
|
+
const profile = p as MergedProfile
|
|
174
|
+
const homeResolution = resolveDefaultOrigin(undefined, profile)
|
|
175
|
+
const homePref = profile.homeCityPreference
|
|
176
|
+
const typedPreferenceIsValid = isValidHomeCityPreference(profile)
|
|
177
|
+
const hasHomeCityData = Object.prototype.hasOwnProperty.call(profile, 'homeCity')
|
|
178
|
+
|| Object.prototype.hasOwnProperty.call(profile, 'homeCityPreference')
|
|
179
|
+
if (homeResolution.source === 'home_default' && homeResolution.origin && typedPreferenceIsValid && homePref) {
|
|
180
|
+
const evidence = homeResolution.homeCityEvidence?.[0] ?? ''
|
|
181
|
+
lines.push(`- 常驻城市: ${homeResolution.origin}(原话证据:${evidence};记录于 ${homePref.updated_at})——本轮未指明出发地时作为软默认;本轮显式出发地优先;不进可行性硬约束,不删候选`)
|
|
182
|
+
} else if (typedPreferenceIsValid && homePref?.value === null) {
|
|
183
|
+
lines.push(`- 常驻城市: 已显式清除(原话证据:${homePref.evidence};记录于 ${homePref.updated_at})——已无持久软默认;每次行程必须由用户明确出发地,不猜测`)
|
|
184
|
+
} else if (hasHomeCityData) {
|
|
185
|
+
lines.push('- 常驻城市: 无可验证的持久默认——本轮请明确出发地;不从旧画像或全局 evidence 猜测')
|
|
186
|
+
}
|
|
133
187
|
// 旅行时间线(memory-design P1):去过的地方不再主动推荐,除非用户点名
|
|
134
188
|
const trips = readTimelineTrips(stateRoot)
|
|
135
189
|
if (trips.length) {
|
|
@@ -142,10 +196,11 @@ function renderMotivationBrief(stateRoot: string): string {
|
|
|
142
196
|
}
|
|
143
197
|
lines.push(`- 愿望池: 用 gotry_wish_pool_list 按条件召回(0..1),勿直接堆砌`)
|
|
144
198
|
if (p.updated_at) lines.push(`- 更新于: ${p.updated_at}`)
|
|
145
|
-
return weights.length || hard.length ? lines.join('\n') : ''
|
|
199
|
+
return weights.length || hard.length || hasHomeCityData ? lines.join('\n') : ''
|
|
146
200
|
}
|
|
147
201
|
|
|
148
|
-
/**
|
|
202
|
+
/**
|
|
203
|
+
* 时间线摘要(brief 用):最近 3 次行程(目的地+年月);账本优先,未迁移回退文件 */
|
|
149
204
|
/** 同行人摘要(brief 用):label + 约束串 */
|
|
150
205
|
function readCompanions(stateRoot: string): Array<{ label: string; brief: string }> {
|
|
151
206
|
return readCompanionsWithFallback(stateRoot)
|
|
@@ -184,7 +239,62 @@ function benchmarkPinnedPolicy(): { softMs: number; hardMs: number; exit: 'conve
|
|
|
184
239
|
return { softMs, hardMs, exit: 'converge' }
|
|
185
240
|
}
|
|
186
241
|
|
|
187
|
-
|
|
242
|
+
/** 把 hotel-date-gate.message 里的模型转手措辞(请告诉用户 / 请告诉 gotry /
|
|
243
|
+
* 请向用户确认)归一为直接对用户措辞(请补充 / 请提供)。仅做这一类短语的字面
|
|
244
|
+
* 替换,日期示例等可行动细节保留不动;只服务 hotel_search input_required 卡
|
|
245
|
+
* 单点,不动 date-gate 契约、不动其它 presentResult 路径。 */
|
|
246
|
+
function normalizeHotelGateHandoff(message: unknown): string {
|
|
247
|
+
const raw = String(message ?? '')
|
|
248
|
+
if (!raw) return ''
|
|
249
|
+
return raw
|
|
250
|
+
.replace(/请告诉\s*(?:用户|gotry)\s*/g, '请补充 ')
|
|
251
|
+
.replace(/请向用户确认/g, '请提供')
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export interface ApplyTestSeams {
|
|
255
|
+
doctor?: {
|
|
256
|
+
check?: typeof runDoctorChecks
|
|
257
|
+
repair?: DoctorRepairOptions
|
|
258
|
+
}
|
|
259
|
+
/** Isolated test-only effect interpreter; production keeps interpretEffect. */
|
|
260
|
+
effect?: typeof interpretEffect
|
|
261
|
+
/** Test-only clock injection; production derives the reference date from Date. */
|
|
262
|
+
clock?: () => Date
|
|
263
|
+
/** Isolated public map-tool result seam; production uses nested dsh dispatch. */
|
|
264
|
+
groundTransfer?: { provider?: GroundTransferProvider }
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function parseFeasibilityPlanning(raw: unknown, anchor: ReturnType<typeof buildTimeAnchor>): PlanningWindow | null {
|
|
268
|
+
if (raw === undefined) return null
|
|
269
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) throw new Error('planning must be an object')
|
|
270
|
+
const planning = raw as Record<string, unknown>
|
|
271
|
+
const intent = planning['intent']
|
|
272
|
+
const requestedYear = planning['requested_year']
|
|
273
|
+
if (intent !== 'future' && intent !== 'historical') {
|
|
274
|
+
throw new Error('planning.intent must be future or historical')
|
|
275
|
+
}
|
|
276
|
+
if (!Number.isInteger(requestedYear) || Number(requestedYear) < 1_000 || Number(requestedYear) > 9_999) {
|
|
277
|
+
throw new Error('planning.requested_year must be a four-digit integer')
|
|
278
|
+
}
|
|
279
|
+
// referenceDate is always the host clock; the payload cannot supply a stale now.
|
|
280
|
+
return { intent, requestedYear: Number(requestedYear), referenceDate: anchor.today }
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function feasibilityValidationFailure(
|
|
284
|
+
code: FeasibilityValidationFailure['code'],
|
|
285
|
+
summary: string,
|
|
286
|
+
rejected?: FeasibilityValidationFailure['rejected'],
|
|
287
|
+
): FeasibilityValidationFailure {
|
|
288
|
+
return { ok: false, code, summary, rejected: rejected ?? [] }
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function apply(ctx: Context, config: Config, seams: ApplyTestSeams = {}): void {
|
|
292
|
+
const clock = seams.clock ?? (() => new Date())
|
|
293
|
+
const runFlyaiEffect = seams.effect ?? interpretEffect
|
|
294
|
+
const groundTransferResolver = createGroundTransferResolver({
|
|
295
|
+
provider: seams.groundTransfer?.provider ?? createPublicMapDrivingRouteProvider(ctx),
|
|
296
|
+
clock,
|
|
297
|
+
})
|
|
188
298
|
const rawBenchmarkEnvironmentConfigPath = config.benchmarkEnvironmentConfigPath ?? ''
|
|
189
299
|
// ADR-24 v2:产品路径装「路由 + wall-clock 双出口」——用户主观时间是唯一
|
|
190
300
|
// 预算,复杂度决定出口结构(converge/handoff)。benchmark opt-in 钉死
|
|
@@ -239,7 +349,7 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
239
349
|
sp?.variable?.('motivation_brief', () => renderMotivationBrief(config.stateRoot ?? '.'))
|
|
240
350
|
|
|
241
351
|
// 通道路由卡(通道注册表生成,docs/design/tool-orchestration-design.md §2.1/D-8):
|
|
242
|
-
// persona
|
|
352
|
+
// persona 检索条款只留行为契约,机/火/酒通道顺位与额度口径查卡——prose 教义
|
|
243
353
|
// 变查表教义,注册表加通道卡片自动一致。
|
|
244
354
|
sp?.variable?.('channel_routing_card', () => renderRoutingCard())
|
|
245
355
|
|
|
@@ -266,14 +376,29 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
266
376
|
// (headless 无用户在场)一律 fail-closed 拒绝。sessionAccess: ask(默认)|allow|off。
|
|
267
377
|
// v1 教训(2026-08-29 founder 实测「每次都要弹,经常无法点击」):逐调用弹卡 = 骚扰,
|
|
268
378
|
// 会话态收归 capabilities/session-consent.ts。防御:极简宿主/mock ctx 无事件总线时跳过。
|
|
379
|
+
// issue #194 A-轨道 guard 先注册;生产 Cordis 的 prepend 保证它在授权闸
|
|
380
|
+
// 前运行,smoke 的轻量事件采集器也继续把授权闸作为最后一个观察项。
|
|
381
|
+
installSubagentJobIdGuard(ctx)
|
|
269
382
|
const ctxOn = (ctx as unknown as { on?: unknown }).on
|
|
270
383
|
if (typeof ctxOn === 'function') {
|
|
271
|
-
|
|
384
|
+
const gate = createConsentGate({
|
|
272
385
|
access: () => config.sessionAccess ?? 'ask',
|
|
273
386
|
approval: approvalFromContext(ctx),
|
|
274
|
-
})
|
|
387
|
+
})
|
|
388
|
+
// site 绑定(#308):dsh pre-execute 实际派发的 arguments 原样进入 gate;
|
|
389
|
+
// gate 与 execute 共用 resolveSessionSearchKind/interpretArgs,因此 wrapped
|
|
390
|
+
// query.*、缺省 flight、unknown/malformed 的选择与授权和效应路径保持一致。
|
|
391
|
+
ctx.on('tools/pre-execute', gate)
|
|
275
392
|
}
|
|
276
393
|
|
|
394
|
+
// issue #284 修复闸:gotry_doctor.action='repair' 走同一审批缝(scope-keyed,
|
|
395
|
+
// WeakMap<agent>)——同 agent + 同 scope 复用批准,不同 scope 重问,rejected/cancelled
|
|
396
|
+
// = 本会话吊销,unavailable 通道不记。同口径 session-consent,不增新持久面。
|
|
397
|
+
// 注册早于 gotry_doctor(下方),保证工具面闭包捕获当前会话的闸实例。
|
|
398
|
+
const repairApprovalGate = createRepairApprovalGate({
|
|
399
|
+
approval: approvalFromContext(ctx),
|
|
400
|
+
})
|
|
401
|
+
|
|
277
402
|
// LLM_MODEL → dsh 会话面模型覆盖(issue #77;机制与分层见
|
|
278
403
|
// capabilities/model-override.ts 头注)。GOTRY_LLM_MODEL 未设时零行为变化。
|
|
279
404
|
installModelOverride(ctx as unknown as Parameters<typeof installModelOverride>[0])
|
|
@@ -315,7 +440,90 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
315
440
|
required: true,
|
|
316
441
|
properties: {
|
|
317
442
|
request: { type: 'object', additionalProperties: true, required: true, description: '引擎请求:{ motivation weights, hard constraints, window, budget, home hubs }' },
|
|
318
|
-
candidates: { type: 'array', items: { type: 'object', additionalProperties: true }, required: true, description: '候选列表:[{ id, label, services, transfers, stay, minDays }]' },
|
|
443
|
+
candidates: { type: 'array', items: { type: 'object', additionalProperties: true }, required: true, description: '候选列表:[{ id, label, date?, services, transfers, stay, minDays }]' },
|
|
444
|
+
planning: {
|
|
445
|
+
type: 'object', additionalProperties: false,
|
|
446
|
+
description: '可选显式规划上下文;dated 候选省略时默认按宿主时钟作 future 下界过滤,指定 future 年份再加年末上界,historical 仅用于明确历史/回测查询;完全 dateless 输入保持旧可行性计算',
|
|
447
|
+
properties: {
|
|
448
|
+
intent: { type: 'string', enum: ['future', 'historical'], required: true },
|
|
449
|
+
requested_year: { type: 'integer', required: true, description: '用户明确请求的四位年份,不传宿主当前年份' },
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
ground_transfer: {
|
|
453
|
+
type: 'object',
|
|
454
|
+
additionalProperties: false,
|
|
455
|
+
description: '可选显式地面接驳覆盖:仅 destination 位置、显式经纬度和 mode=driving;路线同时绑定抵达方向(A→B)与返程方向(B→A),两方向分别请求/缓存/降级,价格仍来自静态包;缓存 TTL 由能力层固定',
|
|
456
|
+
properties: {
|
|
457
|
+
candidate_id: { type: 'string', description: '要绑定的候选 id;不会按相似度推断' },
|
|
458
|
+
transfer_index: { type: 'integer', description: 'dest_transfers 的零基位置;不会按 mode 推断' },
|
|
459
|
+
position: { type: 'string', description: '当前切片固定为 destination' },
|
|
460
|
+
mode: { type: 'string', description: '当前只接受 driving;其它/缺省模式显式回退静态分钟' },
|
|
461
|
+
origin: {
|
|
462
|
+
type: 'object',
|
|
463
|
+
additionalProperties: false,
|
|
464
|
+
description: '旧版单对坐标:等同于 outbound.origin(能力层自动把返程方向设为其互换)',
|
|
465
|
+
properties: {
|
|
466
|
+
longitude: { type: 'number' },
|
|
467
|
+
latitude: { type: 'number' },
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
destination: {
|
|
471
|
+
type: 'object',
|
|
472
|
+
additionalProperties: false,
|
|
473
|
+
description: '旧版单对坐标:等同于 outbound.destination',
|
|
474
|
+
properties: {
|
|
475
|
+
longitude: { type: 'number' },
|
|
476
|
+
latitude: { type: 'number' },
|
|
477
|
+
},
|
|
478
|
+
},
|
|
479
|
+
outbound: {
|
|
480
|
+
type: 'object',
|
|
481
|
+
additionalProperties: false,
|
|
482
|
+
description: '显式抵达方向坐标对(A→B):不指定时回退 origin/destination',
|
|
483
|
+
properties: {
|
|
484
|
+
origin: {
|
|
485
|
+
type: 'object',
|
|
486
|
+
additionalProperties: false,
|
|
487
|
+
properties: {
|
|
488
|
+
longitude: { type: 'number' },
|
|
489
|
+
latitude: { type: 'number' },
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
destination: {
|
|
493
|
+
type: 'object',
|
|
494
|
+
additionalProperties: false,
|
|
495
|
+
properties: {
|
|
496
|
+
longitude: { type: 'number' },
|
|
497
|
+
latitude: { type: 'number' },
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
},
|
|
501
|
+
},
|
|
502
|
+
return: {
|
|
503
|
+
type: 'object',
|
|
504
|
+
additionalProperties: false,
|
|
505
|
+
description: '显式返程方向坐标对(B→A):不指定时回退 outbound 互换',
|
|
506
|
+
properties: {
|
|
507
|
+
origin: {
|
|
508
|
+
type: 'object',
|
|
509
|
+
additionalProperties: false,
|
|
510
|
+
properties: {
|
|
511
|
+
longitude: { type: 'number' },
|
|
512
|
+
latitude: { type: 'number' },
|
|
513
|
+
},
|
|
514
|
+
},
|
|
515
|
+
destination: {
|
|
516
|
+
type: 'object',
|
|
517
|
+
additionalProperties: false,
|
|
518
|
+
properties: {
|
|
519
|
+
longitude: { type: 'number' },
|
|
520
|
+
latitude: { type: 'number' },
|
|
521
|
+
},
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
},
|
|
525
|
+
},
|
|
526
|
+
},
|
|
319
527
|
},
|
|
320
528
|
},
|
|
321
529
|
},
|
|
@@ -331,10 +539,38 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
331
539
|
// 路径——候选形态枚举求解,~6ms/次)。unified 内部有 try-catch 护栏覆盖 wasm 异常。
|
|
332
540
|
const started = Date.now()
|
|
333
541
|
const payload = args.payload as Record<string, unknown>
|
|
542
|
+
const anchor = buildTimeAnchor(clock())
|
|
543
|
+
let planning: PlanningWindow | null
|
|
544
|
+
try {
|
|
545
|
+
planning = parseFeasibilityPlanning(payload['planning'], anchor)
|
|
546
|
+
} catch (error) {
|
|
547
|
+
const summary = error instanceof Error ? error.message : 'planning context is invalid'
|
|
548
|
+
return feasibilityValidationFailure('invalid_planning_context', summary)
|
|
549
|
+
}
|
|
334
550
|
const req = parseRequest(payload['request'] as Record<string, unknown>)
|
|
335
551
|
const cands = (payload['candidates'] as Record<string, unknown>[]).map(parseCandidate)
|
|
336
|
-
const
|
|
337
|
-
|
|
552
|
+
const execution = _exec && typeof _exec === 'object' ? _exec as ToolRunContext : undefined
|
|
553
|
+
let solverCandidates = cands
|
|
554
|
+
let groundTransferEvidence: GroundTransferResolution | undefined
|
|
555
|
+
if (payload['ground_transfer'] !== undefined) {
|
|
556
|
+
const prepared = await resolveGroundTransferPayload(
|
|
557
|
+
payload['ground_transfer'],
|
|
558
|
+
cands,
|
|
559
|
+
groundTransferResolver,
|
|
560
|
+
{ signal: execution?.signal, execution },
|
|
561
|
+
)
|
|
562
|
+
solverCandidates = prepared.candidates
|
|
563
|
+
groundTransferEvidence = prepared.resolution
|
|
564
|
+
}
|
|
565
|
+
const spec = segmentsFromCandidate(req, solverCandidates)
|
|
566
|
+
const planningCheck = applyPlanningWindow(spec, planning, anchor, { defaultDatedFuture: true })
|
|
567
|
+
if (planningCheck.error) {
|
|
568
|
+
return feasibilityValidationFailure('planning_window_rejected', planningCheck.error, planningCheck.rejected)
|
|
569
|
+
}
|
|
570
|
+
const solved = solveChoiceSegment(planningCheck.spec, req) as Record<string, unknown>
|
|
571
|
+
const result = groundTransferEvidence
|
|
572
|
+
? exposeGroundTransferEvidence(solved, groundTransferEvidence)
|
|
573
|
+
: solved
|
|
338
574
|
const dir = await ensureStateDir(config.stateRoot)
|
|
339
575
|
await recordLatency(join(dir, 'bridge-latency.jsonl'), Date.now() - started, 'feasibility_check:in-process-unified').catch(() => {})
|
|
340
576
|
return { ok: true, ...result, latency_ms: Date.now() - started, via: 'in-process-unified' }
|
|
@@ -371,6 +607,9 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
371
607
|
+ 'existing history is never deleted. Requires evidence on every call (P0 anti-fabrication rule).',
|
|
372
608
|
// D-30 第五刀(issue #112):profile blob → 结构化闭合对象;evidence(P0 反伪造红线)进嵌套
|
|
373
609
|
// schema 由宿主权闸——无证据的保存请求入口即被结构化拒绝(ToolFailure 形状)
|
|
610
|
+
// issue #338:homeCity 字段(string=设置,null=显式清除,undefined=不动);单条非空
|
|
611
|
+
// evidence 可省略 binding,多条 evidence 必须显式 homeCityEvidence。
|
|
612
|
+
// dsh 参数 schema 仅支持单值 type,string|null 用 oneOf 表达。
|
|
374
613
|
parameters: {
|
|
375
614
|
profile: {
|
|
376
615
|
type: 'object',
|
|
@@ -380,6 +619,17 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
380
619
|
weights: { type: 'object', additionalProperties: true, description: '动机权重增量,如 { escape_rest: 0.7 }(可选,权重变更须伴新证据)' },
|
|
381
620
|
evidence: { type: 'array', items: { type: 'string' }, required: true, description: '本轮新事实的用户原话数组(P0:必带)' },
|
|
382
621
|
hard: { type: 'object', additionalProperties: true, description: '硬约束:{ wake_not_before, min_arrival_energy_pct }(可选)' },
|
|
622
|
+
homeCity: {
|
|
623
|
+
oneOf: [
|
|
624
|
+
{ type: 'string', description: '设置持久常驻城市/默认出发地(用户原话指明常驻),如 "上海"' },
|
|
625
|
+
{ type: 'null', description: '显式清除常驻城市(用于遗忘/迁居)' },
|
|
626
|
+
],
|
|
627
|
+
description: '持久常驻城市/默认出发地(string=设置,null=显式清除);改值须伴新 evidence(P0);恰一条非空可省略 binding,多条须显式;不删候选/不压当轮 origin',
|
|
628
|
+
},
|
|
629
|
+
homeCityEvidence: {
|
|
630
|
+
type: 'string',
|
|
631
|
+
description: 'homeCity 对应的 exact 用户原话;显式传入时必须逐字出现在本次 profile.evidence 中且为新证据(同值同绑定重复调用例外为幂等);evidence 恰好一条非空时可省略并绑定该条,多条时必须显式传入',
|
|
632
|
+
},
|
|
383
633
|
},
|
|
384
634
|
},
|
|
385
635
|
},
|
|
@@ -388,17 +638,48 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
388
638
|
schema: { type: 'json' },
|
|
389
639
|
render: (_args, value) => [{ type: 'text', text: `动机画像已保存:${String((value as { path?: string }).path ?? '')}` }],
|
|
390
640
|
},
|
|
391
|
-
async execute(args
|
|
641
|
+
async execute(args, _exec: unknown) {
|
|
392
642
|
// T1 接线:本工具是增量补丁语义(契约 18——模型每轮把对话新事实带进来),
|
|
393
643
|
// 经 mergeProfile 守门合并进既有画像(追加不删史/幂等/权重变更须伴新证据),
|
|
394
644
|
// 不再整档覆盖。首次调用(无档案)= 全量建立。
|
|
395
645
|
// ADR-15:守门+事件+投影在账本单事务内完成,evidence 红线拒绝即回滚,账本无痕。
|
|
396
|
-
|
|
646
|
+
// issue #338:homeCity 同走 mergeProfile 守门(null=显式清除)。单条非空
|
|
647
|
+
// evidence 可提供省略的 unambiguous binding;多条 evidence 不按数组位置推断。
|
|
648
|
+
const incoming = (args.profile ?? {}) as { weights?: Record<string, number>; evidence?: string[]; hard?: Record<string, unknown>; homeCity?: string | null; homeCityEvidence?: string }
|
|
397
649
|
if (!incoming.evidence?.length) {
|
|
398
650
|
throw new Error('refusing to save a motivation profile without evidence (P0 anti-fabrication rule)')
|
|
399
651
|
}
|
|
652
|
+
// issue #338:trim 后空白拒绝(零事件落账)。空白字符串「无值」≠
|
|
653
|
+
// 「显式清除」——后者是 null,前者是没有声明。不擅自解释,显式拒收。
|
|
654
|
+
if (typeof incoming.homeCity === 'string' && incoming.homeCity.trim().length === 0) {
|
|
655
|
+
throw new Error('gotry_motivation_save.homeCity 是空白字符串,既不是声明也不是清除;请明确传城市名或 null')
|
|
656
|
+
}
|
|
657
|
+
let boundHomeCityEvidence: string | undefined
|
|
658
|
+
if (incoming.homeCity !== undefined) {
|
|
659
|
+
if (incoming.homeCityEvidence !== undefined) {
|
|
660
|
+
if (typeof incoming.homeCityEvidence !== 'string' || incoming.homeCityEvidence.trim().length === 0) {
|
|
661
|
+
throw new Error('gotry_motivation_save.homeCityEvidence 为空白;homeCity 必须绑定本次 evidence 中的一条 exact 用户原话')
|
|
662
|
+
}
|
|
663
|
+
if (!incoming.evidence.some(e => e === incoming.homeCityEvidence)) {
|
|
664
|
+
throw new Error('gotry_motivation_save.homeCityEvidence 必须逐字匹配本次 profile.evidence 中的一条用户原话')
|
|
665
|
+
}
|
|
666
|
+
boundHomeCityEvidence = incoming.homeCityEvidence
|
|
667
|
+
} else if (incoming.evidence.length === 1 && typeof incoming.evidence[0] === 'string' && incoming.evidence[0].trim().length > 0) {
|
|
668
|
+
boundHomeCityEvidence = incoming.evidence[0]
|
|
669
|
+
} else {
|
|
670
|
+
throw new Error('gotry_motivation_save.homeCityEvidence 缺失或有歧义;evidence 恰好一条非空时可省略,多条 evidence 必须显式绑定 exact 用户原话')
|
|
671
|
+
}
|
|
672
|
+
} else if (incoming.homeCityEvidence !== undefined) {
|
|
673
|
+
throw new Error('gotry_motivation_save.homeCityEvidence 只能与 homeCity 一起传入')
|
|
674
|
+
}
|
|
400
675
|
const ledger = ensureLedger(config.stateRoot)
|
|
401
|
-
const res = ledger.appendMotivationPatch({
|
|
676
|
+
const res = ledger.appendMotivationPatch({
|
|
677
|
+
weights: incoming.weights,
|
|
678
|
+
evidence: incoming.evidence,
|
|
679
|
+
hard: incoming.hard,
|
|
680
|
+
homeCity: incoming.homeCity,
|
|
681
|
+
homeCityEvidence: boundHomeCityEvidence,
|
|
682
|
+
} as ProfilePatch)
|
|
402
683
|
const profileJson = JSON.parse(JSON.stringify(res.profile)) as JsonObject
|
|
403
684
|
return res.saved
|
|
404
685
|
? { ok: true, saved: true, path: ledger.dbPath, profile: profileJson, summary: '画像已合并入账本(单事务)' }
|
|
@@ -651,14 +932,18 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
651
932
|
description: routed('gotry_hotel_search',
|
|
652
933
|
'Search hotels via hotelbyte-cli (real-time when hbcli credentials exist, falls back to the static pack with explicit evidence tagging). '
|
|
653
934
|
+ 'Input: destination city name + optional dates/adults. Dates accept verbatim natural expressions (下周五 / 8.20 / 下周五+3) — '
|
|
654
|
-
+ 'the code layer resolves them against the time anchor
|
|
655
|
-
+ '
|
|
935
|
+
+ 'the code layer resolves them against the time anchor. **Dates are required for a supplier search** (issue #283): missing, blank, '
|
|
936
|
+
+ 'single-side, unresolvable, or checkOut ≤ checkIn returns an actionable input_required result WITHOUT spawning the vendor CLI; '
|
|
937
|
+
+ 'only after a complete valid stay window is fixed do we forward to hbcli. '
|
|
938
|
+
+ 'Output: hotel list with evidence chain ([realtime-API:hbcli] + fetch timestamp, '
|
|
656
939
|
+ 'or [static-pack:estimate]) per the L4 invariant.'),
|
|
657
940
|
// D-30 第二刀(issue #112):query blob → 平铺 typed 契约(同 flyai 刀法)。
|
|
658
941
|
parameters: {
|
|
659
942
|
destination: { type: 'string', required: true, description: '目的地城市,如 大理' },
|
|
660
|
-
|
|
661
|
-
|
|
943
|
+
// issue #283 闸:日期字段对模型面仍是契约必填(required 标在 description,schema 不强制,
|
|
944
|
+
// 让 gate 产出更可行动的错误——dsh schema 拒绝信息太泛)
|
|
945
|
+
checkIn: { type: 'string', description: '入住日:YYYY-MM-DD 或自然表达(下周五/8.20/下周五+3);REQUIRED——缺失/空白/无法解析由 date-gate 返回 input_required,不查' },
|
|
946
|
+
checkOut: { type: 'string', description: 'checkOut 退房日:YYYY-MM-DD 或自然表达,形态同 checkIn;REQUIRED——必须晚于入住日,否则不查' },
|
|
662
947
|
adults: { type: 'integer', description: '成人数(顶层字段,默认 2)' },
|
|
663
948
|
},
|
|
664
949
|
output: {
|
|
@@ -670,25 +955,37 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
670
955
|
if (!q.destination) throw new Error('gotry_hotel_search requires destination')
|
|
671
956
|
const started = Date.now()
|
|
672
957
|
const fallbackPath = join(import.meta.dirname, '..', '..', 'data', 'hotels_2026.json')
|
|
673
|
-
//
|
|
674
|
-
//
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
const
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
958
|
+
// issue #283 闸:完整、有效且退房晚于入住才能派发 hbcli;失败直接返回
|
|
959
|
+
// input_required,不发供应商命令,不把当前窗口查询当所需日期结果。
|
|
960
|
+
// 复用既有 time-anchor/slot-spec:词表内自然表达 → 绝对日期;
|
|
961
|
+
// 词表外 → unresolved,闸失败,不再走「无日期降级」伪装默认窗口价。
|
|
962
|
+
const gate = evaluateHotelStayDates(q.checkIn, q.checkOut)
|
|
963
|
+
if (!gate.ok) {
|
|
964
|
+
// 闸失败:不调用 interpretEffect,不调 hbcli;延迟日志不写(no spawn)。
|
|
965
|
+
// summary 用「需要 X 信息才能查询酒店」开头的可行动句式,presentResult 直接
|
|
966
|
+
// 渲成「酒店:<dest> 缺日期」行动卡(替代旧版「无结果」空卡)。
|
|
967
|
+
const summary = `需要补充${(gate.missing ?? []).map(m => m === 'checkIn' ? '入住日' : '退房日').join('和')}才能查询酒店「${q.destination ?? ''}」:${gate.message}`
|
|
968
|
+
return {
|
|
969
|
+
ok: false,
|
|
970
|
+
verdict: gate.verdict,
|
|
971
|
+
reason: gate.reason,
|
|
972
|
+
missing: gate.missing,
|
|
973
|
+
raw: gate.raw,
|
|
974
|
+
destination: q.destination,
|
|
975
|
+
message: gate.message,
|
|
976
|
+
summary,
|
|
977
|
+
evidence: gate.evidence,
|
|
978
|
+
latency_ms: Date.now() - started,
|
|
979
|
+
} as never
|
|
686
980
|
}
|
|
981
|
+
// 闸通过:绝对日期已就位;slot 解析过程产生的原话 → 绝对日期 注记回显
|
|
982
|
+
// (工具层 date_notes 与既有 D-10 切片 B 形态兼容)
|
|
983
|
+
const dateNotes = [...gate.notes]
|
|
687
984
|
// 效应解译层(ADR-18):渠道选择/韧性(重试/熔断)在解译器,result 原样透传——
|
|
688
985
|
// 断路拒绝时返回平铺失败面(不发起查询,不伪装成 miss)
|
|
689
986
|
const itp = await interpretEffect({
|
|
690
987
|
effect: 'HBCLI_HOTEL_SEARCH',
|
|
691
|
-
params: { destination: q.destination, checkIn:
|
|
988
|
+
params: { destination: q.destination, checkIn: gate.checkIn, checkOut: gate.checkOut, adults: q.adults, hbcliBin: config.hbcliBin, timeoutMs: config.timeoutMs, fallbackPath },
|
|
692
989
|
})
|
|
693
990
|
if (!itp.result) return declinedObservation('HBCLI_HOTEL_SEARCH', itp.trace)
|
|
694
991
|
const resp = itp.result
|
|
@@ -711,7 +1008,25 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
711
1008
|
},
|
|
712
1009
|
presentCall: args => ({ card: 'generic', title: `酒店搜索:${args.destination ?? ''}`, kind: 'search', rawInput: args }),
|
|
713
1010
|
presentResult: (_args, value) => {
|
|
714
|
-
|
|
1011
|
+
// 闸失败:渲染为「input_required」行动卡,引导模型向用户追问日期;
|
|
1012
|
+
// 不再退化为「无结果」空卡——闸拒绝的可行动语义必须显式进 UI。
|
|
1013
|
+
// UI 面禁用内部工程词(check_in_blank / 闸拒绝 / ISO 字符串),只露「入住日/退房日」
|
|
1014
|
+
// 与具体日期示例供模型追问。
|
|
1015
|
+
const r = value as { hotels?: unknown; via?: string; destination?: string; summary?: string; verdict?: string; reason?: string; missing?: string[]; message?: string }
|
|
1016
|
+
if (r.verdict === 'input_required') {
|
|
1017
|
+
const missingLabels = (r.missing ?? []).map(m => m === 'checkIn' ? '入住日' : '退房日')
|
|
1018
|
+
const fieldText = missingLabels.length ? missingLabels.join('和') : '入住日和退房日'
|
|
1019
|
+
const dateHint = !missingLabels.length || missingLabels.length === 2
|
|
1020
|
+
? '例如 2026-09-18 / 2026-09-20'
|
|
1021
|
+
: missingLabels[0] === '入住日' ? '例如 2026-09-18' : '例如 2026-09-20'
|
|
1022
|
+
return {
|
|
1023
|
+
card: 'generic',
|
|
1024
|
+
title: `酒店:${r.destination ?? ''} 需要${fieldText}`,
|
|
1025
|
+
// P2 措辞修正:直接对用户说话的句式,不再绕一道「请告诉.../请向用户确认」转给模型;
|
|
1026
|
+
// gate.message 里残留的字段指引也归一为直接对用户措辞,保留日期示例等可行动细节。
|
|
1027
|
+
content: [{ type: 'text', text: `请补充 ${fieldText}(${dateHint})后再次查询。\n${normalizeHotelGateHandoff(r.message)}` }],
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
715
1030
|
const h = r.hotels
|
|
716
1031
|
const liveCount = Array.isArray(h) ? h.length : 0
|
|
717
1032
|
// 静态包是 {meta, stays} 对象而非数组(issue #24:此前计数恒 0 → UI 显示「无结果」)
|
|
@@ -915,7 +1230,7 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
915
1230
|
return { ok: false, summary: 'checkOut 需 YYYY-MM-DD(与 checkIn 成对)' } as const
|
|
916
1231
|
}
|
|
917
1232
|
}
|
|
918
|
-
const itp = await
|
|
1233
|
+
const itp = await runFlyaiEffect({ effect: 'FLYAI_SEARCH', params: { kind: 'hotel', destName: dest, checkInDate: q.checkIn, checkOutDate: q.checkOut, keyWords: q.keyWords } })
|
|
919
1234
|
if (!itp.result) return declinedObservation('FLYAI_SEARCH', itp.trace)
|
|
920
1235
|
const r = itp.result
|
|
921
1236
|
await noteChannel('flyai', r.verdict)
|
|
@@ -948,7 +1263,7 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
948
1263
|
+ `多为用户时间表达未带年份所致——向用户确认年份(或按未来最近的同月日修正)后再查。`,
|
|
949
1264
|
})) as Record<string, never>
|
|
950
1265
|
}
|
|
951
|
-
const itp = await
|
|
1266
|
+
const itp = await runFlyaiEffect({ effect: 'FLYAI_SEARCH', params: { kind, origin: q.from, destination: q.to, depDate: q.date } })
|
|
952
1267
|
if (!itp.result) return declinedObservation('FLYAI_SEARCH', itp.trace)
|
|
953
1268
|
const r = itp.result
|
|
954
1269
|
await noteChannel('flyai', r.verdict)
|
|
@@ -1039,6 +1354,7 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
1039
1354
|
+ 'Transport: GoTry Session Bridge browser extension (one-time install) — the agent side never talks to Chrome debugging, ZERO system dialogs; read-only by construction (the extension never issues requests; it only passively forwards the site\'s own search responses; agent NEVER touches credentials/captcha; on captcha it stops and returns challenged). '
|
|
1040
1355
|
+ 'kind="flight": from/to 中文城市名 + date YYYY-MM-DD — sniffs the site search API for structured options. Evidence [会话:ctrip-flight@ts]. '
|
|
1041
1356
|
+ 'kind="hotel": to=目的地中文, cityId? = the numeric city= in a hotels.ctrip.com list URL (web-search it when the destination is outside the built-in city table), checkIn?/checkOut? (YYYY-MM-DD), adults?; hotel prices are the user\'s real logged-in prices. Evidence [会话:ctrip-hotel@ts]. '
|
|
1357
|
+
+ 'kind="dida": Dida supplier-portal realtime hotel rates on the employee\'s own logged-in session (hotel-be portal integration line) — no from/to needed; the portal find page\'s own requests are sniffed passively. Rates carry ratePlanId/referenceNo for the server-side booking chain. Evidence [会话:dida-portal@ts]. '
|
|
1042
1358
|
+ 'kind="train": from/to/date(YYYY-MM-DD) + fromStationTelecode?/toStationTelecode? (three-letter codes in the kyfw query URL, for cities outside the built-in table) — 12306 left-ticket query (public face): train codes, times, durations, seat availability; the list API carries NO prices (prices live on the 12306 page). Evidence [会话:train-12306@ts]. '
|
|
1043
1359
|
+ 'verdict needs-login = call gotry_session_login (opens the Ctrip login entry in the user\'s own foreground tab — no terminal, no credentials through GoTry); '
|
|
1044
1360
|
+ `needs-extension = one-time browser-extension install (Chrome Web Store one-click, installUrl is also surfaced as a clickable link in the verdict field for dsh UI to render) — the DEFAULT transport; cdp (chrome://inspect remote debugging) is a diagnostic fallback only via GOTRY_SESSION_TRANSPORT=cdp. `
|
|
@@ -1049,7 +1365,7 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
1049
1365
|
// (docs/design/tool-orchestration-design.md §4③「interpretArgs 留作旧形态容忍层」),blob 调用在
|
|
1050
1366
|
// execute 内归一后走原条件闸,结构化报错不崩。flyai 因 kind required 仍在宿主权即拒。
|
|
1051
1367
|
parameters: {
|
|
1052
|
-
kind: { type: 'string', enum: ['flight', 'hotel', 'train'], description: '默认 flight 机票;hotel 携程酒店(用户登录态真实价);train 12306 余票(公开面)' },
|
|
1368
|
+
kind: { type: 'string', enum: ['flight', 'hotel', 'train', 'dida'], description: '默认 flight 机票;hotel 携程酒店(用户登录态真实价);train 12306 余票(公开面);dida 供应商门户实时价(员工登录态)' },
|
|
1053
1369
|
from: { type: 'string', description: '出发城市中文(词表内),如 上海——kind=flight|train 必填' },
|
|
1054
1370
|
to: { type: 'string', description: '到达城市中文——kind=flight|train 必填;kind=hotel 时为目的地(必填)' },
|
|
1055
1371
|
date: { type: 'string', description: '出发日期 YYYY-MM-DD——kind=flight|train 必填,须为今天或未来' },
|
|
@@ -1062,7 +1378,34 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
1062
1378
|
},
|
|
1063
1379
|
output: { schema: { type: 'json' }, render: (_a, v) => [{ type: 'text', text: String((v as { summary?: string }).summary ?? JSON.stringify(v).slice(0, 600)) }] },
|
|
1064
1380
|
async execute(args, _exec) {
|
|
1065
|
-
const
|
|
1381
|
+
const selectedKind = resolveSessionSearchKind(args)
|
|
1382
|
+
if (!selectedKind.ok) return { ok: false, summary: selectedKind.reason } as const
|
|
1383
|
+
const q = { ...unwrapQuery<{ kind?: string; from?: string; to?: string; date?: string; cityId?: number | string; checkIn?: string; checkOut?: string; adults?: number; fromStationTelecode?: string; toStationTelecode?: string }>(args), kind: selectedKind.kind }
|
|
1384
|
+
const runSessionEffect = seams.effect ?? interpretEffect
|
|
1385
|
+
// ---- Dida 供应商门户(2026-09-09 实装;hotel-be portal integration 迁移线)----
|
|
1386
|
+
if (q.kind === 'dida') {
|
|
1387
|
+
const itpD = await runSessionEffect({
|
|
1388
|
+
effect: 'SESSION_DIDA_SEARCH',
|
|
1389
|
+
params: {
|
|
1390
|
+
auditPath: join(config.stateRoot ?? '.', 'gotry-state', 'session-incidents.jsonl'),
|
|
1391
|
+
},
|
|
1392
|
+
})
|
|
1393
|
+
if (!itpD.result) return declinedObservation('SESSION_DIDA_SEARCH', itpD.trace)
|
|
1394
|
+
const rD = itpD.result
|
|
1395
|
+
await noteChannel('session:dida-portal', rD.verdict)
|
|
1396
|
+
const topD = (rD.rates ?? []).slice(0, 8).map(x => `${x.hotelName ?? x.hotelId ?? '?'} ${x.roomName ?? ''} ${x.ratePlanId ?? ''} ${x.currency ?? ''}${x.price} 库存${x.inventory ?? '?'}${x.referenceNo ? ` ref=${x.referenceNo.slice(0, 12)}…` : ''}`)
|
|
1397
|
+
const summaryD = rD.verdict === 'hit'
|
|
1398
|
+
? `Dida 门户实时价(员工本人登录态,被动嗅探)前 ${topD.length} 条(预订由服务端适配器凭 referenceNo 续链,gotry 不碰下单):\n${topD.join('\n')}\n${rD.evidence}`
|
|
1399
|
+
: rD.verdict === 'cooldown'
|
|
1400
|
+
? `会话检索节律闸冷却中(两次会话检索需 ≥30s 间隔)——稍候重试或先用其他工具推进。${rD.evidence}`
|
|
1401
|
+
: rD.verdict === 'needs-login'
|
|
1402
|
+
? `${rD.error ?? ''} ${rD.evidence}`
|
|
1403
|
+
: `Dida 门户会话检索未取回(${rD.verdict}):${rD.error ?? ''} ${rD.evidence}`
|
|
1404
|
+
return JSON.parse(JSON.stringify({
|
|
1405
|
+
...rD, summary: summaryD,
|
|
1406
|
+
...(rD.verdict !== 'hit' ? routingField('search-hotel', 'session:dida-portal') : {}),
|
|
1407
|
+
})) as Record<string, never>
|
|
1408
|
+
}
|
|
1066
1409
|
// ---- 会话酒店(2026-09-03 实装;2026-09-02 迪拜 session:用户要携程找酒店,会话面却只有机票)----
|
|
1067
1410
|
// ---- 会话火车(2026-09-03 实装;12306 公开查询面,无登录闸)----
|
|
1068
1411
|
if (q.kind === 'train') {
|
|
@@ -1077,7 +1420,7 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
1077
1420
|
summary: `未发起查询:日期 ${q.date} 已是过去(今天 ${todayT}),过去不存在在售火车票。向用户确认日期后再查。`,
|
|
1078
1421
|
})) as Record<string, never>
|
|
1079
1422
|
}
|
|
1080
|
-
const itpT = await
|
|
1423
|
+
const itpT = await runSessionEffect({
|
|
1081
1424
|
effect: 'SESSION_TRAIN_SEARCH',
|
|
1082
1425
|
params: {
|
|
1083
1426
|
from: q.from, to: q.to, date: q.date,
|
|
@@ -1088,7 +1431,11 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
1088
1431
|
if (!itpT.result) return declinedObservation('SESSION_TRAIN_SEARCH', itpT.trace)
|
|
1089
1432
|
const rT = itpT.result
|
|
1090
1433
|
await noteChannel('session:12306-train', rT.verdict)
|
|
1091
|
-
|
|
1434
|
+
// #355:only the invocation-produced binding may enter the fact log;
|
|
1435
|
+
// malformed/transport/challenge states remain no-fact, and Y alone is
|
|
1436
|
+
// not rendered as bookable without a recognized available seat token.
|
|
1437
|
+
await appendFacts(config.stateRoot ?? '.', factsFromSessionTrain({ from: q.from, to: q.to, date: q.date }, rT, clock()))
|
|
1438
|
+
const topT = (rT.trains ?? []).slice(0, 10).map(t => `${t.trainCode} ${t.depTime}→${t.arrTime} 历时${Math.round(t.durationMin / 60 * 10) / 10}h ${t.canWebBuy === 'Y' && hasRecognizedAvailableSeat(t) ? '可订' : '余票待确认'}${Object.entries(t.seats).filter(([, v]) => v && v !== '--' && v !== '无').slice(0, 3).map(([k, v]) => `${k}:${v}`).join(' ')}`)
|
|
1092
1439
|
const summaryT = rT.verdict === 'hit'
|
|
1093
1440
|
? `${q.from}→${q.to} ${q.date} 余票(12306 公开查询面,${(rT.trains ?? []).length} 趟)前 ${topT.length} 条(列表接口不含票价,票价以 12306 落地页为准):\n${topT.join('\n')}\n${rT.evidence}`
|
|
1094
1441
|
: rT.verdict === 'cooldown'
|
|
@@ -1103,7 +1450,7 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
1103
1450
|
if (!q.to) {
|
|
1104
1451
|
return { ok: false, summary: 'kind=hotel 需要 to(目的地中文;城市码表外带 cityId=携程酒店 list 页 URL 里的 city= 数字)' } as const
|
|
1105
1452
|
}
|
|
1106
|
-
const itp = await
|
|
1453
|
+
const itp = await runSessionEffect({
|
|
1107
1454
|
effect: 'SESSION_HOTEL_SEARCH',
|
|
1108
1455
|
params: {
|
|
1109
1456
|
to: q.to, cityId: q.cityId, checkIn: q.checkIn, checkOut: q.checkOut, adults: q.adults,
|
|
@@ -1136,7 +1483,7 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
1136
1483
|
}
|
|
1137
1484
|
// 效应解译层(ADR-18):SESSION 通道策略=永不重试/不熔断,节律闸在渠道内;
|
|
1138
1485
|
// 解译器只做分发与证据拼装,verdict 语义(risk 型 needs-login/challenged)原样透传
|
|
1139
|
-
const itp = await
|
|
1486
|
+
const itp = await runSessionEffect({
|
|
1140
1487
|
effect: 'SESSION_FLIGHT_SEARCH',
|
|
1141
1488
|
params: {
|
|
1142
1489
|
from: q.from, to: q.to, date: q.date,
|
|
@@ -1160,12 +1507,17 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
1160
1507
|
})) as Record<string, never>
|
|
1161
1508
|
},
|
|
1162
1509
|
presentCall: args => {
|
|
1163
|
-
const
|
|
1510
|
+
const selected = resolveSessionSearchKind(args)
|
|
1511
|
+
const kind = selected.ok ? selected.kind : undefined
|
|
1512
|
+
const raw = args as { to?: unknown; from?: unknown }
|
|
1513
|
+
const callTitle = kind === 'hotel' ? `会话酒店:${raw.to ?? ''}` : kind === 'train' ? `会话火车:${raw.from ?? ''}` : kind === 'dida' ? '会话酒店:Dida 门户实时价' : `会话检索:${raw.from ?? ''}`
|
|
1164
1514
|
return { card: 'generic', title: callTitle, kind: 'fetch', rawInput: args }
|
|
1165
1515
|
},
|
|
1166
1516
|
presentResult: (args, value) => {
|
|
1517
|
+
const selected = resolveSessionSearchKind(args)
|
|
1518
|
+
const kind = selected.ok ? selected.kind : undefined
|
|
1167
1519
|
const r = value as { verdict?: string; options?: unknown[]; hotels?: unknown[]; installUrl?: string }
|
|
1168
|
-
const isHotel =
|
|
1520
|
+
const isHotel = kind === 'hotel'
|
|
1169
1521
|
const n = Math.max((r.options ?? []).length, (r.hotels ?? []).length)
|
|
1170
1522
|
const needsExt = r.verdict === 'needs-extension'
|
|
1171
1523
|
const label = r.verdict === 'hit'
|
|
@@ -1177,7 +1529,7 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
1177
1529
|
: r.verdict ?? '降级'
|
|
1178
1530
|
const content: Array<{ type: 'text'; text: string }> = [{ type: 'text', text: String((value as { summary?: string }).summary ?? '') }]
|
|
1179
1531
|
if (needsExt && r.installUrl) content.push({ type: 'text', text: `安装链接:${r.installUrl}` })
|
|
1180
|
-
const face =
|
|
1532
|
+
const face = kind === 'hotel' ? '会话酒店' : kind === 'train' ? '会话火车' : kind === 'dida' ? '会话酒店:Dida' : '会话检索'
|
|
1181
1533
|
return { card: 'generic', title: `${face}:${label}`, content }
|
|
1182
1534
|
},
|
|
1183
1535
|
}))
|
|
@@ -1397,7 +1749,7 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
1397
1749
|
await recordLatency(join(dir, 'bridge-latency.jsonl'), Date.now() - started, 'agent-reach:doctor').catch(() => {})
|
|
1398
1750
|
const summary = st.via === 'agent-reach-cli'
|
|
1399
1751
|
? `Agent Reach doctor(上游 CLI,原样透传):\n${st.output}\n${st.evidence}`
|
|
1400
|
-
: `Agent Reach 未装配(.venv 缺失)——可选依赖,机/酒/网页社媒读取之外的工具不受影响。\n补装:终端跑 npx gotry doctor --fix;整体依赖状态可调 gotry_doctor 工具查看。\n${st.evidence}`
|
|
1752
|
+
: `Agent Reach 未装配(.venv 缺失)——可选依赖,机/酒/网页社媒读取之外的工具不受影响。\n补装:终端跑 npx @danceiny/gotry doctor --fix;整体依赖状态可调 gotry_doctor 工具查看。\n${st.evidence}`
|
|
1401
1753
|
return JSON.parse(JSON.stringify({
|
|
1402
1754
|
ok: st.ok, via: st.via, output: st.output, evidence: st.evidence, summary,
|
|
1403
1755
|
latency_ms: Date.now() - started,
|
|
@@ -1432,7 +1784,7 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
1432
1784
|
: r.verdict === 'needs-setup'
|
|
1433
1785
|
? `${q.channel}.${q.method} → 需配置(上游 check() 原话): ${r.setup ?? ''}\n${r.evidence}`
|
|
1434
1786
|
: r.verdict === 'not-installed'
|
|
1435
|
-
? `${q.channel}.${q.method} → 上游未装配(可选依赖): ${r.setup ?? ''}\n整体依赖状态可调 gotry_doctor 查看;补装: npx gotry doctor --fix\n${r.evidence}`
|
|
1787
|
+
? `${q.channel}.${q.method} → 上游未装配(可选依赖): ${r.setup ?? ''}\n整体依赖状态可调 gotry_doctor 查看;补装: npx @danceiny/gotry doctor --fix\n${r.evidence}`
|
|
1436
1788
|
: `${q.channel}.${q.method} → ${r.error ?? 'error'}${r.inventory ? `\n上游清单: ${JSON.stringify(r.inventory).slice(0, 1200)}` : ''}\n${r.evidence}`
|
|
1437
1789
|
return JSON.parse(JSON.stringify({
|
|
1438
1790
|
ok: r.ok, channel: r.channel, method: q.method, verdict: r.verdict,
|
|
@@ -1460,13 +1812,19 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
1460
1812
|
registerGuarded(defineTool({
|
|
1461
1813
|
name: 'gotry_doctor',
|
|
1462
1814
|
description:
|
|
1463
|
-
'Check optional-dependency health for ALL gotry tools (read-only
|
|
1815
|
+
'Check optional-dependency health for ALL gotry tools (read-only by default). Coverage: GoTry Session Bridge extension / Agent Reach (.venv) / hbcli (hotel realtime) / FlyAI key + recent trial-quota exhaustion time / dsh-calendar mount state / dsh-better-sidebar / dsh-map-tools / dsh-tool-ask-user. '
|
|
1464
1816
|
+ 'Call this when ANY gotry tool returns not-installed / needs-setup, when the user asks 「体检/依赖状态/工具为什么不可用」, or BEFORE leaning on a channel for a plan. '
|
|
1465
1817
|
+ 'Returns per-item status (ok/degraded/missing) with exact fix commands. '
|
|
1466
|
-
+ '
|
|
1818
|
+
+ 'Action=diagnose (default): pure read-only — never installs anything, only renders the report. '
|
|
1819
|
+
+ 'Action=repair: optional autonomous repair — only attempts items the user named via `items` (stable doctor item ids, e.g. ["agent-reach","sidebar"]); empty/missing items means repair every auto-repairable gap. Items requiring browser-store install / credentials / API key / profile config / package reinstall / Node upgrade are NEVER auto-installed — they are reported as needs-user-action / unavailable and require the user to handle them. '
|
|
1820
|
+
+ 'Repair flow is observable: diagnosis → scope-keyed plan (selected auto-repairable items + skipped) → runtime approval card (per agent, per scope; same agent+scope reuses approval, different scope re-asks) → execute installer → recheck → per-item installed/failed with retry command. '
|
|
1821
|
+
+ 'Rejected/cancelled = no execution; unavailable approval channel = no execution (clear structured outcome). LLM keys are the dsh host\'s business and are deliberately out of scope. '
|
|
1467
1822
|
+ 'A markdown report is rendered for the workspace (gotry-state/doctor-report.md) so the sidebar workbench can preview it.',
|
|
1468
1823
|
// D-30 第三刀(issue #112):query blob → 平铺 typed;全字段可选 → interpretArgs 容忍层
|
|
1824
|
+
// issue #284 增量:action + items 两参数;默认 diagnose 保持读-only 不变(向后兼容)
|
|
1469
1825
|
parameters: {
|
|
1826
|
+
action: { type: 'string', enum: ['diagnose', 'repair'], description: '默认 diagnose;repair 时按 items 范围补装可选依赖,user-action / unavailable 项永不冒充 auto' },
|
|
1827
|
+
items: { type: 'array', items: { type: 'string', enum: ['node', 'extension', 'agent-reach', 'hbcli', 'flyai', 'sidebar', 'llm-key', 'calendar', 'map-tools', 'ask-user'] }, description: 'repair 时的 doctor item id 范围;空 = 全部 auto-repairable' },
|
|
1470
1828
|
writeReport: { type: 'boolean', description: '默认 true,把 markdown 报告写进 gotry-state/doctor-report.md(侧栏工作台可预览)' },
|
|
1471
1829
|
},
|
|
1472
1830
|
output: {
|
|
@@ -1474,29 +1832,126 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
1474
1832
|
render: (_args, value) => [{ type: 'text', text: String((value as { summary?: string }).summary ?? JSON.stringify(value).slice(0, 900)) }],
|
|
1475
1833
|
},
|
|
1476
1834
|
async execute(args, _exec: unknown) {
|
|
1477
|
-
const q = unwrapQuery<{ writeReport?: boolean }>(args, '
|
|
1478
|
-
const
|
|
1479
|
-
|
|
1835
|
+
const q = unwrapQuery<{ action?: 'diagnose' | 'repair'; items?: string[]; writeReport?: boolean }>(args, 'action')
|
|
1836
|
+
const action = q?.action === 'repair' ? 'repair' : 'diagnose'
|
|
1837
|
+
const check = seams.doctor?.check ?? runDoctorChecks
|
|
1838
|
+
const report = await check({ stateRoot: config.stateRoot ?? '.' })
|
|
1839
|
+
|
|
1840
|
+
// repair 时必须把复检后的报告写盘,避免侧栏仍显示修复前状态。
|
|
1480
1841
|
let reportPath: string | undefined
|
|
1481
|
-
|
|
1842
|
+
const writeReport = async (current: typeof report): Promise<void> => {
|
|
1843
|
+
if (q?.writeReport === false) return
|
|
1482
1844
|
try {
|
|
1483
1845
|
const { writeFile } = await import('node:fs/promises')
|
|
1484
1846
|
const dir = await ensureStateDir(config.stateRoot)
|
|
1485
1847
|
reportPath = join(dir, 'doctor-report.md')
|
|
1486
|
-
await writeFile(reportPath, renderDoctorReportMd(
|
|
1848
|
+
await writeFile(reportPath, renderDoctorReportMd(current), 'utf-8')
|
|
1487
1849
|
} catch { reportPath = undefined }
|
|
1488
1850
|
}
|
|
1489
1851
|
const icon = (s: string) => (s === 'ok' ? '✅' : s === 'degraded' ? '⚠️' : '❌')
|
|
1490
|
-
const
|
|
1491
|
-
|
|
1492
|
-
|
|
1852
|
+
const reportLines = (current: typeof report) => current.items.map(i => `${icon(i.status)} ${i.label}:${i.detail}${i.fix ? `\n ↳ 修复: ${i.fix}` : ''}`)
|
|
1853
|
+
|
|
1854
|
+
if (action === 'diagnose') {
|
|
1855
|
+
await writeReport(report)
|
|
1856
|
+
const diagSummary = `${report.summary}\n${reportLines(report).join('\n')}${reportPath ? `\n报告已写: ${reportPath}(侧栏工作台可直接预览)` : '\n(报告写盘失败,仅本对话展示)'}`
|
|
1857
|
+
// 读-only 路径:保持向后兼容,模型拿到的 verdict/items 与 issue #284 之前一致
|
|
1858
|
+
return JSON.parse(JSON.stringify({
|
|
1859
|
+
ok: true,
|
|
1860
|
+
action: 'diagnose',
|
|
1861
|
+
verdict: report.ok ? 'all-clear' : 'needs-attention',
|
|
1862
|
+
items: report.items,
|
|
1863
|
+
report_path: reportPath,
|
|
1864
|
+
evidence: `[doctor@${new Date().toISOString()}]`,
|
|
1865
|
+
summary: diagSummary,
|
|
1866
|
+
})) as Record<string, never>
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
// action='repair':从诊断 → 选定 auto-repairable → scope-keyed 审批 → 真执行(走 bootstrap.runOnboardingFix)→ 复检。
|
|
1870
|
+
// 范围过滤:items 非空时只考虑这些 id(未知 id 静默剔除,落 skipped 不冒充 auto)。
|
|
1871
|
+
// runDoctorRepair 内部懒加载 bin/gotry-bootstrap.js,自适应 status→level 适配 + 真实安装器。
|
|
1872
|
+
const requestedItems = Array.isArray(q?.items) && q!.items!.length > 0 ? new Set(q!.items!) : null
|
|
1873
|
+
const scopedReport = requestedItems
|
|
1874
|
+
? { ...report, items: report.items.filter((i) => requestedItems.has(i.id)) }
|
|
1875
|
+
: report
|
|
1876
|
+
const exec = _exec as { agent?: object; callId?: string; signal?: AbortSignal } | undefined
|
|
1877
|
+
// 生产 repair 让 bootstrap 安装编排复用本工具的同一诊断注册表做终态复检;
|
|
1878
|
+
// fixture 若显式提供 recheck,则保留其隔离实现,再单独取一次工具报告。
|
|
1879
|
+
let recheck: typeof report | undefined
|
|
1880
|
+
const injectedRepair = seams.doctor?.repair
|
|
1881
|
+
const repairOptions: DoctorRepairOptions = {
|
|
1882
|
+
...injectedRepair,
|
|
1883
|
+
recheck: injectedRepair?.recheck ?? (async () => {
|
|
1884
|
+
recheck = await check({ stateRoot: config.stateRoot ?? '.' })
|
|
1885
|
+
return recheck.items.map((item) => ({
|
|
1886
|
+
label: item.label,
|
|
1887
|
+
level: item.status,
|
|
1888
|
+
detail: item.detail,
|
|
1889
|
+
fix: item.fix,
|
|
1890
|
+
}))
|
|
1891
|
+
}),
|
|
1892
|
+
}
|
|
1893
|
+
const repair = await runDoctorRepair(
|
|
1894
|
+
scopedReport,
|
|
1895
|
+
(scopeKey, reason) => repairApprovalGate.request(exec?.agent, scopeKey, reason, { callId: exec?.callId, signal: exec?.signal }),
|
|
1896
|
+
repairOptions,
|
|
1897
|
+
)
|
|
1898
|
+
if (repair.approval === 'granted' && !recheck) {
|
|
1899
|
+
recheck = await check({ stateRoot: config.stateRoot ?? '.' })
|
|
1900
|
+
}
|
|
1901
|
+
const finalReport = recheck ?? report
|
|
1902
|
+
await writeReport(finalReport)
|
|
1903
|
+
const diagSummary = `${report.summary}\n${reportLines(report).join('\n')}`
|
|
1904
|
+
// 整合诊断 + 修复双视角 summary(模型可分清哪段是体检、哪段是修复)
|
|
1905
|
+
const repairLines: string[] = []
|
|
1906
|
+
for (const r of repair.repairs) {
|
|
1907
|
+
repairLines.push(`${r.status === 'installed' ? '✅' : '❌'} ${r.label}: ${r.reason}${r.nextAction ? ` → ${r.nextAction}` : ''}`)
|
|
1908
|
+
}
|
|
1909
|
+
for (const s of repair.skipped) {
|
|
1910
|
+
repairLines.push(`⏭ ${s.label}: ${s.reason}(bucket=${s.bucket})`)
|
|
1911
|
+
}
|
|
1912
|
+
const recheckSummary = recheck
|
|
1913
|
+
? `\n\n[recheck] ${recheck.summary}\n${reportLines(recheck).join('\n')}`
|
|
1914
|
+
: ''
|
|
1915
|
+
const reportSummary = reportPath ? `\n报告已写: ${reportPath}(侧栏工作台可直接预览)` : '\n(报告写盘失败,仅本对话展示)'
|
|
1916
|
+
const combinedSummary = `${diagSummary}\n\n[repair] verdict=${repair.verdict}, approval=${repair.approval}, plan=${repair.selected.length} 项 auto / ${repair.skipped.length} 项 skipped\n${repairLines.join('\n')}\n${repair.summary}${recheckSummary}${reportSummary}`
|
|
1917
|
+
return JSON.parse(JSON.stringify({
|
|
1918
|
+
ok: repair.ok,
|
|
1919
|
+
action: 'repair',
|
|
1920
|
+
verdict: repair.verdict,
|
|
1921
|
+
diagnosis: report,
|
|
1922
|
+
recheck,
|
|
1923
|
+
plan: {
|
|
1924
|
+
selected: repair.selected,
|
|
1925
|
+
needs_user_action: repair.skipped.filter((s) => s.bucket === 'user-action').map((s) => ({ id: s.id, label: s.label, reason: s.reason })),
|
|
1926
|
+
unavailable: repair.skipped.filter((s) => s.bucket === 'unavailable').map((s) => ({ id: s.id, label: s.label, reason: s.reason })),
|
|
1927
|
+
scope_key: repair.scopeKey,
|
|
1928
|
+
},
|
|
1929
|
+
approval: repair.approval,
|
|
1930
|
+
repairs: repair.repairs,
|
|
1931
|
+
skipped: repair.skipped,
|
|
1932
|
+
evidence: `[doctor@${new Date().toISOString()}]`,
|
|
1933
|
+
summary: combinedSummary,
|
|
1934
|
+
report_path: reportPath,
|
|
1935
|
+
})) as Record<string, never>
|
|
1936
|
+
},
|
|
1937
|
+
presentCall: args => {
|
|
1938
|
+
const q = unwrapQuery<{ action?: string }>(args, 'action')
|
|
1939
|
+
const a = q?.action === 'repair'
|
|
1940
|
+
return { card: 'generic', title: a ? '🩺 依赖体检+修复(待批准)' : '🩺 依赖体检', kind: 'execute', rawInput: args }
|
|
1493
1941
|
},
|
|
1494
|
-
presentCall: _args => ({ card: 'generic', title: '🩺 依赖体检', kind: 'execute', rawInput: {} }),
|
|
1495
1942
|
presentResult: (_args, value) => {
|
|
1496
|
-
const r = value as { verdict?: string; summary?: string }
|
|
1943
|
+
const r = value as { verdict?: string; summary?: string; action?: string; approval?: string }
|
|
1944
|
+
const deniedLabel = r.approval === 'cancelled'
|
|
1945
|
+
? '↩️ 已取消'
|
|
1946
|
+
: r.approval === 'unavailable'
|
|
1947
|
+
? '⚠️ 无审批通道'
|
|
1948
|
+
: '🚫 已拒绝'
|
|
1949
|
+
const title = r.action === 'repair'
|
|
1950
|
+
? `🩺 修复:${r.verdict === 'repaired' ? '✅ 已就位' : r.verdict === 'partial-repair' ? '⚠️ 部分成功' : r.verdict === 'repair-blocked' ? '❌ 复检未通过' : r.verdict === 'approval-denied' ? deniedLabel : '🔧 无可补装项'}`
|
|
1951
|
+
: `🩺 依赖体检:${r.verdict === 'all-clear' ? '✅ 全部就绪' : '🔧 有项待处理'}`
|
|
1497
1952
|
return {
|
|
1498
1953
|
card: 'generic',
|
|
1499
|
-
title
|
|
1954
|
+
title,
|
|
1500
1955
|
content: [{ type: 'text', text: String(r.summary ?? '') }],
|
|
1501
1956
|
}
|
|
1502
1957
|
},
|
|
@@ -1518,10 +1973,22 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
1518
1973
|
output: {
|
|
1519
1974
|
schema: { type: 'json' },
|
|
1520
1975
|
render: (_args, value) => [{ type: 'text', text: String((value as { summary?: string }).summary ?? JSON.stringify(value).slice(0, 600)) }],
|
|
1976
|
+
presentationMeta: (_args, value) => {
|
|
1977
|
+
const r = value as { artifacts?: Array<{ path?: string }>; total?: number; truncated?: boolean }
|
|
1978
|
+
const paths = Array.isArray(r.artifacts)
|
|
1979
|
+
? r.artifacts.map(a => String(a.path ?? '')).filter(Boolean)
|
|
1980
|
+
: []
|
|
1981
|
+
return {
|
|
1982
|
+
shape: 'paths',
|
|
1983
|
+
paths,
|
|
1984
|
+
truncated: Boolean(r.truncated),
|
|
1985
|
+
total: r.total ?? paths.length,
|
|
1986
|
+
}
|
|
1987
|
+
},
|
|
1521
1988
|
},
|
|
1522
|
-
async execute(args,
|
|
1989
|
+
async execute(args, exec: unknown) {
|
|
1523
1990
|
const q = unwrapQuery<{ limit?: number }>(args, 'limit')
|
|
1524
|
-
const r = await listArtifacts({ stateRoot: config.stateRoot ?? '.', limit: q.limit })
|
|
1991
|
+
const r = await listArtifacts({ stateRoot: config.stateRoot ?? '.', cwd: sessionCwd(exec), limit: q.limit })
|
|
1525
1992
|
const lines = r.artifacts.map(a =>
|
|
1526
1993
|
`- [${a.source}] ${a.title}${a.status ? `(${a.status})` : ''} — ${a.path}${a.updated ? ` @ ${a.updated.slice(0, 16).replace('T', ' ')}` : ''}`)
|
|
1527
1994
|
const summary = r.artifacts.length
|
|
@@ -1529,13 +1996,23 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
1529
1996
|
: '无在册产物(异步深度规划交付与工作目录 md 文件都会出现在这里)'
|
|
1530
1997
|
return JSON.parse(JSON.stringify({ ok: true, artifacts: r.artifacts, total: r.total, truncated: r.truncated, summary })) as Record<string, never>
|
|
1531
1998
|
},
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1999
|
+
// Host presentationMeta remains persisted with the ToolResult for Host-side
|
|
2000
|
+
// consumers; the packaged public Client separately renders the runtime block
|
|
2001
|
+
// for these custom wire names in DSH Web.
|
|
2002
|
+
presentResult: (_args, result: ToolResult) => {
|
|
2003
|
+
if (result.isError || typeof result.meta !== 'object' || result.meta === null || Array.isArray(result.meta)) return undefined
|
|
2004
|
+
const meta = result.meta as { shape?: unknown; paths?: unknown; truncated?: unknown; total?: unknown }
|
|
2005
|
+
if (meta.shape !== 'paths' || !Array.isArray(meta.paths) || !meta.paths.every(p => typeof p === 'string') ||
|
|
2006
|
+
typeof meta.truncated !== 'boolean' || typeof meta.total !== 'number' || !Number.isInteger(meta.total) || meta.total < 0) {
|
|
2007
|
+
return undefined
|
|
2008
|
+
}
|
|
1535
2009
|
return {
|
|
1536
|
-
card: '
|
|
1537
|
-
|
|
1538
|
-
|
|
2010
|
+
card: 'search' as const,
|
|
2011
|
+
shape: 'paths' as const,
|
|
2012
|
+
title: `产物:${meta.total} 项在册${meta.truncated ? '(已截断)' : ''}`,
|
|
2013
|
+
paths: meta.paths,
|
|
2014
|
+
truncated: meta.truncated,
|
|
2015
|
+
total: meta.total,
|
|
1539
2016
|
}
|
|
1540
2017
|
},
|
|
1541
2018
|
}))
|
|
@@ -1556,34 +2033,71 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
1556
2033
|
output: {
|
|
1557
2034
|
schema: { type: 'json' },
|
|
1558
2035
|
render: (_args, value) => [{ type: 'text', text: String((value as { content?: string }).content ?? JSON.stringify(value).slice(0, 600)) }],
|
|
2036
|
+
presentationMeta: (_args, value) => {
|
|
2037
|
+
const r = value as { path?: string; offset?: number; lines?: Array<{ number: number; text: string }>; totalLines?: number; lang?: string; source?: string; version?: string }
|
|
2038
|
+
return {
|
|
2039
|
+
path: String(r.path ?? ''),
|
|
2040
|
+
offset: r.offset ?? 1,
|
|
2041
|
+
lines: Array.isArray(r.lines) ? r.lines.map(line => ({ number: line.number, text: line.text })) : [],
|
|
2042
|
+
totalLines: r.totalLines ?? 0,
|
|
2043
|
+
...(r.lang ? { lang: r.lang } : {}),
|
|
2044
|
+
...(r.source ? { source: r.source } : {}),
|
|
2045
|
+
...(r.version ? { version: r.version } : {}),
|
|
2046
|
+
}
|
|
2047
|
+
},
|
|
1559
2048
|
},
|
|
1560
|
-
async execute(args,
|
|
2049
|
+
async execute(args, exec) {
|
|
1561
2050
|
const q = args
|
|
1562
2051
|
if (!q.path) throw new Error('gotry_artifacts_read requires path')
|
|
1563
2052
|
if (!q.path) return JSON.parse(JSON.stringify({ ok: false, error: 'path 必填(来自 gotry_artifacts_list)' })) as Record<string, never>
|
|
1564
|
-
const r = await readArtifact({ stateRoot: config.stateRoot ?? '.', path: q.path, offset: q.offset, limit: q.limit })
|
|
2053
|
+
const r = await readArtifact({ stateRoot: config.stateRoot ?? '.', cwd: sessionCwd(exec), path: q.path, offset: q.offset, limit: q.limit })
|
|
1565
2054
|
if (!r.ok) return JSON.parse(JSON.stringify(r)) as Record<string, never>
|
|
2055
|
+
// 身份/来源展示(issue #285 第 1 条「显示身份/来源」):行号视图保留 read 卡的
|
|
2056
|
+
// path/offset/lines/totalLines/lang;fallback content 用 source label 前缀,
|
|
2057
|
+
// 让无法渲染 read 卡的 UI 也能区分「本会话选了哪个产物」,避免把旧摘要当新内容。
|
|
1566
2058
|
return JSON.parse(JSON.stringify({
|
|
1567
2059
|
...r,
|
|
2060
|
+
source: r.source,
|
|
1568
2061
|
summary: `${r.path}(${r.totalLines} 行)第 ${r.offset}-${r.offset + r.lines.length - 1} 行${r.windowed ? `(共 ${r.totalLines} 行,可翻页)` : ''}`,
|
|
1569
2062
|
})) as Record<string, never>
|
|
1570
2063
|
},
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
2064
|
+
// Keep the standard location payload for Host consumers. The Web client only
|
|
2065
|
+
// renders its built-in keyed tools, so this is not claimed as native custom-card E2E.
|
|
2066
|
+
presentCall: args => ({
|
|
2067
|
+
card: 'generic',
|
|
2068
|
+
title: `读产物:${args.path ?? ''}`,
|
|
2069
|
+
kind: 'read',
|
|
2070
|
+
rawInput: args,
|
|
2071
|
+
...(args.path ? { locations: [{ path: String(args.path), line: 1 }] } : {}),
|
|
2072
|
+
}),
|
|
2073
|
+
presentResult: (_args, result: ToolResult) => {
|
|
2074
|
+
if (result.isError || typeof result.meta !== 'object' || result.meta === null || Array.isArray(result.meta)) return undefined
|
|
2075
|
+
const meta = result.meta as { path?: unknown; source?: unknown; offset?: unknown; lines?: unknown; totalLines?: unknown; lang?: unknown }
|
|
2076
|
+
if (typeof meta.path !== 'string' || meta.path === '' || typeof meta.offset !== 'number' || !Number.isInteger(meta.offset) || meta.offset < 1 ||
|
|
2077
|
+
!Array.isArray(meta.lines) || !meta.lines.every(line => typeof line === 'object' && line !== null && !Array.isArray(line) &&
|
|
2078
|
+
typeof (line as { number?: unknown }).number === 'number' && Number.isInteger((line as { number: number }).number) &&
|
|
2079
|
+
typeof (line as { text?: unknown }).text === 'string') ||
|
|
2080
|
+
typeof meta.totalLines !== 'number' || !Number.isInteger(meta.totalLines) || meta.totalLines < 0) {
|
|
2081
|
+
return undefined
|
|
1576
2082
|
}
|
|
1577
|
-
|
|
2083
|
+
const source = typeof meta.source === 'string' ? meta.source : 'unknown'
|
|
2084
|
+
const identityLine = `source: ${source} · path: ${meta.path} · ${meta.totalLines} lines`
|
|
2085
|
+
const rawContent = result.content
|
|
2086
|
+
.filter(block => block.type === 'text')
|
|
2087
|
+
.map(block => block.text)
|
|
2088
|
+
.join('\n')
|
|
1578
2089
|
return {
|
|
1579
2090
|
card: 'read' as const,
|
|
1580
|
-
title:
|
|
1581
|
-
path:
|
|
1582
|
-
offset:
|
|
1583
|
-
lines:
|
|
1584
|
-
totalLines:
|
|
1585
|
-
lang:
|
|
1586
|
-
content: [
|
|
2091
|
+
title: meta.path.split('/').pop() ?? meta.path,
|
|
2092
|
+
path: meta.path,
|
|
2093
|
+
offset: meta.offset,
|
|
2094
|
+
lines: meta.lines as Array<{ number: number; text: string }>,
|
|
2095
|
+
totalLines: meta.totalLines,
|
|
2096
|
+
...(typeof meta.lang === 'string' ? { lang: meta.lang } : {}),
|
|
2097
|
+
content: [
|
|
2098
|
+
{ type: 'text', text: identityLine },
|
|
2099
|
+
{ type: 'text', text: rawContent },
|
|
2100
|
+
],
|
|
1587
2101
|
}
|
|
1588
2102
|
},
|
|
1589
2103
|
}))
|
|
@@ -1616,7 +2130,7 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
1616
2130
|
schema: { type: 'json' },
|
|
1617
2131
|
render: (_args, value) => [{ type: 'text', text: String((value as { summary?: string }).summary ?? JSON.stringify(value).slice(0, 800)) }],
|
|
1618
2132
|
},
|
|
1619
|
-
async execute(args,
|
|
2133
|
+
async execute(args, exec: unknown) {
|
|
1620
2134
|
const q = unwrapQuery<{ markdown?: string; path?: string; tripYear?: number; itinerary?: Record<string, unknown> }>(args, 'markdown')
|
|
1621
2135
|
const avMap = await loadAirlineAirportMap()
|
|
1622
2136
|
if (!avMap) {
|
|
@@ -1624,14 +2138,19 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
1624
2138
|
}
|
|
1625
2139
|
let markdown = q.markdown
|
|
1626
2140
|
if (!markdown && q.path) {
|
|
1627
|
-
const r = await readArtifact({ stateRoot: config.stateRoot ?? '.', path: q.path })
|
|
2141
|
+
const r = await readArtifact({ stateRoot: config.stateRoot ?? '.', cwd: sessionCwd(exec), path: q.path })
|
|
1628
2142
|
if (!r.ok) return JSON.parse(JSON.stringify({ ok: false, summary: `产物读取失败:${String((r as { error?: string }).error ?? '')}` })) as Record<string, never>
|
|
1629
2143
|
markdown = r.content
|
|
1630
2144
|
}
|
|
1631
2145
|
if (!markdown) return JSON.parse(JSON.stringify({ ok: false, summary: '需要 markdown(产物全文)或 path(产物路径)' })) as Record<string, never>
|
|
1632
2146
|
const itinerary = q.itinerary as import('./bookable-facts.ts').ItineraryFacts | undefined
|
|
1633
2147
|
const registry = await loadFactRegistry(config.stateRoot ?? '.')
|
|
1634
|
-
|
|
2148
|
+
// 政策复核提醒桥接(issue #359):闸侧 opts.tripStart 与 renderer 同一传入
|
|
2149
|
+
// 才能让合法 `renderPolicyFact(f, tripStart)` 行通过公开闸。已加载的
|
|
2150
|
+
// itinerary.trip_start 是权威源(同次闸同时驱动 itineraryInvariants),不再
|
|
2151
|
+
// 额外要求调用方传 opts.tripStart;空 itinerary/无 trip_start 即无提醒。
|
|
2152
|
+
const tripStart = typeof itinerary?.trip_start === 'string' ? itinerary.trip_start : undefined
|
|
2153
|
+
const report = gateArtifact(markdown, registry, avMap, { trip_year: q.tripYear, itinerary, tripStart })
|
|
1635
2154
|
const lines = report.violations.slice(0, 20).map(v => ` L${v.line} [${v.kind}] ${v.detail}`)
|
|
1636
2155
|
const summary = report.verdict === 'pass'
|
|
1637
2156
|
? `事实闸 PASS:${report.traceable}/${report.claims_checked} 可下单 claim 全部回溯到 exact-date 工具结果(query_id 可重放)——可宣称「已验证方案」。`
|