@danceiny/gotry 0.0.1-rc.21 → 0.0.1-rc.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +102 -194
- package/README.zh-CN.md +115 -210
- package/bin/gotry-backend.js +53 -0
- package/bin/gotry-bootstrap.js +466 -35
- package/bin/gotry-inner.js +255 -36
- package/bin/gotry-process-liveness.d.ts +27 -0
- package/bin/gotry-process-liveness.js +97 -0
- package/bin/gotry-runtime-resolution.d.ts +1 -1
- package/bin/gotry-runtime-resolution.js +2 -2
- package/client/client.js +174 -0
- package/cordis.gotry-patch.yml +70 -93
- package/dist/capabilities/agent-reach-deep.js +45 -122
- package/dist/capabilities/agent-reach.js +63 -148
- package/dist/capabilities/anything.js +89 -75
- package/dist/capabilities/artifacts.js +123 -104
- package/dist/capabilities/channel-health.js +65 -45
- package/dist/capabilities/channel-registry.js +108 -87
- package/dist/capabilities/doctor.js +357 -235
- package/dist/capabilities/effect.js +223 -309
- package/dist/capabilities/fact-log.js +28 -14
- package/dist/capabilities/flyai.js +245 -156
- package/dist/capabilities/ground-transfer.js +509 -0
- package/dist/capabilities/hbcli.js +144 -148
- package/dist/capabilities/hotelbyte-transaction.js +194 -0
- package/dist/capabilities/incident-log.js +131 -61
- package/dist/capabilities/model-override.js +30 -10
- package/dist/capabilities/opensky.js +54 -43
- package/dist/capabilities/resilience.js +50 -32
- package/dist/capabilities/session/action-cache.js +50 -44
- package/dist/capabilities/session/adapters/ctrip-flight.js +119 -62
- package/dist/capabilities/session/adapters/ctrip-hotel.js +125 -110
- package/dist/capabilities/session/adapters/dida-portal.js +206 -0
- package/dist/capabilities/session/adapters/meituan-local.js +35 -54
- package/dist/capabilities/session/adapters/rail-12306.js +169 -112
- package/dist/capabilities/session/benchmark.js +85 -149
- package/dist/capabilities/session/extension-bridge.js +320 -259
- package/dist/capabilities/session/extension-channel.js +57 -76
- package/dist/capabilities/session/extension-distribution.js +115 -119
- package/dist/capabilities/session/extract.js +31 -21
- package/dist/capabilities/session/golden-score.js +55 -33
- package/dist/capabilities/session/health-watch.js +146 -154
- package/dist/capabilities/session/read-guard.js +55 -47
- package/dist/capabilities/session/static-flight-golden.js +49 -36
- package/dist/capabilities/session/transport.js +54 -61
- package/dist/capabilities/session/wizard.js +63 -93
- package/dist/capabilities/session-consent.js +143 -47
- package/dist/capabilities/session-login.js +95 -126
- package/dist/capabilities/session-search.js +391 -253
- package/dist/capabilities/sponsor-plugin.js +341 -0
- package/dist/capabilities/visa-policy.js +87 -73
- package/dist/capabilities/weather.js +163 -235
- package/dist/scripts/action-cache-tests.js +24 -59
- package/dist/scripts/agent-planning-turn-deadline-e2e.js +91 -142
- package/dist/scripts/agent-planning-turn-deadline-tests.js +80 -176
- package/dist/scripts/agent-reach-deep-tests.js +30 -40
- package/dist/scripts/agent-reach-tests.js +26 -18
- package/dist/scripts/agent-reach-wrapper-tests.js +46 -50
- package/dist/scripts/anything-tests.js +58 -62
- package/dist/scripts/artifact-client-contract-tests.js +69 -0
- package/dist/scripts/artifacts-capability-tests.js +235 -0
- package/dist/scripts/async-collect.js +26 -15
- package/dist/scripts/benchmark-environment-bridge-e2e.js +287 -684
- package/dist/scripts/benchmark-environment-bridge-tests.js +818 -3123
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +66 -279
- package/dist/scripts/booking-copilot-availability-policy-tests.js +153 -1257
- package/dist/scripts/booking-copilot-bin-proof-tests.js +22 -45
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +64 -76
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +69 -199
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +393 -759
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +33 -75
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +60 -110
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +28 -66
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +72 -271
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +73 -275
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +784 -3398
- package/dist/scripts/booking-copilot-server-proof-tests.js +63 -168
- package/dist/scripts/booking-copilot-startup-proof-tests.js +38 -124
- package/dist/scripts/booking-executor-tests.js +80 -0
- package/dist/scripts/booking-recovery-chain-tests.js +182 -0
- package/dist/scripts/booking-saga-tests.js +61 -123
- package/dist/scripts/booking-surface-contract-proof-tests.js +152 -1167
- package/dist/scripts/bootstrap-tests.js +1383 -128
- package/dist/scripts/build-changelog.js +89 -130
- package/dist/scripts/build-metrics-report.js +135 -143
- package/dist/scripts/changelog-tests.js +35 -82
- package/dist/scripts/channel-probe-tests.js +44 -116
- package/dist/scripts/channel-probe.js +84 -167
- package/dist/scripts/channel-registry-tests.js +97 -185
- package/dist/scripts/companion-tests.js +25 -70
- package/dist/scripts/dida-cookie-probe.js +87 -0
- package/dist/scripts/dida-sw-probe.js +114 -0
- package/dist/scripts/diff-test.js +17 -12
- package/dist/scripts/doctor-tests.js +428 -120
- package/dist/scripts/dsh-artifact-e2e.js +150 -0
- package/dist/scripts/dsh-artifact-web-e2e.js +443 -0
- package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
- package/dist/scripts/dsh-runtime-closure-tests.js +110 -153
- package/dist/scripts/dsh-runtime-closure.js +75 -63
- package/dist/scripts/dsh-session-v3-migration-proof.js +106 -0
- package/dist/scripts/dsh-subprocess-local-proof.js +149 -0
- package/dist/scripts/dsh-target-closure-proof.js +178 -0
- package/dist/scripts/effect-tests.js +165 -552
- package/dist/scripts/engine-run.js +1 -3
- package/dist/scripts/engine-tests.js +16 -10
- package/dist/scripts/evaluation-cadence-tests.js +69 -242
- package/dist/scripts/evaluation-contract-tests.js +193 -499
- package/dist/scripts/extension-distribution-cli.js +19 -15
- package/dist/scripts/extension-distribution-tests.js +98 -211
- package/dist/scripts/extension-tests.js +366 -389
- package/dist/scripts/fact-gate-tests.js +725 -263
- package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
- package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
- package/dist/scripts/flight-malformed-tests.js +159 -0
- package/dist/scripts/flyai-tests.js +325 -68
- package/dist/scripts/g5-guard-tests.js +131 -0
- package/dist/scripts/g5-guard.js +189 -0
- package/dist/scripts/gen-lunar-anchors.js +106 -0
- package/dist/scripts/gotry-backend-kernel-tests.js +79 -0
- package/dist/scripts/ground-transfer-tests.js +956 -0
- package/dist/scripts/hbcli-e2e-tests.js +71 -90
- package/dist/scripts/hbcli-live-optin-tests.js +96 -0
- package/dist/scripts/hbcli-release-contract-tests.js +122 -0
- package/dist/scripts/hbcli-tests.js +41 -115
- package/dist/scripts/health-watch-cli.js +42 -28
- package/dist/scripts/hotel-date-gate-tests.js +496 -0
- package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
- package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
- package/dist/scripts/i18n-tests.js +16 -9
- package/dist/scripts/incident-tests.js +281 -87
- package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
- package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
- package/dist/scripts/issue-271-liveness-tests.js +547 -0
- package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
- package/dist/scripts/issue-289-model-retry-tests.js +441 -0
- package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
- package/dist/scripts/issue-338-home-city-e2e.js +577 -0
- package/dist/scripts/issue343-real-entry-e2e.js +160 -0
- package/dist/scripts/journey-tests.js +22 -27
- package/dist/scripts/kernel-manifest-evidence.js +109 -0
- package/dist/scripts/kernel-manifest-gate.js +126 -0
- package/dist/scripts/kernel-manifest-tests.js +212 -0
- package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
- package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
- package/dist/scripts/kernel-manifest.js +369 -0
- package/dist/scripts/ledger-repair-apply-tests.js +316 -0
- package/dist/scripts/ledger-repair-plan-tests.js +375 -0
- package/dist/scripts/ledger-tests.js +325 -340
- package/dist/scripts/ledger-workflow-crash.js +10 -20
- package/dist/scripts/map-tools-vendor-package-proof.js +206 -134
- package/dist/scripts/memory-capture-tests.js +242 -61
- package/dist/scripts/memory-decay-tests.js +18 -52
- package/dist/scripts/memory-lifecycle-tests.js +657 -0
- package/dist/scripts/memory-lifecycle.js +196 -0
- package/dist/scripts/memory-metrics.js +24 -12
- package/dist/scripts/memory-value-report.js +474 -222
- package/dist/scripts/memory-value-tests.js +328 -0
- package/dist/scripts/metrics-report-tests.js +62 -199
- package/dist/scripts/model-override-e2e.js +66 -105
- package/dist/scripts/nightly-evidence-tests.js +64 -89
- package/dist/scripts/nightly-evidence.js +108 -111
- package/dist/scripts/nudge-digest.js +39 -28
- package/dist/scripts/onboarding-tests.js +63 -88
- package/dist/scripts/opensky-check.js +18 -13
- package/dist/scripts/opensky-tests.js +28 -28
- package/dist/scripts/persona-surface-guard-tests.js +37 -12
- package/dist/scripts/pnpm-dsh-closure-proof.js +2 -5
- package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
- package/dist/scripts/price-drift-tests.js +126 -227
- package/dist/scripts/price-drift-watch.js +209 -253
- package/dist/scripts/probe-poi-tests.js +52 -45
- package/dist/scripts/product-metrics.js +187 -273
- package/dist/scripts/publish-preverify.js +47 -41
- package/dist/scripts/realtime-pricing-tests.js +37 -76
- package/dist/scripts/replay-async.js +20 -18
- package/dist/scripts/replay-real.js +16 -18
- package/dist/scripts/replay.js +52 -82
- package/dist/scripts/session-attach-diagnose.js +16 -12
- package/dist/scripts/session-attach-poc.js +34 -37
- package/dist/scripts/session-benchmark.js +66 -147
- package/dist/scripts/session-dida-live-e2e.js +151 -0
- package/dist/scripts/session-extract-tests.js +14 -35
- package/dist/scripts/session-login.js +14 -13
- package/dist/scripts/session-tests.js +545 -583
- package/dist/scripts/sf-live-benchmark.js +92 -110
- package/dist/scripts/sf-live-cli-tests.js +2 -9
- package/dist/scripts/sf-soft-score-tests.js +19 -60
- package/dist/scripts/sf-summary-tests.js +149 -0
- package/dist/scripts/sf-summary.js +466 -84
- package/dist/scripts/skeleton-check.js +16 -14
- package/dist/scripts/skeleton-integration-test.js +11 -10
- package/dist/scripts/skills-contract-tests.js +51 -46
- package/dist/scripts/smoke-session-gate-tests.js +12 -20
- package/dist/scripts/smoke.js +541 -422
- package/dist/scripts/sponsor-reuse-tests.js +262 -0
- package/dist/scripts/state-cli-tests.js +226 -101
- package/dist/scripts/state-cli.js +549 -212
- package/dist/scripts/static-golden-tests.js +46 -180
- package/dist/scripts/time-eval-tests.js +210 -192
- package/dist/scripts/travel-timeline-tests.js +29 -93
- package/dist/scripts/turn-handoff-collect-tests.js +46 -93
- package/dist/scripts/turn-handoff-collect.js +71 -99
- package/dist/scripts/turn-policy-tests.js +28 -29
- package/dist/scripts/typed-contract-canary.js +77 -162
- package/dist/scripts/tz-resolver-sanity.js +50 -0
- package/dist/scripts/tz-resolver-tests.js +584 -0
- package/dist/scripts/unified-tests.js +23 -19
- package/dist/scripts/visa-policy-tests.js +31 -44
- package/dist/scripts/weather-tests.js +121 -543
- package/dist/scripts/wish-channel-gate-tests.js +29 -86
- package/dist/scripts/write-gate-tests.js +522 -0
- package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
- package/dist/scripts/z3-lifecycle-tests.js +170 -0
- package/dist/scripts/z3-race-repeat-tests.js +39 -0
- package/dist/scripts/z3-race-tests.js +18 -12
- package/dist/src/artifact-gate.js +652 -221
- package/dist/src/backend/kernel.js +91 -0
- package/dist/src/backend/modules/booking-copilot.js +55 -0
- package/dist/src/backend/modules/booking-executor.js +124 -0
- package/dist/src/backend/modules/session-search.js +194 -0
- package/dist/src/benchmark-agent-conformance.js +296 -262
- package/dist/src/benchmark-environment-bridge.js +298 -413
- package/dist/src/benchmark-headless-child-diagnostics.js +93 -100
- package/dist/src/benchmark-tool-isolation.js +94 -67
- package/dist/src/bookable-facts.js +299 -174
- package/dist/src/booking-saga.js +64 -120
- package/dist/src/booking-surface/availability-policy.js +443 -576
- package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
- package/dist/src/booking-surface/contracts.js +24 -80
- package/dist/src/booking-surface/dsh-planner.js +510 -281
- package/dist/src/booking-surface/dsh-plugin.js +17 -1
- package/dist/src/booking-surface/error-codes.js +82 -10
- package/dist/src/booking-surface/index.js +0 -3
- package/dist/src/booking-surface/profile.js +10 -54
- package/dist/src/booking-surface/recovery-chain.js +367 -0
- package/dist/src/booking-surface/runtime.js +765 -891
- package/dist/src/booking-surface/server.js +200 -251
- package/dist/src/booking-surface/startup.js +58 -54
- package/dist/src/booking-surface/validation.js +150 -218
- package/dist/src/bridge.js +20 -13
- package/dist/src/companions.js +48 -74
- package/dist/src/contracts.js +21 -43
- package/dist/src/dsh-llm.js +82 -120
- package/dist/src/engine.js +253 -239
- package/dist/src/evaluation-cadence.js +72 -209
- package/dist/src/evaluation-contracts.js +220 -843
- package/dist/src/flight-pack-adapter.js +54 -0
- package/dist/src/flight-pack-contract.js +2 -0
- package/dist/src/gotry-backend.js +70 -0
- package/dist/src/hotel-date-gate.js +118 -0
- package/dist/src/i18n.js +18 -7
- package/dist/src/index.js +1425 -1629
- package/dist/src/journey.js +132 -81
- package/dist/src/ledger-repair-apply.js +538 -0
- package/dist/src/ledger-repair-plan.js +463 -0
- package/dist/src/loop.js +257 -166
- package/dist/src/memory-capture.js +180 -32
- package/dist/src/memory-decay.js +30 -14
- package/dist/src/memory-lifecycle.js +1310 -0
- package/dist/src/memory-utility.js +27 -40
- package/dist/src/mock-llm.js +36 -61
- package/dist/src/model.js +81 -36
- package/dist/src/realtime-pricing.js +63 -72
- package/dist/src/slot-spec.js +84 -106
- package/dist/src/state-ledger.js +310 -471
- package/dist/src/subagent-job-id-guard.js +58 -0
- package/dist/src/time-anchor.js +231 -33
- package/dist/src/tool-packet.js +20 -8
- package/dist/src/travel-slots.js +58 -35
- package/dist/src/travel-timeline.js +44 -63
- package/dist/src/turn-deadline.js +121 -127
- package/dist/src/turn-policy.js +61 -89
- package/dist/src/tz-resolver.js +143 -0
- package/dist/src/unified.js +504 -381
- package/dist/src/wish-pool.js +39 -14
- package/dist/src/write-gate.js +495 -0
- package/dist/src/z3-shared.js +273 -7
- package/extension/README.md +4 -3
- package/extension/background.js +125 -14
- package/extension/content-bridge.js +23 -1
- package/extension/content-main.js +15 -4
- package/extension/manifest.json +16 -8
- package/package.json +251 -220
- package/ts/capabilities/agent-reach.ts +4 -4
- package/ts/capabilities/anything.ts +1 -1
- package/ts/capabilities/artifacts.ts +46 -9
- package/ts/capabilities/flyai.ts +156 -37
- package/ts/capabilities/hbcli.ts +1 -1
- package/ts/capabilities/incident-log.ts +88 -59
- package/ts/capabilities/session/adapters/ctrip-flight.ts +99 -36
- package/ts/capabilities/session/adapters/dida-portal.ts +250 -0
- package/ts/capabilities/session/adapters/rail-12306.ts +123 -24
- package/ts/capabilities/session/extension-bridge.ts +226 -101
- package/ts/capabilities/session/extension-channel.ts +22 -9
- package/ts/capabilities/session/wizard.ts +2 -2
- package/ts/capabilities/session-consent.ts +98 -14
- package/ts/capabilities/session-login.ts +1 -0
- package/ts/capabilities/session-search.ts +315 -31
- package/ts/dsh-runtime/vendor/README.md +8 -4
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +7 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +5 -3
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +1 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +1 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +6 -6
- package/ts/package.json +236 -16
- package/ts/scripts/map-tools-vendor-package-proof.ts +152 -31
- package/ts/scripts/state-cli.ts +418 -51
- package/ts/src/artifact-gate.ts +561 -15
- package/ts/src/benchmark-environment-bridge.ts +37 -20
- package/ts/src/bookable-facts.ts +183 -7
- package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
- package/ts/src/booking-surface/dsh-planner.ts +240 -32
- package/ts/src/booking-surface/dsh-plugin.js +17 -1
- package/ts/src/booking-surface/error-codes.ts +75 -0
- package/ts/src/booking-surface/recovery-chain.ts +409 -0
- package/ts/src/booking-surface/server.ts +84 -51
- package/ts/src/booking-surface/validation.ts +2 -0
- package/ts/src/contracts.ts +12 -2
- package/ts/src/dsh-llm.ts +4 -8
- package/ts/src/index.ts +609 -90
- package/ts/src/loop.ts +116 -7
- package/ts/src/memory-capture.ts +227 -3
- package/ts/src/mock-llm.ts +4 -8
- package/ts/src/model.ts +88 -2
- package/ts/src/state-ledger.ts +47 -29
- package/ts/src/subagent-job-id-guard.ts +72 -0
- package/ts/src/time-anchor.ts +217 -13
- package/ts/src/unified.ts +316 -54
package/dist/src/index.js
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GoTry dsh 插件(gotry-tools):把 GoTry 的领域能力注册为 dsh 工具。
|
|
3
|
+
*
|
|
4
|
+
* 对应总纲 3.2 插件清单中的三个最小集:
|
|
5
|
+
* - gotry_feasibility_check 可行性引擎(门到门全成本,bridge → Python Z3)
|
|
6
|
+
* - gotry_motivation_save 动机画像落盘(为什么出发;B2B 接缝的契约对象)
|
|
7
|
+
* - gotry_wish_pool_add 「下一次出发」清单(憧憬不被拒绝)
|
|
8
|
+
*
|
|
9
|
+
* 插件形态遵循 dsh 约定(name/inject/Config/apply + ctx.tools.register(defineTool(...))),
|
|
10
|
+
* 对齐 @deepseek-ai/dsh-tools@0.1.5-rc.1 的契约:
|
|
11
|
+
* render 位于 output 对象内,参数属性是 ValueSchemaSpec(支持 type:'json')。
|
|
12
|
+
*
|
|
13
|
+
* @module @gotry/plugin
|
|
14
|
+
*/
|
|
1
15
|
import { join } from 'node:path';
|
|
2
16
|
import z from '@deepseek-ai/schemastery';
|
|
3
17
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
@@ -10,19 +24,21 @@ import { interpretArgs } from './tool-packet.js';
|
|
|
10
24
|
import { projectUtility } from './memory-utility.js';
|
|
11
25
|
import { pickNudgeWish } from './wish-pool.js';
|
|
12
26
|
import { resolveTimelineDate } from './travel-timeline.js';
|
|
13
|
-
import { ensureLedger, readCompanionsWithFallback, readMotivationWithFallback, readTripsWithFallback
|
|
27
|
+
import { ensureLedger, readCompanionsWithFallback, readMotivationWithFallback, readTripsWithFallback } from './state-ledger.js';
|
|
28
|
+
import { isValidHomeCityPreference, resolveDefaultOrigin } from './memory-capture.js';
|
|
29
|
+
import { applyPlanningWindow } from './loop.js';
|
|
14
30
|
import { buildTimeAnchor } from './time-anchor.js';
|
|
15
|
-
import {
|
|
31
|
+
import { evaluateHotelStayDates } from './hotel-date-gate.js';
|
|
16
32
|
import { wmoLabel } from '../capabilities/weather.js';
|
|
17
|
-
import {
|
|
18
|
-
import { runDoctorChecks, renderDoctorReportMd } from '../capabilities/doctor.js';
|
|
33
|
+
import { runDoctorChecks, renderDoctorReportMd, runDoctorRepair, createRepairApprovalGate } from '../capabilities/doctor.js';
|
|
19
34
|
import { EXTENSION_STORE_URL } from '../capabilities/session/extension-bridge.js';
|
|
20
|
-
import { createConsentGate, approvalFromContext } from '../capabilities/session-consent.js';
|
|
35
|
+
import { createConsentGate, approvalFromContext, resolveSessionSearchKind } from '../capabilities/session-consent.js';
|
|
21
36
|
import { installModelOverride } from '../capabilities/model-override.js';
|
|
22
37
|
import { listArtifacts, readArtifact } from '../capabilities/artifacts.js';
|
|
23
38
|
import { interpretEffect, declinedObservation } from '../capabilities/effect.js';
|
|
24
39
|
import { appendFacts, loadFactRegistry } from '../capabilities/fact-log.js';
|
|
25
|
-
import { factsFromFlyai, factsFromHotel, factsFromSession } from './bookable-facts.js';
|
|
40
|
+
import { factsFromFlyai, factsFromHotel, factsFromSession, factsFromSessionTrain } from './bookable-facts.js';
|
|
41
|
+
import { hasRecognizedAvailableSeat } from '../capabilities/session/adapters/rail-12306.js';
|
|
26
42
|
import { gateArtifact } from './artifact-gate.js';
|
|
27
43
|
import { installTurnDeadline, listTurnHandoffTickets } from './turn-deadline.js';
|
|
28
44
|
import { noteChannelVerdict, recordChannelEvent, readLatestChannelEvents } from '../capabilities/channel-health.js';
|
|
@@ -30,75 +46,118 @@ import { routingAdvice, renderRoutingCard, toolRoutingHeadline } from '../capabi
|
|
|
30
46
|
import { registerBenchmarkEnvironmentBridge } from './benchmark-environment-bridge.js';
|
|
31
47
|
import { installBenchmarkToolIsolation } from './benchmark-tool-isolation.js';
|
|
32
48
|
import { installBenchmarkAgentConformance } from './benchmark-agent-conformance.js';
|
|
49
|
+
import { installSubagentJobIdGuard } from './subagent-job-id-guard.js';
|
|
50
|
+
import { createGroundTransferResolver, createPublicMapDrivingRouteProvider, exposeGroundTransferEvidence, resolveGroundTransferPayload, } from '../capabilities/ground-transfer.js';
|
|
51
|
+
/** 航司→机场映射表(issue #46 冲突检测面;data/airline-airports.json,as_of 快照) */
|
|
33
52
|
let airlineAirportMapCache = null;
|
|
34
53
|
async function loadAirlineAirportMap() {
|
|
35
|
-
if (airlineAirportMapCache)
|
|
54
|
+
if (airlineAirportMapCache)
|
|
55
|
+
return airlineAirportMapCache;
|
|
36
56
|
try {
|
|
37
57
|
const { readFile } = await import('node:fs/promises');
|
|
38
58
|
airlineAirportMapCache = JSON.parse(await readFile(join(import.meta.dirname, '..', '..', 'data', 'airline-airports.json'), 'utf-8'));
|
|
39
59
|
return airlineAirportMapCache;
|
|
40
|
-
}
|
|
41
|
-
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null; // 映射缺失不阻塞检索主路径;闸工具侧另行显式报错
|
|
42
63
|
}
|
|
43
64
|
}
|
|
44
65
|
export const name = 'gotry-tools';
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
];
|
|
66
|
+
// The bridge obtains subprocess through an optional Cordis lookup at apply
|
|
67
|
+
// time. Declaring it as a required injection would make the whole plugin
|
|
68
|
+
// depend on a service that is only needed for explicit benchmark opt-in.
|
|
69
|
+
export const inject = ['tools', 'systemPrompt'];
|
|
70
|
+
function sessionCwd(exec) {
|
|
71
|
+
const candidate = exec;
|
|
72
|
+
return typeof candidate?.agent?.session?.header?.cwd === 'string'
|
|
73
|
+
? candidate.agent.session.header.cwd
|
|
74
|
+
: undefined;
|
|
75
|
+
}
|
|
49
76
|
export const Config = z.object({
|
|
50
77
|
stateRoot: z.string().default('.'),
|
|
51
78
|
timeoutMs: z.number().default(30_000),
|
|
52
79
|
hbcliBin: z.string().default('hbcli'),
|
|
53
80
|
sessionAccess: z.string().default('ask'),
|
|
54
|
-
benchmarkEnvironmentConfigPath: z.string().default('')
|
|
81
|
+
benchmarkEnvironmentConfigPath: z.string().default(''),
|
|
55
82
|
});
|
|
83
|
+
/**
|
|
84
|
+
* query 包装参数的三形态归一(#12/#13):实现移居 tool-packet.ts(interpretArgs,
|
|
85
|
+
* RFC S1 interpretation 层语义归位),此处按旧名引 handy 别名,调用点零漂移。
|
|
86
|
+
*/
|
|
56
87
|
const unwrapQuery = interpretArgs;
|
|
88
|
+
/**
|
|
89
|
+
* 动机画像 → persona 紧凑 brief(M4 T1 读回路径):空画像返回 ''(首访),
|
|
90
|
+
* persona 据此决定是否访谈。只读,不猜——画像里没有的字段不编。
|
|
91
|
+
* ADR-15:读经账本(未迁移 root 回退旧文件,只读)。
|
|
92
|
+
*/
|
|
57
93
|
function renderMotivationBrief(stateRoot) {
|
|
58
94
|
const p = readMotivationWithFallback(stateRoot);
|
|
59
|
-
if (!p)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
];
|
|
95
|
+
if (!p)
|
|
96
|
+
return ''; // 首访:无画像
|
|
97
|
+
const lines = ['## 用户记忆(跨会话画像;与用户当轮说法冲突时以用户为准,更新经 gotry_motivation_save)'];
|
|
63
98
|
const weights = Object.entries(p.weights ?? {});
|
|
64
|
-
if (weights.length)
|
|
99
|
+
if (weights.length)
|
|
100
|
+
lines.push(`- 动机权重: ${weights.map(([k, v]) => `${k}=${v}`).join(', ')}(证据 ${p.evidence?.length ?? 0} 条)`);
|
|
65
101
|
const hard = Object.entries(p.hard ?? {});
|
|
66
|
-
if (hard.length)
|
|
102
|
+
if (hard.length)
|
|
103
|
+
lines.push(`- 硬约束: ${hard.map(([k, v]) => `${k}=${String(v)}`).join(', ')}`);
|
|
104
|
+
// 持久默认城市(issue #338):生产 brief 复用纯 resolver 的 precedence
|
|
105
|
+
// 契约;这里仍只负责读回渲染,不把 resolver 接入 feasibility/evaluate。
|
|
106
|
+
const profile = p;
|
|
107
|
+
const homeResolution = resolveDefaultOrigin(undefined, profile);
|
|
108
|
+
const homePref = profile.homeCityPreference;
|
|
109
|
+
const typedPreferenceIsValid = isValidHomeCityPreference(profile);
|
|
110
|
+
const hasHomeCityData = Object.prototype.hasOwnProperty.call(profile, 'homeCity')
|
|
111
|
+
|| Object.prototype.hasOwnProperty.call(profile, 'homeCityPreference');
|
|
112
|
+
if (homeResolution.source === 'home_default' && homeResolution.origin && typedPreferenceIsValid && homePref) {
|
|
113
|
+
const evidence = homeResolution.homeCityEvidence?.[0] ?? '';
|
|
114
|
+
lines.push(`- 常驻城市: ${homeResolution.origin}(原话证据:${evidence};记录于 ${homePref.updated_at})——本轮未指明出发地时作为软默认;本轮显式出发地优先;不进可行性硬约束,不删候选`);
|
|
115
|
+
}
|
|
116
|
+
else if (typedPreferenceIsValid && homePref?.value === null) {
|
|
117
|
+
lines.push(`- 常驻城市: 已显式清除(原话证据:${homePref.evidence};记录于 ${homePref.updated_at})——已无持久软默认;每次行程必须由用户明确出发地,不猜测`);
|
|
118
|
+
}
|
|
119
|
+
else if (hasHomeCityData) {
|
|
120
|
+
lines.push('- 常驻城市: 无可验证的持久默认——本轮请明确出发地;不从旧画像或全局 evidence 猜测');
|
|
121
|
+
}
|
|
122
|
+
// 旅行时间线(memory-design P1):去过的地方不再主动推荐,除非用户点名
|
|
67
123
|
const trips = readTimelineTrips(stateRoot);
|
|
68
124
|
if (trips.length) {
|
|
69
|
-
lines.push(`- 去过: ${trips.map(
|
|
125
|
+
lines.push(`- 去过: ${trips.map(t => `${t.destination}(${t.start.slice(0, 7)})`).join('、')}——去过的地方不再主动推荐,除非用户点名`);
|
|
70
126
|
}
|
|
127
|
+
// 同行人档案(memory-design P2):约束只进排序与行程结构建议,永不硬过滤
|
|
71
128
|
const companions = readCompanions(stateRoot);
|
|
72
129
|
if (companions.length) {
|
|
73
|
-
lines.push(`- 同行人: ${companions.map(
|
|
130
|
+
lines.push(`- 同行人: ${companions.map(c => `${c.label}(${c.brief})`).join('、')}——约束进结构与排序建议,不硬过滤;引用时带当初的原话依据`);
|
|
74
131
|
}
|
|
75
132
|
lines.push(`- 愿望池: 用 gotry_wish_pool_list 按条件召回(0..1),勿直接堆砌`);
|
|
76
|
-
if (p.updated_at)
|
|
77
|
-
|
|
133
|
+
if (p.updated_at)
|
|
134
|
+
lines.push(`- 更新于: ${p.updated_at}`);
|
|
135
|
+
return weights.length || hard.length || hasHomeCityData ? lines.join('\n') : '';
|
|
78
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* 时间线摘要(brief 用):最近 3 次行程(目的地+年月);账本优先,未迁移回退文件 */
|
|
139
|
+
/** 同行人摘要(brief 用):label + 约束串 */
|
|
79
140
|
function readCompanions(stateRoot) {
|
|
80
|
-
return readCompanionsWithFallback(stateRoot)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
...c.constraints.prefs ?? []
|
|
85
|
-
].filter(Boolean);
|
|
86
|
-
return {
|
|
87
|
-
label: c.label,
|
|
88
|
-
brief: bits.join('/')
|
|
89
|
-
};
|
|
141
|
+
return readCompanionsWithFallback(stateRoot)
|
|
142
|
+
.map(c => {
|
|
143
|
+
const bits = [c.constraints.mobility, ...(c.constraints.health ?? []), ...(c.constraints.prefs ?? [])].filter(Boolean);
|
|
144
|
+
return { label: c.label, brief: bits.join('/') };
|
|
90
145
|
});
|
|
91
146
|
}
|
|
92
147
|
function readTimelineTrips(stateRoot) {
|
|
93
|
-
return readTripsWithFallback(stateRoot)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
148
|
+
return readTripsWithFallback(stateRoot)
|
|
149
|
+
.map(t => ({ destination: t.destination, start: t.start }))
|
|
150
|
+
.sort((a, b) => (a.start < b.start ? 1 : -1))
|
|
151
|
+
.slice(0, 3);
|
|
97
152
|
}
|
|
153
|
+
/** Benchmark 治理面钉死预算(Round 9,#100/#102):官方任务形态(如 ChinaTravel
|
|
154
|
+
* 单查询 300s)超出内置 60/120s;预算仍必须「按 run 钉死」保证可复现——允许
|
|
155
|
+
* owner 经 env 按次显式设定,缺省维持既有值,非法值启动即抛。 */
|
|
98
156
|
function benchmarkPinnedPolicy() {
|
|
99
|
-
const parse = (name, fallback)=>{
|
|
157
|
+
const parse = (name, fallback) => {
|
|
100
158
|
const raw = process.env[name];
|
|
101
|
-
if (raw === undefined || raw === '')
|
|
159
|
+
if (raw === undefined || raw === '')
|
|
160
|
+
return fallback;
|
|
102
161
|
const value = Number.parseInt(raw, 10);
|
|
103
162
|
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
104
163
|
throw new Error(`invalid ${name}: ${JSON.stringify(raw)} (expected a positive safe integer)`);
|
|
@@ -107,719 +166,727 @@ function benchmarkPinnedPolicy() {
|
|
|
107
166
|
};
|
|
108
167
|
const softMs = parse('GOTRY_BENCHMARK_SOFT_MS', 60_000);
|
|
109
168
|
const hardMs = parse('GOTRY_BENCHMARK_HARD_MS', 120_000);
|
|
110
|
-
if (hardMs <= softMs)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
hardMs,
|
|
114
|
-
exit: 'converge'
|
|
115
|
-
};
|
|
169
|
+
if (hardMs <= softMs)
|
|
170
|
+
throw new Error(`GOTRY_BENCHMARK_HARD_MS (${hardMs}) must exceed GOTRY_BENCHMARK_SOFT_MS (${softMs})`);
|
|
171
|
+
return { softMs, hardMs, exit: 'converge' };
|
|
116
172
|
}
|
|
117
|
-
|
|
173
|
+
/** 把 hotel-date-gate.message 里的模型转手措辞(请告诉用户 / 请告诉 gotry /
|
|
174
|
+
* 请向用户确认)归一为直接对用户措辞(请补充 / 请提供)。仅做这一类短语的字面
|
|
175
|
+
* 替换,日期示例等可行动细节保留不动;只服务 hotel_search input_required 卡
|
|
176
|
+
* 单点,不动 date-gate 契约、不动其它 presentResult 路径。 */
|
|
177
|
+
function normalizeHotelGateHandoff(message) {
|
|
178
|
+
const raw = String(message ?? '');
|
|
179
|
+
if (!raw)
|
|
180
|
+
return '';
|
|
181
|
+
return raw
|
|
182
|
+
.replace(/请告诉\s*(?:用户|gotry)\s*/g, '请补充 ')
|
|
183
|
+
.replace(/请向用户确认/g, '请提供');
|
|
184
|
+
}
|
|
185
|
+
function parseFeasibilityPlanning(raw, anchor) {
|
|
186
|
+
if (raw === undefined)
|
|
187
|
+
return null;
|
|
188
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
189
|
+
throw new Error('planning must be an object');
|
|
190
|
+
const planning = raw;
|
|
191
|
+
const intent = planning['intent'];
|
|
192
|
+
const requestedYear = planning['requested_year'];
|
|
193
|
+
if (intent !== 'future' && intent !== 'historical') {
|
|
194
|
+
throw new Error('planning.intent must be future or historical');
|
|
195
|
+
}
|
|
196
|
+
if (!Number.isInteger(requestedYear) || Number(requestedYear) < 1_000 || Number(requestedYear) > 9_999) {
|
|
197
|
+
throw new Error('planning.requested_year must be a four-digit integer');
|
|
198
|
+
}
|
|
199
|
+
// referenceDate is always the host clock; the payload cannot supply a stale now.
|
|
200
|
+
return { intent, requestedYear: Number(requestedYear), referenceDate: anchor.today };
|
|
201
|
+
}
|
|
202
|
+
function feasibilityValidationFailure(code, summary, rejected) {
|
|
203
|
+
return { ok: false, code, summary, rejected: rejected ?? [] };
|
|
204
|
+
}
|
|
205
|
+
export function apply(ctx, config, seams = {}) {
|
|
206
|
+
const clock = seams.clock ?? (() => new Date());
|
|
207
|
+
const runFlyaiEffect = seams.effect ?? interpretEffect;
|
|
208
|
+
const groundTransferResolver = createGroundTransferResolver({
|
|
209
|
+
provider: seams.groundTransfer?.provider ?? createPublicMapDrivingRouteProvider(ctx),
|
|
210
|
+
clock,
|
|
211
|
+
});
|
|
118
212
|
const rawBenchmarkEnvironmentConfigPath = config.benchmarkEnvironmentConfigPath ?? '';
|
|
213
|
+
// ADR-24 v2:产品路径装「路由 + wall-clock 双出口」——用户主观时间是唯一
|
|
214
|
+
// 预算,复杂度决定出口结构(converge/handoff)。benchmark opt-in 钉死
|
|
215
|
+
// 固定 policy 保证评测可复现,不走路由。GOTRY_HANDOFF_CHILD=1 是收集器
|
|
216
|
+
// 派生的后台规划会话:唯一出口 converge + 长 leash——前台转后台,后台
|
|
217
|
+
// 必须产出最终交付物,不得再次 handoff(否则无限递归)。
|
|
119
218
|
const handoffChild = process.env.GOTRY_HANDOFF_CHILD === '1';
|
|
120
|
-
installTurnDeadline(ctx, handoffChild
|
|
121
|
-
fixedPolicy: {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
} : rawBenchmarkEnvironmentConfigPath.trim() ? {
|
|
127
|
-
fixedPolicy: benchmarkPinnedPolicy()
|
|
128
|
-
} : {
|
|
129
|
-
stateRoot: process.env.GOTRY_TURN_HANDOFF_ROOT ?? config.stateRoot ?? '.'
|
|
130
|
-
});
|
|
219
|
+
installTurnDeadline(ctx, handoffChild
|
|
220
|
+
? { fixedPolicy: { softMs: 300_000, hardMs: 900_000, exit: 'converge' } }
|
|
221
|
+
: rawBenchmarkEnvironmentConfigPath.trim()
|
|
222
|
+
? { fixedPolicy: benchmarkPinnedPolicy() }
|
|
223
|
+
: { stateRoot: process.env.GOTRY_TURN_HANDOFF_ROOT ?? config.stateRoot ?? '.' });
|
|
131
224
|
if (rawBenchmarkEnvironmentConfigPath.trim()) {
|
|
225
|
+
// Benchmark mode is a deliberately minimal kernel: only the model
|
|
226
|
+
// override and the environment bridge are installed besides the pinned
|
|
227
|
+
// turn-deadline policy above. In particular, product prompt variables,
|
|
228
|
+
// process/consent guards, and guarded product tools must not be
|
|
229
|
+
// observable on this path.
|
|
132
230
|
installModelOverride(ctx);
|
|
133
231
|
const getService = ctx.get;
|
|
134
|
-
const directSubprocess = typeof getService === 'function'
|
|
135
|
-
|
|
232
|
+
const directSubprocess = (typeof getService === 'function'
|
|
233
|
+
? getService.call(ctx, 'subprocess')
|
|
234
|
+
: undefined);
|
|
235
|
+
const projection = registerBenchmarkEnvironmentBridge(rawBenchmarkEnvironmentConfigPath, tool => ctx.tools.register(tool), directSubprocess);
|
|
136
236
|
installBenchmarkToolIsolation(ctx);
|
|
137
237
|
installBenchmarkAgentConformance(ctx, projection);
|
|
138
238
|
return;
|
|
139
239
|
}
|
|
240
|
+
// 时间感知:注册动态变量,persona 里用 {{current_date}} 引用。
|
|
241
|
+
// 每次 assemble 时取系统时钟——LLM 始终知道「今天是几号」。
|
|
140
242
|
const sp = ctx['systemPrompt'];
|
|
141
|
-
sp?.variable?.('current_date', ()=>{
|
|
243
|
+
sp?.variable?.('current_date', () => {
|
|
142
244
|
const d = new Date();
|
|
143
245
|
const ymd = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
144
|
-
const weekdays = [
|
|
145
|
-
'日',
|
|
146
|
-
'一',
|
|
147
|
-
'二',
|
|
148
|
-
'三',
|
|
149
|
-
'四',
|
|
150
|
-
'五',
|
|
151
|
-
'六'
|
|
152
|
-
];
|
|
246
|
+
const weekdays = ['日', '一', '二', '三', '四', '五', '六'];
|
|
153
247
|
return `${ymd} 周${weekdays[d.getDay()]}`;
|
|
154
248
|
});
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
sp?.variable?.('
|
|
158
|
-
|
|
249
|
+
// 时间锚点卡(time-anchor 层,确定性):相对日期换算的唯一依据——
|
|
250
|
+
// 明天/下周X/下个月中旬/节日都查卡,LLM 不自算(算术只在代码层)。
|
|
251
|
+
sp?.variable?.('time_anchor_card', () => buildTimeAnchor(new Date()).card);
|
|
252
|
+
// M4 记忆读回(T1 闭环的另一半):motivation-profile 此前只写不读,每个新会话
|
|
253
|
+
// 模型都是盲的、重新访谈——「回访规划时长降 ≥50%」不可能成立。这里把画像
|
|
254
|
+
// 渲染成紧凑 brief 注入 persona;为空 = 首访。与当轮说法冲突时以用户为准。
|
|
255
|
+
sp?.variable?.('motivation_brief', () => renderMotivationBrief(config.stateRoot ?? '.'));
|
|
256
|
+
// 通道路由卡(通道注册表生成,docs/design/tool-orchestration-design.md §2.1/D-8):
|
|
257
|
+
// persona 检索条款只留行为契约,机/火/酒通道顺位与额度口径查卡——prose 教义
|
|
258
|
+
// 变查表教义,注册表加通道卡片自动一致。
|
|
259
|
+
sp?.variable?.('channel_routing_card', () => renderRoutingCard());
|
|
260
|
+
// 通道健康面接线(issue #107/#108):检索通道 verdict → 会话状态标记 +
|
|
261
|
+
// down/cooldown 事件落持久面(doctor CLI 进程消费)。落盘失败不阻塞检索。
|
|
262
|
+
const noteChannel = async (channel, verdict) => {
|
|
159
263
|
const ev = noteChannelVerdict(channel, verdict);
|
|
160
|
-
if (ev)
|
|
264
|
+
if (ev)
|
|
265
|
+
await recordChannelEvent(config.stateRoot ?? '.', ev);
|
|
161
266
|
};
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
267
|
+
/** verdict≠hit 时在平铺 envelope 上追加 routing 建议(§3.3:失败现场教学) */
|
|
268
|
+
const routingField = (intent, excludeChannel) => ({ routing: routingAdvice(intent, { excludeChannel }) });
|
|
269
|
+
// D-NEW 进程护栏(Z3 WASM crash 教训):dsh 0.1.1-rc.1 缺 uncaughtException
|
|
270
|
+
// handler,插件异常穿透即杀进程。我们在 gotry 侧挂护栏:同步 fsync 写事故证据
|
|
271
|
+
// (gotry-state/incidents.jsonl),handler 自身不再抛,不阻塞后续控制流。
|
|
272
|
+
// 不调 process.exit——让 dsh/上级容器决定生死,我们只留现场。
|
|
273
|
+
installProcessGuards(config.stateRoot ?? '.', { uncaughtException: 'gotry-tools', unhandledRejection: 'gotry-tools' });
|
|
274
|
+
// 账号会话授权闸(RFC 支柱④「用户明示授权 + 站点白名单 + 随时可关」进代码;v2 每会话一次):
|
|
275
|
+
// 会话面工具动用用户本人登录态,**每会话每站点首次调用**弹 dsh 原生审批卡——
|
|
276
|
+
// allowed-once 记入会话 granted 集,后续同站调用免弹;用户拒绝 = 本会话吊销
|
|
277
|
+
// (denied 集,不再弹卡也不再执行——拒绝是裁决,不反复骚扰);无审批通道
|
|
278
|
+
// (headless 无用户在场)一律 fail-closed 拒绝。sessionAccess: ask(默认)|allow|off。
|
|
279
|
+
// v1 教训(2026-08-29 founder 实测「每次都要弹,经常无法点击」):逐调用弹卡 = 骚扰,
|
|
280
|
+
// 会话态收归 capabilities/session-consent.ts。防御:极简宿主/mock ctx 无事件总线时跳过。
|
|
281
|
+
// issue #194 A-轨道 guard 先注册;生产 Cordis 的 prepend 保证它在授权闸
|
|
282
|
+
// 前运行,smoke 的轻量事件采集器也继续把授权闸作为最后一个观察项。
|
|
283
|
+
installSubagentJobIdGuard(ctx);
|
|
171
284
|
const ctxOn = ctx.on;
|
|
172
285
|
if (typeof ctxOn === 'function') {
|
|
173
|
-
|
|
174
|
-
access: ()=>config.sessionAccess ?? 'ask',
|
|
175
|
-
approval: approvalFromContext(ctx)
|
|
176
|
-
})
|
|
286
|
+
const gate = createConsentGate({
|
|
287
|
+
access: () => config.sessionAccess ?? 'ask',
|
|
288
|
+
approval: approvalFromContext(ctx),
|
|
289
|
+
});
|
|
290
|
+
// site 绑定(#308):dsh pre-execute 实际派发的 arguments 原样进入 gate;
|
|
291
|
+
// gate 与 execute 共用 resolveSessionSearchKind/interpretArgs,因此 wrapped
|
|
292
|
+
// query.*、缺省 flight、unknown/malformed 的选择与授权和效应路径保持一致。
|
|
293
|
+
ctx.on('tools/pre-execute', gate);
|
|
177
294
|
}
|
|
295
|
+
// issue #284 修复闸:gotry_doctor.action='repair' 走同一审批缝(scope-keyed,
|
|
296
|
+
// WeakMap<agent>)——同 agent + 同 scope 复用批准,不同 scope 重问,rejected/cancelled
|
|
297
|
+
// = 本会话吊销,unavailable 通道不记。同口径 session-consent,不增新持久面。
|
|
298
|
+
// 注册早于 gotry_doctor(下方),保证工具面闭包捕获当前会话的闸实例。
|
|
299
|
+
const repairApprovalGate = createRepairApprovalGate({
|
|
300
|
+
approval: approvalFromContext(ctx),
|
|
301
|
+
});
|
|
302
|
+
// LLM_MODEL → dsh 会话面模型覆盖(issue #77;机制与分层见
|
|
303
|
+
// capabilities/model-override.ts 头注)。GOTRY_LLM_MODEL 未设时零行为变化。
|
|
178
304
|
installModelOverride(ctx);
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
};
|
|
305
|
+
// D-NEW 收尾:全部工具 execute 统一异常隔离——单个工具抛错/拒绝不再沿 cordis
|
|
306
|
+
// 传到 dsh 主循环,降级为结构化错误返回给 LLM + incident 落盘(incident-log.ts)。
|
|
307
|
+
const registerGuarded = (tool) => {
|
|
308
|
+
const t = { ...tool };
|
|
183
309
|
if (typeof t.execute === 'function') {
|
|
184
310
|
t.execute = guardToolExecute(String(t.name), config.stateRoot ?? '.', t.execute);
|
|
185
311
|
}
|
|
186
312
|
ctx.tools.register(t);
|
|
187
313
|
};
|
|
188
|
-
|
|
314
|
+
// 工具描述首行生成(issue #113,L1):检索工具描述统一前置注册表生成的
|
|
315
|
+
// 「服务意图 × 通道顺位」卡——模型选工具时(读描述)与失败后(读 routing)
|
|
316
|
+
// 两个决策点拿到同一张表;注册表加行,描述自动一致。非检索工具原样返回。
|
|
317
|
+
const routed = (tool, desc) => {
|
|
189
318
|
const head = toolRoutingHeadline(tool);
|
|
190
319
|
return head ? `${head}\n${desc}` : desc;
|
|
191
320
|
};
|
|
192
321
|
registerGuarded(defineTool({
|
|
193
322
|
name: 'gotry_feasibility_check',
|
|
194
|
-
description: 'Check travel candidates against the user\'s motivation and hard constraints using the '
|
|
323
|
+
description: 'Check travel candidates against the user\'s motivation and hard constraints using the '
|
|
324
|
+
+ 'door-to-door true-cost engine (wake time, arrival state, energy, usable hours, money). '
|
|
325
|
+
+ 'Input is the structured request (motivation weights + hard constraints + window + budget + home hubs) '
|
|
326
|
+
+ 'and the candidate list (services/transfers/stay costs/min days): '
|
|
327
|
+
+ 'structure { request: { motivation weights, hard constraints, window, budget, home hubs }, candidates: [ { id, label, services, transfers, stay, minDays } ] }. '
|
|
328
|
+
+ 'Returns per-candidate verdicts, unsat cores with minimal-modification suggestions, '
|
|
329
|
+
+ 'a wish-pool entry for infeasible aspirations, and a ready-to-show markdown answer.',
|
|
330
|
+
// D-30 第五刀(issue #112):payload json blob → 结构化对象(request/candidates 模型可见,
|
|
331
|
+
// 嵌套形状权威在 unified 引擎契约,工具面不越层强约束)
|
|
195
332
|
parameters: {
|
|
196
333
|
payload: {
|
|
197
334
|
type: 'object',
|
|
198
335
|
additionalProperties: false,
|
|
199
336
|
required: true,
|
|
200
337
|
properties: {
|
|
201
|
-
request: {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
description: '
|
|
338
|
+
request: { type: 'object', additionalProperties: true, required: true, description: '引擎请求:{ motivation weights, hard constraints, window, budget, home hubs }' },
|
|
339
|
+
candidates: { type: 'array', items: { type: 'object', additionalProperties: true }, required: true, description: '候选列表:[{ id, label, date?, services, transfers, stay, minDays }]' },
|
|
340
|
+
planning: {
|
|
341
|
+
type: 'object', additionalProperties: false,
|
|
342
|
+
description: '可选显式规划上下文;dated 候选省略时默认按宿主时钟作 future 下界过滤,指定 future 年份再加年末上界,historical 仅用于明确历史/回测查询;完全 dateless 输入保持旧可行性计算',
|
|
343
|
+
properties: {
|
|
344
|
+
intent: { type: 'string', enum: ['future', 'historical'], required: true },
|
|
345
|
+
requested_year: { type: 'integer', required: true, description: '用户明确请求的四位年份,不传宿主当前年份' },
|
|
346
|
+
},
|
|
206
347
|
},
|
|
207
|
-
|
|
208
|
-
type: '
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
348
|
+
ground_transfer: {
|
|
349
|
+
type: 'object',
|
|
350
|
+
additionalProperties: false,
|
|
351
|
+
description: '可选显式地面接驳覆盖:仅 destination 位置、显式经纬度和 mode=driving;路线同时绑定抵达方向(A→B)与返程方向(B→A),两方向分别请求/缓存/降级,价格仍来自静态包;缓存 TTL 由能力层固定',
|
|
352
|
+
properties: {
|
|
353
|
+
candidate_id: { type: 'string', description: '要绑定的候选 id;不会按相似度推断' },
|
|
354
|
+
transfer_index: { type: 'integer', description: 'dest_transfers 的零基位置;不会按 mode 推断' },
|
|
355
|
+
position: { type: 'string', description: '当前切片固定为 destination' },
|
|
356
|
+
mode: { type: 'string', description: '当前只接受 driving;其它/缺省模式显式回退静态分钟' },
|
|
357
|
+
origin: {
|
|
358
|
+
type: 'object',
|
|
359
|
+
additionalProperties: false,
|
|
360
|
+
description: '旧版单对坐标:等同于 outbound.origin(能力层自动把返程方向设为其互换)',
|
|
361
|
+
properties: {
|
|
362
|
+
longitude: { type: 'number' },
|
|
363
|
+
latitude: { type: 'number' },
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
destination: {
|
|
367
|
+
type: 'object',
|
|
368
|
+
additionalProperties: false,
|
|
369
|
+
description: '旧版单对坐标:等同于 outbound.destination',
|
|
370
|
+
properties: {
|
|
371
|
+
longitude: { type: 'number' },
|
|
372
|
+
latitude: { type: 'number' },
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
outbound: {
|
|
376
|
+
type: 'object',
|
|
377
|
+
additionalProperties: false,
|
|
378
|
+
description: '显式抵达方向坐标对(A→B):不指定时回退 origin/destination',
|
|
379
|
+
properties: {
|
|
380
|
+
origin: {
|
|
381
|
+
type: 'object',
|
|
382
|
+
additionalProperties: false,
|
|
383
|
+
properties: {
|
|
384
|
+
longitude: { type: 'number' },
|
|
385
|
+
latitude: { type: 'number' },
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
destination: {
|
|
389
|
+
type: 'object',
|
|
390
|
+
additionalProperties: false,
|
|
391
|
+
properties: {
|
|
392
|
+
longitude: { type: 'number' },
|
|
393
|
+
latitude: { type: 'number' },
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
return: {
|
|
399
|
+
type: 'object',
|
|
400
|
+
additionalProperties: false,
|
|
401
|
+
description: '显式返程方向坐标对(B→A):不指定时回退 outbound 互换',
|
|
402
|
+
properties: {
|
|
403
|
+
origin: {
|
|
404
|
+
type: 'object',
|
|
405
|
+
additionalProperties: false,
|
|
406
|
+
properties: {
|
|
407
|
+
longitude: { type: 'number' },
|
|
408
|
+
latitude: { type: 'number' },
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
destination: {
|
|
412
|
+
type: 'object',
|
|
413
|
+
additionalProperties: false,
|
|
414
|
+
properties: {
|
|
415
|
+
longitude: { type: 'number' },
|
|
416
|
+
latitude: { type: 'number' },
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
},
|
|
212
421
|
},
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
217
|
-
}
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
},
|
|
218
425
|
},
|
|
219
426
|
output: {
|
|
220
|
-
schema: {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
type: 'text',
|
|
226
|
-
text: String(value.answer_md ?? JSON.stringify(value))
|
|
227
|
-
}
|
|
228
|
-
]
|
|
427
|
+
schema: { type: 'json' },
|
|
428
|
+
render: (_args, value) => [{
|
|
429
|
+
type: 'text',
|
|
430
|
+
text: String(value.answer_md ?? JSON.stringify(value)),
|
|
431
|
+
}],
|
|
229
432
|
},
|
|
230
|
-
async execute
|
|
433
|
+
async execute(args, _exec) {
|
|
434
|
+
// 纯 TS 路径:D-7 后 unified solveChoiceSegment 是唯一求解入口(无 Python 桥、无 z3 WASM
|
|
435
|
+
// 路径——候选形态枚举求解,~6ms/次)。unified 内部有 try-catch 护栏覆盖 wasm 异常。
|
|
231
436
|
const started = Date.now();
|
|
232
437
|
const payload = args.payload;
|
|
438
|
+
const anchor = buildTimeAnchor(clock());
|
|
439
|
+
let planning;
|
|
440
|
+
try {
|
|
441
|
+
planning = parseFeasibilityPlanning(payload['planning'], anchor);
|
|
442
|
+
}
|
|
443
|
+
catch (error) {
|
|
444
|
+
const summary = error instanceof Error ? error.message : 'planning context is invalid';
|
|
445
|
+
return feasibilityValidationFailure('invalid_planning_context', summary);
|
|
446
|
+
}
|
|
233
447
|
const req = parseRequest(payload['request']);
|
|
234
448
|
const cands = payload['candidates'].map(parseCandidate);
|
|
235
|
-
const
|
|
236
|
-
|
|
449
|
+
const execution = _exec && typeof _exec === 'object' ? _exec : undefined;
|
|
450
|
+
let solverCandidates = cands;
|
|
451
|
+
let groundTransferEvidence;
|
|
452
|
+
if (payload['ground_transfer'] !== undefined) {
|
|
453
|
+
const prepared = await resolveGroundTransferPayload(payload['ground_transfer'], cands, groundTransferResolver, { signal: execution?.signal, execution });
|
|
454
|
+
solverCandidates = prepared.candidates;
|
|
455
|
+
groundTransferEvidence = prepared.resolution;
|
|
456
|
+
}
|
|
457
|
+
const spec = segmentsFromCandidate(req, solverCandidates);
|
|
458
|
+
const planningCheck = applyPlanningWindow(spec, planning, anchor, { defaultDatedFuture: true });
|
|
459
|
+
if (planningCheck.error) {
|
|
460
|
+
return feasibilityValidationFailure('planning_window_rejected', planningCheck.error, planningCheck.rejected);
|
|
461
|
+
}
|
|
462
|
+
const solved = solveChoiceSegment(planningCheck.spec, req);
|
|
463
|
+
const result = groundTransferEvidence
|
|
464
|
+
? exposeGroundTransferEvidence(solved, groundTransferEvidence)
|
|
465
|
+
: solved;
|
|
237
466
|
const dir = await ensureStateDir(config.stateRoot);
|
|
238
|
-
await recordLatency(join(dir, 'bridge-latency.jsonl'), Date.now() - started, 'feasibility_check:in-process-unified').catch(()=>{});
|
|
239
|
-
return {
|
|
240
|
-
ok: true,
|
|
241
|
-
...result,
|
|
242
|
-
latency_ms: Date.now() - started,
|
|
243
|
-
via: 'in-process-unified'
|
|
244
|
-
};
|
|
467
|
+
await recordLatency(join(dir, 'bridge-latency.jsonl'), Date.now() - started, 'feasibility_check:in-process-unified').catch(() => { });
|
|
468
|
+
return { ok: true, ...result, latency_ms: Date.now() - started, via: 'in-process-unified' };
|
|
245
469
|
},
|
|
246
|
-
presentCall:
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
kind: 'execute',
|
|
250
|
-
rawInput: args.payload
|
|
251
|
-
}),
|
|
252
|
-
presentResult: (args, value)=>{
|
|
470
|
+
presentCall: args => ({ card: 'generic', title: 'GoTry 可行性检查(门到门全成本)', kind: 'execute', rawInput: args.payload }),
|
|
471
|
+
presentResult: (args, value) => {
|
|
472
|
+
// D-4 卡片赎回:结果卡不再是裸 JSON —— 逐候选判定 + 全成本 vs 预算紧凑行 + 人话答案
|
|
253
473
|
const r = value;
|
|
254
|
-
const budget = args.payload?.request?.budget_cny;
|
|
255
|
-
const lines = (r.verdicts ?? []).map(
|
|
256
|
-
const tc = v.true_cost ?? {};
|
|
257
|
-
const money = typeof tc.money_cny === 'number'
|
|
258
|
-
|
|
474
|
+
const budget = (args.payload?.request)?.budget_cny;
|
|
475
|
+
const lines = (r.verdicts ?? []).map(v => {
|
|
476
|
+
const tc = (v.true_cost ?? {});
|
|
477
|
+
const money = typeof tc.money_cny === 'number'
|
|
478
|
+
? ` ¥${tc.money_cny}/人` + (typeof budget === 'number' ? `(预算 ¥${budget},${tc.money_cny <= budget ? '余' : '超'} ¥${Math.abs(budget - tc.money_cny)})` : '')
|
|
479
|
+
: '';
|
|
480
|
+
return v.feasible
|
|
481
|
+
? `✅ ${String(v.name ?? v.candidate_id)}${money}${v.candidate_id === r.recommended ? ' ← 推荐' : ''}`
|
|
482
|
+
: `❌ ${String(v.name ?? v.candidate_id)} — ${(Array.isArray(v.unsat_core) ? v.unsat_core.join(',') : '不可行')}`;
|
|
259
483
|
});
|
|
260
484
|
return {
|
|
261
485
|
card: 'generic',
|
|
262
486
|
title: `可行性:${r.recommended ? `推荐 ${r.recommended}` : '全部不可行'}`,
|
|
263
|
-
content: [
|
|
264
|
-
{
|
|
265
|
-
type: 'text',
|
|
266
|
-
text: (lines.length ? lines.join('\n') + '\n\n' : '') + String(r.answer_md ?? '').slice(0, 1500)
|
|
267
|
-
}
|
|
268
|
-
]
|
|
487
|
+
content: [{ type: 'text', text: (lines.length ? lines.join('\n') + '\n\n' : '') + String(r.answer_md ?? '').slice(0, 1500) }],
|
|
269
488
|
};
|
|
270
|
-
}
|
|
489
|
+
},
|
|
271
490
|
}));
|
|
272
491
|
registerGuarded(defineTool({
|
|
273
492
|
name: 'gotry_motivation_save',
|
|
274
|
-
description: 'Persist the traveler\'s motivation profile (the "why depart" contract object). '
|
|
493
|
+
description: 'Persist the traveler\'s motivation profile (the "why depart" contract object). '
|
|
494
|
+
+ 'This is the B2B reuse seam: downstream plugins consume only MotivationProfile + constraints, '
|
|
495
|
+
+ 'never the principal/sponsor distinction. MERGE semantics (T1): call again with just the NEW '
|
|
496
|
+
+ 'facts learned this turn (weights delta optional but MUST bring fresh evidence; evidence = user quotes); '
|
|
497
|
+
+ 'existing history is never deleted. Requires evidence on every call (P0 anti-fabrication rule).',
|
|
498
|
+
// D-30 第五刀(issue #112):profile blob → 结构化闭合对象;evidence(P0 反伪造红线)进嵌套
|
|
499
|
+
// schema 由宿主权闸——无证据的保存请求入口即被结构化拒绝(ToolFailure 形状)
|
|
500
|
+
// issue #338:homeCity 字段(string=设置,null=显式清除,undefined=不动);单条非空
|
|
501
|
+
// evidence 可省略 binding,多条 evidence 必须显式 homeCityEvidence。
|
|
502
|
+
// dsh 参数 schema 仅支持单值 type,string|null 用 oneOf 表达。
|
|
275
503
|
parameters: {
|
|
276
504
|
profile: {
|
|
277
505
|
type: 'object',
|
|
278
506
|
additionalProperties: false,
|
|
279
507
|
required: true,
|
|
280
508
|
properties: {
|
|
281
|
-
weights: {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
509
|
+
weights: { type: 'object', additionalProperties: true, description: '动机权重增量,如 { escape_rest: 0.7 }(可选,权重变更须伴新证据)' },
|
|
510
|
+
evidence: { type: 'array', items: { type: 'string' }, required: true, description: '本轮新事实的用户原话数组(P0:必带)' },
|
|
511
|
+
hard: { type: 'object', additionalProperties: true, description: '硬约束:{ wake_not_before, min_arrival_energy_pct }(可选)' },
|
|
512
|
+
homeCity: {
|
|
513
|
+
oneOf: [
|
|
514
|
+
{ type: 'string', description: '设置持久常驻城市/默认出发地(用户原话指明常驻),如 "上海"' },
|
|
515
|
+
{ type: 'null', description: '显式清除常驻城市(用于遗忘/迁居)' },
|
|
516
|
+
],
|
|
517
|
+
description: '持久常驻城市/默认出发地(string=设置,null=显式清除);改值须伴新 evidence(P0);恰一条非空可省略 binding,多条须显式;不删候选/不压当轮 origin',
|
|
285
518
|
},
|
|
286
|
-
|
|
287
|
-
type: '
|
|
288
|
-
|
|
289
|
-
type: 'string'
|
|
290
|
-
},
|
|
291
|
-
required: true,
|
|
292
|
-
description: '本轮新事实的用户原话数组(P0:必带)'
|
|
519
|
+
homeCityEvidence: {
|
|
520
|
+
type: 'string',
|
|
521
|
+
description: 'homeCity 对应的 exact 用户原话;显式传入时必须逐字出现在本次 profile.evidence 中且为新证据(同值同绑定重复调用例外为幂等);evidence 恰好一条非空时可省略并绑定该条,多条时必须显式传入',
|
|
293
522
|
},
|
|
294
|
-
|
|
295
|
-
type: 'object',
|
|
296
|
-
additionalProperties: true,
|
|
297
|
-
description: '硬约束:{ wake_not_before, min_arrival_energy_pct }(可选)'
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
},
|
|
302
|
-
output: {
|
|
303
|
-
schema: {
|
|
304
|
-
type: 'json'
|
|
523
|
+
},
|
|
305
524
|
},
|
|
306
|
-
render: (_args, value)=>[
|
|
307
|
-
{
|
|
308
|
-
type: 'text',
|
|
309
|
-
text: `动机画像已保存:${String(value.path ?? '')}`
|
|
310
|
-
}
|
|
311
|
-
]
|
|
312
525
|
},
|
|
313
|
-
|
|
314
|
-
|
|
526
|
+
output: {
|
|
527
|
+
// loose json:guard 兜底字段(ok/summary)不被 strict schema 拒(骨架事故同款)
|
|
528
|
+
schema: { type: 'json' },
|
|
529
|
+
render: (_args, value) => [{ type: 'text', text: `动机画像已保存:${String(value.path ?? '')}` }],
|
|
530
|
+
},
|
|
531
|
+
async execute(args, _exec) {
|
|
532
|
+
// T1 接线:本工具是增量补丁语义(契约 18——模型每轮把对话新事实带进来),
|
|
533
|
+
// 经 mergeProfile 守门合并进既有画像(追加不删史/幂等/权重变更须伴新证据),
|
|
534
|
+
// 不再整档覆盖。首次调用(无档案)= 全量建立。
|
|
535
|
+
// ADR-15:守门+事件+投影在账本单事务内完成,evidence 红线拒绝即回滚,账本无痕。
|
|
536
|
+
// issue #338:homeCity 同走 mergeProfile 守门(null=显式清除)。单条非空
|
|
537
|
+
// evidence 可提供省略的 unambiguous binding;多条 evidence 不按数组位置推断。
|
|
538
|
+
const incoming = (args.profile ?? {});
|
|
315
539
|
if (!incoming.evidence?.length) {
|
|
316
540
|
throw new Error('refusing to save a motivation profile without evidence (P0 anti-fabrication rule)');
|
|
317
541
|
}
|
|
542
|
+
// issue #338:trim 后空白拒绝(零事件落账)。空白字符串「无值」≠
|
|
543
|
+
// 「显式清除」——后者是 null,前者是没有声明。不擅自解释,显式拒收。
|
|
544
|
+
if (typeof incoming.homeCity === 'string' && incoming.homeCity.trim().length === 0) {
|
|
545
|
+
throw new Error('gotry_motivation_save.homeCity 是空白字符串,既不是声明也不是清除;请明确传城市名或 null');
|
|
546
|
+
}
|
|
547
|
+
let boundHomeCityEvidence;
|
|
548
|
+
if (incoming.homeCity !== undefined) {
|
|
549
|
+
if (incoming.homeCityEvidence !== undefined) {
|
|
550
|
+
if (typeof incoming.homeCityEvidence !== 'string' || incoming.homeCityEvidence.trim().length === 0) {
|
|
551
|
+
throw new Error('gotry_motivation_save.homeCityEvidence 为空白;homeCity 必须绑定本次 evidence 中的一条 exact 用户原话');
|
|
552
|
+
}
|
|
553
|
+
if (!incoming.evidence.some(e => e === incoming.homeCityEvidence)) {
|
|
554
|
+
throw new Error('gotry_motivation_save.homeCityEvidence 必须逐字匹配本次 profile.evidence 中的一条用户原话');
|
|
555
|
+
}
|
|
556
|
+
boundHomeCityEvidence = incoming.homeCityEvidence;
|
|
557
|
+
}
|
|
558
|
+
else if (incoming.evidence.length === 1 && typeof incoming.evidence[0] === 'string' && incoming.evidence[0].trim().length > 0) {
|
|
559
|
+
boundHomeCityEvidence = incoming.evidence[0];
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
throw new Error('gotry_motivation_save.homeCityEvidence 缺失或有歧义;evidence 恰好一条非空时可省略,多条 evidence 必须显式绑定 exact 用户原话');
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
else if (incoming.homeCityEvidence !== undefined) {
|
|
566
|
+
throw new Error('gotry_motivation_save.homeCityEvidence 只能与 homeCity 一起传入');
|
|
567
|
+
}
|
|
318
568
|
const ledger = ensureLedger(config.stateRoot);
|
|
319
569
|
const res = ledger.appendMotivationPatch({
|
|
320
570
|
weights: incoming.weights,
|
|
321
571
|
evidence: incoming.evidence,
|
|
322
|
-
hard: incoming.hard
|
|
572
|
+
hard: incoming.hard,
|
|
573
|
+
homeCity: incoming.homeCity,
|
|
574
|
+
homeCityEvidence: boundHomeCityEvidence,
|
|
323
575
|
});
|
|
324
576
|
const profileJson = JSON.parse(JSON.stringify(res.profile));
|
|
325
|
-
return res.saved
|
|
326
|
-
ok: true,
|
|
327
|
-
|
|
328
|
-
path: ledger.dbPath,
|
|
329
|
-
profile: profileJson,
|
|
330
|
-
summary: '画像已合并入账本(单事务)'
|
|
331
|
-
} : {
|
|
332
|
-
ok: true,
|
|
333
|
-
saved: false,
|
|
334
|
-
path: ledger.dbPath,
|
|
335
|
-
profile: profileJson,
|
|
336
|
-
summary: '无新内容(幂等跳过)'
|
|
337
|
-
};
|
|
577
|
+
return res.saved
|
|
578
|
+
? { ok: true, saved: true, path: ledger.dbPath, profile: profileJson, summary: '画像已合并入账本(单事务)' }
|
|
579
|
+
: { ok: true, saved: false, path: ledger.dbPath, profile: profileJson, summary: '无新内容(幂等跳过)' };
|
|
338
580
|
},
|
|
339
|
-
presentCall:
|
|
340
|
-
card: 'generic',
|
|
341
|
-
title: '保存动机画像',
|
|
342
|
-
kind: 'edit',
|
|
343
|
-
rawInput: args.profile
|
|
344
|
-
})
|
|
581
|
+
presentCall: args => ({ card: 'generic', title: '保存动机画像', kind: 'edit', rawInput: args.profile }),
|
|
345
582
|
}));
|
|
346
583
|
registerGuarded(defineTool({
|
|
347
584
|
name: 'gotry_wish_pool_add',
|
|
348
|
-
description: 'Add an aspiration to the "next departure" wish pool — the graceful home for infeasible dreams. '
|
|
585
|
+
description: 'Add an aspiration to the "next departure" wish pool — the graceful home for infeasible dreams. '
|
|
586
|
+
+ 'An entry carries its fulfilment conditions (days needed, budget, best months) so a future '
|
|
587
|
+
+ '"next departure" nudge can fire when the window matches. Each entry gets a stable wish_id '
|
|
588
|
+
+ '(the memory-utility sidecar keys on it); muted:true puts a wish dormant (永不删除,只是不再召回). 憧憬不被拒绝。',
|
|
349
589
|
parameters: {
|
|
350
590
|
entry: {
|
|
351
591
|
type: 'json',
|
|
352
592
|
required: true,
|
|
353
|
-
description: '{ name, reason?, conditions: { days, budget_cny, best_months, channels? }, muted?: boolean }'
|
|
354
|
-
}
|
|
593
|
+
description: '{ name, reason?, conditions: { days, budget_cny, best_months, channels? }, muted?: boolean }',
|
|
594
|
+
},
|
|
355
595
|
},
|
|
356
596
|
output: {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
render: (_args, value)=>{
|
|
597
|
+
// loose json:guard 兜底字段(ok/summary)不被 strict schema 拒(骨架事故同款)
|
|
598
|
+
schema: { type: 'json' },
|
|
599
|
+
render: (_args, value) => {
|
|
361
600
|
const v = value;
|
|
362
|
-
return [
|
|
363
|
-
|
|
364
|
-
type: 'text',
|
|
365
|
-
text: `已加入「下一次出发」清单(共 ${v.total ?? '?'} 项):${v.path ?? ''}`
|
|
366
|
-
}
|
|
367
|
-
];
|
|
368
|
-
}
|
|
601
|
+
return [{ type: 'text', text: `已加入「下一次出发」清单(共 ${v.total ?? '?'} 项):${v.path ?? ''}` }];
|
|
602
|
+
},
|
|
369
603
|
},
|
|
370
|
-
async execute
|
|
371
|
-
const entry = args.entry ?? {};
|
|
604
|
+
async execute(args, _exec) {
|
|
605
|
+
const entry = (args.entry ?? {});
|
|
606
|
+
// 同名憧憬幂等更新(刷新理由与成行条件),不重复入池;wish_id 语义派生自名称,稳定可重放。
|
|
607
|
+
// ADR-15:conditions 红线在账本事务内校验拒绝。
|
|
372
608
|
const ledger = ensureLedger(config.stateRoot);
|
|
373
|
-
const r = ledger.appendWish({
|
|
374
|
-
|
|
375
|
-
reason: entry.reason,
|
|
376
|
-
conditions: entry.conditions,
|
|
377
|
-
muted: entry.muted
|
|
378
|
-
});
|
|
379
|
-
return {
|
|
380
|
-
ok: true,
|
|
381
|
-
added: r.added,
|
|
382
|
-
wish_id: r.wish_id,
|
|
383
|
-
total: r.total,
|
|
384
|
-
path: ledger.dbPath
|
|
385
|
-
};
|
|
609
|
+
const r = ledger.appendWish({ name: entry.name, reason: entry.reason, conditions: entry.conditions, muted: entry.muted });
|
|
610
|
+
return { ok: true, added: r.added, wish_id: r.wish_id, total: r.total, path: ledger.dbPath };
|
|
386
611
|
},
|
|
387
|
-
presentCall:
|
|
388
|
-
card: 'generic',
|
|
389
|
-
title: '加入「下一次出发」清单',
|
|
390
|
-
kind: 'edit',
|
|
391
|
-
rawInput: args.entry
|
|
392
|
-
})
|
|
612
|
+
presentCall: args => ({ card: 'generic', title: '加入「下一次出发」清单', kind: 'edit', rawInput: args.entry }),
|
|
393
613
|
}));
|
|
394
614
|
registerGuarded(defineTool({
|
|
395
615
|
name: 'gotry_wish_pool_list',
|
|
396
|
-
description: 'Surface AT MOST ONE "next departure" wish whose fulfilment conditions match the user\'s current window '
|
|
616
|
+
description: 'Surface AT MOST ONE "next departure" wish whose fulfilment conditions match the user\'s current window '
|
|
617
|
+
+ '(0..1 rule: never more than one nudge per turn, never push when nothing matches — 憧憬不被拒绝,也不被硬推). '
|
|
618
|
+
+ 'Muted wishes never surface. Surfacing records a recalled event in the memory-utility sidecar. '
|
|
619
|
+
+ 'A wish whose conditions.channels names a channel that is currently down (out-of-band health facts) is vetoed for this recall — 不推当下走不通的憧憬. '
|
|
620
|
+
+ 'action="confirm-outcome" records the user-confirmed real-world outcome (attribution helpful/harmful/neutral) — '
|
|
621
|
+
+ 'ONLY pass attribution the user explicitly stated; the agent must never self-attribute usefulness.',
|
|
622
|
+
// D-30 第四刀(issue #112):query blob → 平铺 typed;全字段可选 → interpretArgs 容忍层
|
|
397
623
|
parameters: {
|
|
398
|
-
action: {
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
},
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
},
|
|
406
|
-
budgetCny: {
|
|
407
|
-
type: 'number',
|
|
408
|
-
description: '预算上限(recall 过滤)'
|
|
409
|
-
},
|
|
410
|
-
month: {
|
|
411
|
-
type: 'integer',
|
|
412
|
-
description: '出行月份 1-12(recall)'
|
|
413
|
-
},
|
|
414
|
-
wishId: {
|
|
415
|
-
type: 'string',
|
|
416
|
-
description: 'confirm-outcome 目标愿望 id'
|
|
417
|
-
},
|
|
418
|
-
attribution: {
|
|
419
|
-
type: 'string',
|
|
420
|
-
enum: [
|
|
421
|
-
'helpful',
|
|
422
|
-
'harmful',
|
|
423
|
-
'neutral'
|
|
424
|
-
],
|
|
425
|
-
description: 'confirm-outcome 归因'
|
|
426
|
-
},
|
|
427
|
-
detail: {
|
|
428
|
-
type: 'string',
|
|
429
|
-
description: '一句话细节(用户原话)'
|
|
430
|
-
},
|
|
431
|
-
tripStart: {
|
|
432
|
-
type: 'string',
|
|
433
|
-
description: '确认成行时挂时间线的行程起始日'
|
|
434
|
-
}
|
|
624
|
+
action: { type: 'string', description: 'recall(默认,召回下一次出发建议)或 confirm-outcome(确认某条建议成/不成)' },
|
|
625
|
+
days: { type: 'integer', description: '回看天数窗(recall)' },
|
|
626
|
+
budgetCny: { type: 'number', description: '预算上限(recall 过滤)' },
|
|
627
|
+
month: { type: 'integer', description: '出行月份 1-12(recall)' },
|
|
628
|
+
wishId: { type: 'string', description: 'confirm-outcome 目标愿望 id' },
|
|
629
|
+
attribution: { type: 'string', enum: ['helpful', 'harmful', 'neutral'], description: 'confirm-outcome 归因' },
|
|
630
|
+
detail: { type: 'string', description: '一句话细节(用户原话)' },
|
|
631
|
+
tripStart: { type: 'string', description: '确认成行时挂时间线的行程起始日' },
|
|
435
632
|
},
|
|
436
633
|
output: {
|
|
437
|
-
schema: {
|
|
438
|
-
|
|
439
|
-
},
|
|
440
|
-
render: (_args, value)=>[
|
|
441
|
-
{
|
|
442
|
-
type: 'text',
|
|
443
|
-
text: String(value.summary ?? '')
|
|
444
|
-
}
|
|
445
|
-
]
|
|
634
|
+
schema: { type: 'json' },
|
|
635
|
+
render: (_args, value) => [{ type: 'text', text: String(value.summary ?? '') }],
|
|
446
636
|
},
|
|
447
|
-
async execute
|
|
637
|
+
async execute(args, _exec) {
|
|
448
638
|
const q = unwrapQuery(args, 'action');
|
|
449
639
|
const ledger = ensureLedger(config.stateRoot);
|
|
450
640
|
const now = new Date().toISOString();
|
|
451
641
|
if (q.action === 'confirm-outcome') {
|
|
452
642
|
if (!q.wishId || !q.attribution) {
|
|
453
|
-
return {
|
|
454
|
-
ok: false,
|
|
455
|
-
summary: 'confirm-outcome 需要 wishId + attribution(helpful|harmful|neutral)'
|
|
456
|
-
};
|
|
643
|
+
return { ok: false, summary: 'confirm-outcome 需要 wishId + attribution(helpful|harmful|neutral)' };
|
|
457
644
|
}
|
|
645
|
+
// 成行确认 = 效用事件 + 可选时间线行程,账本单事务(ADR-15:原两文件写在崩溃时分叉)
|
|
458
646
|
let tripInput;
|
|
459
|
-
const wish = ledger.readWishPool().find(
|
|
647
|
+
const wish = ledger.readWishPool().find(e => String(e.wish_id) === q.wishId);
|
|
460
648
|
if (q.tripStart && wish?.name) {
|
|
461
649
|
const anchor = buildTimeAnchor(new Date());
|
|
462
650
|
const start = resolveTimelineDate(q.tripStart, anchor) ?? (/^\d{4}-\d{2}-\d{2}$/.test(q.tripStart) ? q.tripStart : undefined);
|
|
463
651
|
if (start) {
|
|
464
652
|
tripInput = {
|
|
465
|
-
destination: String(wish.name),
|
|
466
|
-
|
|
467
|
-
companions: undefined,
|
|
468
|
-
source: 'wish-confirmed',
|
|
469
|
-
evidence: `wish ${q.wishId} confirm-outcome(${q.attribution})`
|
|
653
|
+
destination: String(wish.name), start, companions: undefined,
|
|
654
|
+
source: 'wish-confirmed', evidence: `wish ${q.wishId} confirm-outcome(${q.attribution})`,
|
|
470
655
|
};
|
|
471
656
|
}
|
|
472
657
|
}
|
|
473
|
-
const r = ledger.confirmOutcome({
|
|
474
|
-
|
|
475
|
-
attribution: q.attribution,
|
|
476
|
-
detail: q.detail,
|
|
477
|
-
trip: tripInput
|
|
478
|
-
});
|
|
479
|
-
return {
|
|
480
|
-
ok: true,
|
|
481
|
-
recorded: r.recorded,
|
|
482
|
-
wish_id: q.wishId,
|
|
483
|
-
status: q.attribution,
|
|
484
|
-
...r.trip ? {
|
|
485
|
-
trip: r.trip
|
|
486
|
-
} : {}
|
|
487
|
-
};
|
|
658
|
+
const r = ledger.confirmOutcome({ wishId: q.wishId, attribution: q.attribution, detail: q.detail, trip: tripInput });
|
|
659
|
+
return { ok: true, recorded: r.recorded, wish_id: q.wishId, status: q.attribution, ...(r.trip ? { trip: r.trip } : {}) };
|
|
488
660
|
}
|
|
661
|
+
// recall:0..1 条件匹配(判定归 wish-pool 纯函数),muted 永不召回,无命中不硬推;
|
|
662
|
+
// 通道否证(接缝第 2 段):持久健康面里处于 down 的通道集合进 context——
|
|
663
|
+
// conditions.channels 命名依赖通道的愿望在被否证时不当场推荐(读不到健康面=不启用)。
|
|
489
664
|
const pool = ledger.readWishPool();
|
|
490
|
-
const candidates = pool.filter(
|
|
665
|
+
const candidates = pool.filter(e => !e.muted && typeof e.wish_id === 'string');
|
|
491
666
|
const month = q.month ?? new Date().getMonth() + 1;
|
|
492
667
|
let channelDown;
|
|
493
668
|
try {
|
|
494
669
|
const latest = await readLatestChannelEvents(config.stateRoot);
|
|
495
670
|
const down = new Set();
|
|
496
|
-
for (const [ch, ev] of latest)
|
|
671
|
+
for (const [ch, ev] of latest)
|
|
672
|
+
if (ev.state === 'down')
|
|
673
|
+
down.add(ch);
|
|
497
674
|
channelDown = down;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
budgetCny: q.budgetCny,
|
|
502
|
-
month,
|
|
503
|
-
channelDown
|
|
504
|
-
});
|
|
675
|
+
}
|
|
676
|
+
catch { /* 健康面缺席=不启用否证,召回行为与旧版一致 */ }
|
|
677
|
+
const match = pickNudgeWish(candidates, { days: q.days, budgetCny: q.budgetCny, month, channelDown });
|
|
505
678
|
if (!match) {
|
|
506
|
-
return {
|
|
507
|
-
ok: true,
|
|
508
|
-
suggestion: null,
|
|
509
|
-
summary: `无可成行的憧憬匹配当前窗口(${candidates.length} 条在册,0..1 纪律:不硬推)`
|
|
510
|
-
};
|
|
679
|
+
return { ok: true, suggestion: null, summary: `无可成行的憧憬匹配当前窗口(${candidates.length} 条在册,0..1 纪律:不硬推)` };
|
|
511
680
|
}
|
|
512
681
|
const { events: next } = ledger.appendUtilityEvent({
|
|
513
|
-
wish_id: match.wishId,
|
|
514
|
-
kind: 'recalled',
|
|
515
|
-
ts: now,
|
|
516
|
-
ctx: 'gotry_wish_pool_list.recall'
|
|
682
|
+
wish_id: match.wishId, kind: 'recalled', ts: now, ctx: 'gotry_wish_pool_list.recall',
|
|
517
683
|
});
|
|
518
684
|
const utility = projectUtility(next)[match.wishId];
|
|
519
685
|
return {
|
|
520
686
|
ok: true,
|
|
521
|
-
suggestion: {
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
reason: match.entry['reason'],
|
|
525
|
-
conditions: match.entry['conditions'],
|
|
526
|
-
match_score: match.score,
|
|
527
|
-
hits: match.hits
|
|
528
|
-
},
|
|
529
|
-
utility: {
|
|
530
|
-
status: utility?.status ?? 'unknown',
|
|
531
|
-
recalled: utility?.recalled ?? 1
|
|
532
|
-
},
|
|
533
|
-
summary: `「下一次出发」候选(0..1):${String(match.entry['name'])}——成行条件 ${JSON.stringify(match.entry['conditions'])},本次窗口命中 ${match.score}/3 项(${match.hits.join('+')});效用状态 ${utility?.status ?? 'unknown'}`
|
|
687
|
+
suggestion: { wish_id: match.entry['wish_id'], name: match.entry['name'], reason: match.entry['reason'], conditions: match.entry['conditions'], match_score: match.score, hits: match.hits },
|
|
688
|
+
utility: { status: utility?.status ?? 'unknown', recalled: utility?.recalled ?? 1 },
|
|
689
|
+
summary: `「下一次出发」候选(0..1):${String(match.entry['name'])}——成行条件 ${JSON.stringify(match.entry['conditions'])},本次窗口命中 ${match.score}/3 项(${match.hits.join('+')});效用状态 ${utility?.status ?? 'unknown'}`,
|
|
534
690
|
};
|
|
535
691
|
},
|
|
536
|
-
presentCall:
|
|
537
|
-
card: 'generic',
|
|
538
|
-
title: '「下一次出发」召回',
|
|
539
|
-
kind: 'search',
|
|
540
|
-
rawInput: args
|
|
541
|
-
})
|
|
692
|
+
presentCall: args => ({ card: 'generic', title: '「下一次出发」召回', kind: 'search', rawInput: args }),
|
|
542
693
|
}));
|
|
694
|
+
// ADR-24 v2 复访交付面:用户回问「行程规划好了吗」时,模型用这个只读工具
|
|
695
|
+
// 查后台工单状态;settled 附交付物摘录(完整内容在工单同目录
|
|
696
|
+
// .deliverable.md)。工单由 scripts/turn-handoff-collect.ts 收集结算。
|
|
543
697
|
registerGuarded(defineTool({
|
|
544
698
|
name: 'gotry_turn_handoff_list',
|
|
545
|
-
description: 'List background deep-planning handoff tickets (read-only). Call this whenever the user asks about '
|
|
699
|
+
description: 'List background deep-planning handoff tickets (read-only). Call this whenever the user asks about '
|
|
700
|
+
+ 'a previously handed-off plan («规划好了吗» / «上次那个行程»): open = still being worked in the '
|
|
701
|
+
+ 'background (ETA ' + '约 1 小时' + '), settled = deliverable ready (excerpt included, full text in the '
|
|
702
|
+
+ 'ticket\'s .deliverable.md), failed = honest failure note. Never fabricate a deliverable that is not here.',
|
|
703
|
+
// D-30 第三刀(issue #112):query blob → 平铺 typed;全字段可选 → interpretArgs 容忍层(同 session 刀法)
|
|
546
704
|
parameters: {
|
|
547
|
-
ticketId: {
|
|
548
|
-
type: 'string',
|
|
549
|
-
description: '只看这一单,如 th-...(缺省列全部)'
|
|
550
|
-
}
|
|
705
|
+
ticketId: { type: 'string', description: '只看这一单,如 th-...(缺省列全部)' },
|
|
551
706
|
},
|
|
552
707
|
output: {
|
|
553
|
-
schema: {
|
|
554
|
-
|
|
555
|
-
},
|
|
556
|
-
render: (_args, value)=>[
|
|
557
|
-
{
|
|
558
|
-
type: 'text',
|
|
559
|
-
text: String(value.summary ?? '')
|
|
560
|
-
}
|
|
561
|
-
]
|
|
708
|
+
schema: { type: 'json' },
|
|
709
|
+
render: (_args, value) => [{ type: 'text', text: String(value.summary ?? '') }],
|
|
562
710
|
},
|
|
563
|
-
async execute
|
|
711
|
+
async execute(args, _exec) {
|
|
564
712
|
const q = interpretArgs(args);
|
|
565
713
|
const root = process.env.GOTRY_TURN_HANDOFF_ROOT ?? config.stateRoot ?? '.';
|
|
566
714
|
const all = await listTurnHandoffTickets(root);
|
|
567
|
-
const tickets = (q.ticketId ? all.filter(
|
|
715
|
+
const tickets = (q.ticketId ? all.filter(t => t.id === q.ticketId) : all).slice(0, 10);
|
|
568
716
|
if (tickets.length === 0) {
|
|
569
717
|
return {
|
|
570
|
-
ok: true,
|
|
571
|
-
|
|
572
|
-
|
|
718
|
+
ok: true, count: 0,
|
|
719
|
+
summary: q.ticketId
|
|
720
|
+
? `没有 id 为 ${q.ticketId} 的后台规划工单(可能已被清理或记错 id)`
|
|
721
|
+
: '当前没有后台深度规划工单(无挂起、无历史)',
|
|
573
722
|
};
|
|
574
723
|
}
|
|
575
|
-
const lines = tickets.map(
|
|
724
|
+
const lines = tickets.map(t => {
|
|
576
725
|
const head = `[${t.status}] ${t.id} ${t.objective.slice(0, 40)}`;
|
|
577
|
-
if (t.status === 'open')
|
|
578
|
-
|
|
726
|
+
if (t.status === 'open')
|
|
727
|
+
return `${head}——后台规划中,ETA ${t.etaLabel}`;
|
|
728
|
+
if (t.status === 'failed')
|
|
729
|
+
return `${head}——失败:${t.error ?? '未知原因'}(请重新发起规划)`;
|
|
579
730
|
return `${head}——已交付,摘要:${(t.deliverableExcerpt ?? '').slice(0, 120).replace(/\n/g, ' ')}`;
|
|
580
731
|
});
|
|
581
732
|
return {
|
|
582
|
-
ok: true,
|
|
583
|
-
|
|
584
|
-
tickets,
|
|
585
|
-
summary: `后台规划工单 ${tickets.length} 张:\n${lines.join('\n')}`
|
|
733
|
+
ok: true, count: tickets.length, tickets,
|
|
734
|
+
summary: `后台规划工单 ${tickets.length} 张:\n${lines.join('\n')}`,
|
|
586
735
|
};
|
|
587
736
|
},
|
|
588
|
-
presentCall:
|
|
589
|
-
card: 'generic',
|
|
590
|
-
title: '后台规划工单查询',
|
|
591
|
-
kind: 'search',
|
|
592
|
-
rawInput: args
|
|
593
|
-
})
|
|
737
|
+
presentCall: args => ({ card: 'generic', title: '后台规划工单查询', kind: 'search', rawInput: args }),
|
|
594
738
|
}));
|
|
595
739
|
registerGuarded(defineTool({
|
|
596
740
|
name: 'gotry_companion_save',
|
|
597
|
-
description: 'Save/update a travel companion profile (memory-design M5 layer): constraints that shape itinerary STRUCTURE and ranking — '
|
|
741
|
+
description: 'Save/update a travel companion profile (memory-design M5 layer): constraints that shape itinerary STRUCTURE and ranking — '
|
|
742
|
+
+ 'never a hard filter (「爸爸65轻度高血压」→ 不排高海拔/控制步行量;「晕车」→ 优先火车/备提示). '
|
|
743
|
+
+ 'evidence MUST be the user\'s verbatim words (append-only, traceable). '
|
|
744
|
+
+ 'NEGATIVE LIST: passport/ID/phone numbers are rejected on sight — such fields never enter storage.',
|
|
745
|
+
// D-30 第五刀(issue #112):companion blob → 结构化闭合对象;label/evidence required 由宿主权闸
|
|
746
|
+
// (evidence append-only 溯源 P0;additionalProperties:false 与负面清单同向——护照/证件号类
|
|
747
|
+
// 杂散字段入口即拒)
|
|
598
748
|
parameters: {
|
|
599
749
|
companion: {
|
|
600
750
|
type: 'object',
|
|
601
751
|
additionalProperties: false,
|
|
602
752
|
required: true,
|
|
603
753
|
properties: {
|
|
604
|
-
label: {
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
},
|
|
609
|
-
constraints: {
|
|
610
|
-
type: 'object',
|
|
611
|
-
additionalProperties: true,
|
|
612
|
-
description: '{ mobility?: "步行≤4h", health?: ["轻度高血压"], prefs?: ["怕吵"] }'
|
|
613
|
-
},
|
|
614
|
-
evidence: {
|
|
615
|
-
type: 'string',
|
|
616
|
-
required: true,
|
|
617
|
-
description: '用户原话(append-only 可溯源,P0)'
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
},
|
|
622
|
-
output: {
|
|
623
|
-
schema: {
|
|
624
|
-
type: 'json'
|
|
754
|
+
label: { type: 'string', required: true, description: '称呼,如 爸爸' },
|
|
755
|
+
constraints: { type: 'object', additionalProperties: true, description: '{ mobility?: "步行≤4h", health?: ["轻度高血压"], prefs?: ["怕吵"] }' },
|
|
756
|
+
evidence: { type: 'string', required: true, description: '用户原话(append-only 可溯源,P0)' },
|
|
757
|
+
},
|
|
625
758
|
},
|
|
626
|
-
render: (_args, value)=>[
|
|
627
|
-
{
|
|
628
|
-
type: 'text',
|
|
629
|
-
text: String(value.summary ?? '')
|
|
630
|
-
}
|
|
631
|
-
]
|
|
632
759
|
},
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
};
|
|
760
|
+
output: {
|
|
761
|
+
schema: { type: 'json' },
|
|
762
|
+
render: (_args, value) => [{ type: 'text', text: String(value.summary ?? '') }],
|
|
763
|
+
},
|
|
764
|
+
async execute(args, _exec) {
|
|
765
|
+
const c = (args.companion ?? {});
|
|
766
|
+
if (!c.evidence)
|
|
767
|
+
return { ok: false, summary: 'evidence 必填(用户原话,溯源 P0)' };
|
|
768
|
+
// ADR-15:负面清单守卫(upsertCompanion)在账本事务内执行,拒收即回滚
|
|
639
769
|
const ledger = ensureLedger(config.stateRoot);
|
|
640
770
|
const res = ledger.appendCompanion({
|
|
641
771
|
label: String(c.label ?? ''),
|
|
642
|
-
constraints: {
|
|
643
|
-
|
|
644
|
-
health: c.constraints?.health,
|
|
645
|
-
prefs: c.constraints?.prefs
|
|
646
|
-
},
|
|
647
|
-
evidence: c.evidence
|
|
772
|
+
constraints: { mobility: c.constraints?.mobility, health: c.constraints?.health, prefs: c.constraints?.prefs },
|
|
773
|
+
evidence: c.evidence,
|
|
648
774
|
});
|
|
649
|
-
if (!res.appended && res.reason)
|
|
650
|
-
ok: false,
|
|
651
|
-
|
|
652
|
-
};
|
|
653
|
-
return {
|
|
654
|
-
ok: true,
|
|
655
|
-
appended: res.appended,
|
|
656
|
-
companion_id: res.companionId,
|
|
657
|
-
total: res.total,
|
|
658
|
-
summary: res.appended ? `同行人已保存:${res.companionId}` : `无新内容(幂等跳过):${res.companionId}`
|
|
659
|
-
};
|
|
775
|
+
if (!res.appended && res.reason)
|
|
776
|
+
return { ok: false, summary: res.reason };
|
|
777
|
+
return { ok: true, appended: res.appended, companion_id: res.companionId, total: res.total, summary: res.appended ? `同行人已保存:${res.companionId}` : `无新内容(幂等跳过):${res.companionId}` };
|
|
660
778
|
},
|
|
661
|
-
presentCall:
|
|
662
|
-
card: 'generic',
|
|
663
|
-
title: '保存同行人',
|
|
664
|
-
kind: 'edit',
|
|
665
|
-
rawInput: args.companion
|
|
666
|
-
})
|
|
779
|
+
presentCall: args => ({ card: 'generic', title: '保存同行人', kind: 'edit', rawInput: args.companion }),
|
|
667
780
|
}));
|
|
668
781
|
registerGuarded(defineTool({
|
|
669
782
|
name: 'gotry_trip_log',
|
|
670
|
-
description: 'Record a PAST trip into the travel timeline (memory-design M4 layer). Use when the user mentions a trip they already took '
|
|
783
|
+
description: 'Record a PAST trip into the travel timeline (memory-design M4 layer). Use when the user mentions a trip they already took '
|
|
784
|
+
+ '(「去年国庆去了大理」) — dates resolve via the time anchor (词表外日期会拒绝,请用户给绝对日期), evidence MUST be the user\'s verbatim words. '
|
|
785
|
+
+ 'Append-only and idempotent (same destination+start+source = same trip); overlapping same-destination trips are rejected for human adjudication. '
|
|
786
|
+
+ 'Past trips power origin resolution and 「去过不再推」 ranking — never a hard filter.',
|
|
787
|
+
// D-30 第四刀(issue #112):平铺 typed;destination/evidence required → 宿主权入口拒缺参
|
|
788
|
+
// (evidence 是 P0 溯源红线,进 schema 由宿主权闸,不再依赖 execute 散文报错)
|
|
671
789
|
parameters: {
|
|
672
|
-
destination: {
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
},
|
|
677
|
-
start: {
|
|
678
|
-
type: 'string',
|
|
679
|
-
required: true,
|
|
680
|
-
description: '行程起始:YYYY-MM-DD 或绝对日期表达(解析不了 execute 结构化报错不猜)'
|
|
681
|
-
},
|
|
682
|
-
end: {
|
|
683
|
-
type: 'string',
|
|
684
|
-
description: '行程结束,形态同 start(可选)'
|
|
685
|
-
},
|
|
686
|
-
companions: {
|
|
687
|
-
type: 'array',
|
|
688
|
-
items: {
|
|
689
|
-
type: 'string'
|
|
690
|
-
},
|
|
691
|
-
description: '同行人,如 ["爸爸"]'
|
|
692
|
-
},
|
|
693
|
-
evidence: {
|
|
694
|
-
type: 'string',
|
|
695
|
-
required: true,
|
|
696
|
-
description: '用户原话(溯源 P0 红线)'
|
|
697
|
-
}
|
|
790
|
+
destination: { type: 'string', required: true, description: '目的地,如 大理' },
|
|
791
|
+
start: { type: 'string', required: true, description: '行程起始:YYYY-MM-DD 或绝对日期表达(解析不了 execute 结构化报错不猜)' },
|
|
792
|
+
end: { type: 'string', description: '行程结束,形态同 start(可选)' },
|
|
793
|
+
companions: { type: 'array', items: { type: 'string' }, description: '同行人,如 ["爸爸"]' },
|
|
794
|
+
evidence: { type: 'string', required: true, description: '用户原话(溯源 P0 红线)' },
|
|
698
795
|
},
|
|
699
796
|
output: {
|
|
700
|
-
schema: {
|
|
701
|
-
|
|
702
|
-
},
|
|
703
|
-
render: (_args, value)=>[
|
|
704
|
-
{
|
|
705
|
-
type: 'text',
|
|
706
|
-
text: String(value.summary ?? '')
|
|
707
|
-
}
|
|
708
|
-
]
|
|
797
|
+
schema: { type: 'json' },
|
|
798
|
+
render: (_args, value) => [{ type: 'text', text: String(value.summary ?? '') }],
|
|
709
799
|
},
|
|
710
|
-
async execute
|
|
800
|
+
async execute(args, _exec) {
|
|
711
801
|
const q = args;
|
|
712
|
-
if (!q.destination)
|
|
713
|
-
ok: false,
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
if (!q.evidence) return {
|
|
717
|
-
ok: false,
|
|
718
|
-
summary: 'evidence 必填(用户原话,溯源 P0)'
|
|
719
|
-
};
|
|
802
|
+
if (!q.destination)
|
|
803
|
+
return { ok: false, summary: 'destination 必填' };
|
|
804
|
+
if (!q.evidence)
|
|
805
|
+
return { ok: false, summary: 'evidence 必填(用户原话,溯源 P0)' };
|
|
720
806
|
const anchor = buildTimeAnchor(new Date());
|
|
721
807
|
const start = resolveTimelineDate(q.start ?? '', anchor) ?? (/^\d{4}-\d{2}-\d{2}$/.test(q.start ?? '') ? q.start : undefined);
|
|
722
|
-
if (!start)
|
|
723
|
-
ok: false,
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
const end = q.end ? resolveTimelineDate(q.end, anchor) ?? (/^\d{4}-\d{2}-\d{2}$/.test(q.end) ? q.end : undefined) : undefined;
|
|
808
|
+
if (!start)
|
|
809
|
+
return { ok: false, summary: `start 无法解析为绝对日期(${q.start ?? '缺'})——请向用户要具体日期,不猜` };
|
|
810
|
+
const end = q.end ? (resolveTimelineDate(q.end, anchor) ?? (/^\d{4}-\d{2}-\d{2}$/.test(q.end) ? q.end : undefined)) : undefined;
|
|
811
|
+
// ADR-15:appendTrip 守门(必填/绝对日期/重叠冲突即停)在账本事务内执行
|
|
727
812
|
const ledger = ensureLedger(config.stateRoot);
|
|
728
813
|
const res = ledger.appendTripEvent({
|
|
729
|
-
destination: q.destination.trim(),
|
|
730
|
-
|
|
731
|
-
end,
|
|
732
|
-
companions: q.companions,
|
|
733
|
-
source: 'user-verbatim',
|
|
734
|
-
evidence: q.evidence
|
|
814
|
+
destination: q.destination.trim(), start, end, companions: q.companions,
|
|
815
|
+
source: 'user-verbatim', evidence: q.evidence,
|
|
735
816
|
});
|
|
736
|
-
if (!res.appended && res.reason)
|
|
737
|
-
ok: false,
|
|
738
|
-
|
|
739
|
-
};
|
|
740
|
-
return {
|
|
741
|
-
ok: true,
|
|
742
|
-
appended: res.appended,
|
|
743
|
-
trip_id: res.tripId,
|
|
744
|
-
total: res.total,
|
|
745
|
-
summary: res.appended ? `已入时间线:${q.destination} @ ${start}` : `已存在(幂等跳过):${res.tripId}`
|
|
746
|
-
};
|
|
817
|
+
if (!res.appended && res.reason)
|
|
818
|
+
return { ok: false, summary: res.reason };
|
|
819
|
+
return { ok: true, appended: res.appended, trip_id: res.tripId, total: res.total, summary: res.appended ? `已入时间线:${q.destination} @ ${start}` : `已存在(幂等跳过):${res.tripId}` };
|
|
747
820
|
},
|
|
748
|
-
presentCall:
|
|
749
|
-
card: 'generic',
|
|
750
|
-
title: '记录旅行',
|
|
751
|
-
kind: 'edit',
|
|
752
|
-
rawInput: args
|
|
753
|
-
})
|
|
821
|
+
presentCall: args => ({ card: 'generic', title: '记录旅行', kind: 'edit', rawInput: args }),
|
|
754
822
|
}));
|
|
755
823
|
registerGuarded(defineTool({
|
|
756
824
|
name: 'gotry_hotel_search',
|
|
757
|
-
description: routed('gotry_hotel_search', 'Search hotels via hotelbyte-cli (real-time when hbcli credentials exist, falls back to the static pack with explicit evidence tagging). '
|
|
825
|
+
description: routed('gotry_hotel_search', 'Search hotels via hotelbyte-cli (real-time when hbcli credentials exist, falls back to the static pack with explicit evidence tagging). '
|
|
826
|
+
+ 'Input: destination city name + optional dates/adults. Dates accept verbatim natural expressions (下周五 / 8.20 / 下周五+3) — '
|
|
827
|
+
+ 'the code layer resolves them against the time anchor. **Dates are required for a supplier search** (issue #283): missing, blank, '
|
|
828
|
+
+ 'single-side, unresolvable, or checkOut ≤ checkIn returns an actionable input_required result WITHOUT spawning the vendor CLI; '
|
|
829
|
+
+ 'only after a complete valid stay window is fixed do we forward to hbcli. '
|
|
830
|
+
+ 'Output: hotel list with evidence chain ([realtime-API:hbcli] + fetch timestamp, '
|
|
831
|
+
+ 'or [static-pack:estimate]) per the L4 invariant.'),
|
|
832
|
+
// D-30 第二刀(issue #112):query blob → 平铺 typed 契约(同 flyai 刀法)。
|
|
758
833
|
parameters: {
|
|
759
|
-
destination: {
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
},
|
|
764
|
-
|
|
765
|
-
type: 'string',
|
|
766
|
-
description: '入住日:YYYY-MM-DD 或自然表达(下周五/8.20/下周五+3);解析不了降级无日期检索并记 date_notes'
|
|
767
|
-
},
|
|
768
|
-
checkOut: {
|
|
769
|
-
type: 'string',
|
|
770
|
-
description: 'checkOut 退房日:YYYY-MM-DD 或自然表达,形态同 checkIn'
|
|
771
|
-
},
|
|
772
|
-
adults: {
|
|
773
|
-
type: 'integer',
|
|
774
|
-
description: '成人数(顶层字段,默认 2)'
|
|
775
|
-
}
|
|
834
|
+
destination: { type: 'string', required: true, description: '目的地城市,如 大理' },
|
|
835
|
+
// issue #283 闸:日期字段对模型面仍是契约必填(required 标在 description,schema 不强制,
|
|
836
|
+
// 让 gate 产出更可行动的错误——dsh schema 拒绝信息太泛)
|
|
837
|
+
checkIn: { type: 'string', description: '入住日:YYYY-MM-DD 或自然表达(下周五/8.20/下周五+3);REQUIRED——缺失/空白/无法解析由 date-gate 返回 input_required,不查' },
|
|
838
|
+
checkOut: { type: 'string', description: 'checkOut 退房日:YYYY-MM-DD 或自然表达,形态同 checkIn;REQUIRED——必须晚于入住日,否则不查' },
|
|
839
|
+
adults: { type: 'integer', description: '成人数(顶层字段,默认 2)' },
|
|
776
840
|
},
|
|
777
841
|
output: {
|
|
778
|
-
schema: {
|
|
779
|
-
|
|
780
|
-
},
|
|
781
|
-
render: (_args, value)=>[
|
|
782
|
-
{
|
|
783
|
-
type: 'text',
|
|
784
|
-
text: String(value.summary ?? JSON.stringify(value).slice(0, 400))
|
|
785
|
-
}
|
|
786
|
-
]
|
|
842
|
+
schema: { type: 'json' },
|
|
843
|
+
render: (_args, value) => [{ type: 'text', text: String(value.summary ?? JSON.stringify(value).slice(0, 400)) }],
|
|
787
844
|
},
|
|
788
|
-
async execute
|
|
845
|
+
async execute(args, _exec) {
|
|
789
846
|
const q = args;
|
|
790
|
-
if (!q.destination)
|
|
847
|
+
if (!q.destination)
|
|
848
|
+
throw new Error('gotry_hotel_search requires destination');
|
|
791
849
|
const started = Date.now();
|
|
792
850
|
const fallbackPath = join(import.meta.dirname, '..', '..', 'data', 'hotels_2026.json');
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
return
|
|
804
|
-
|
|
851
|
+
// issue #283 闸:完整、有效且退房晚于入住才能派发 hbcli;失败直接返回
|
|
852
|
+
// input_required,不发供应商命令,不把当前窗口查询当所需日期结果。
|
|
853
|
+
// 复用既有 time-anchor/slot-spec:词表内自然表达 → 绝对日期;
|
|
854
|
+
// 词表外 → unresolved,闸失败,不再走「无日期降级」伪装默认窗口价。
|
|
855
|
+
const gate = evaluateHotelStayDates(q.checkIn, q.checkOut);
|
|
856
|
+
if (!gate.ok) {
|
|
857
|
+
// 闸失败:不调用 interpretEffect,不调 hbcli;延迟日志不写(no spawn)。
|
|
858
|
+
// summary 用「需要 X 信息才能查询酒店」开头的可行动句式,presentResult 直接
|
|
859
|
+
// 渲成「酒店:<dest> 缺日期」行动卡(替代旧版「无结果」空卡)。
|
|
860
|
+
const summary = `需要补充${(gate.missing ?? []).map(m => m === 'checkIn' ? '入住日' : '退房日').join('和')}才能查询酒店「${q.destination ?? ''}」:${gate.message}`;
|
|
861
|
+
return {
|
|
862
|
+
ok: false,
|
|
863
|
+
verdict: gate.verdict,
|
|
864
|
+
reason: gate.reason,
|
|
865
|
+
missing: gate.missing,
|
|
866
|
+
raw: gate.raw,
|
|
867
|
+
destination: q.destination,
|
|
868
|
+
message: gate.message,
|
|
869
|
+
summary,
|
|
870
|
+
evidence: gate.evidence,
|
|
871
|
+
latency_ms: Date.now() - started,
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
// 闸通过:绝对日期已就位;slot 解析过程产生的原话 → 绝对日期 注记回显
|
|
875
|
+
// (工具层 date_notes 与既有 D-10 切片 B 形态兼容)
|
|
876
|
+
const dateNotes = [...gate.notes];
|
|
877
|
+
// 效应解译层(ADR-18):渠道选择/韧性(重试/熔断)在解译器,result 原样透传——
|
|
878
|
+
// 断路拒绝时返回平铺失败面(不发起查询,不伪装成 miss)
|
|
805
879
|
const itp = await interpretEffect({
|
|
806
880
|
effect: 'HBCLI_HOTEL_SEARCH',
|
|
807
|
-
params: {
|
|
808
|
-
destination: q.destination,
|
|
809
|
-
checkIn: resolveDate(q.checkIn),
|
|
810
|
-
checkOut: resolveDate(q.checkOut),
|
|
811
|
-
adults: q.adults,
|
|
812
|
-
hbcliBin: config.hbcliBin,
|
|
813
|
-
timeoutMs: config.timeoutMs,
|
|
814
|
-
fallbackPath
|
|
815
|
-
}
|
|
881
|
+
params: { destination: q.destination, checkIn: gate.checkIn, checkOut: gate.checkOut, adults: q.adults, hbcliBin: config.hbcliBin, timeoutMs: config.timeoutMs, fallbackPath },
|
|
816
882
|
});
|
|
817
|
-
if (!itp.result)
|
|
883
|
+
if (!itp.result)
|
|
884
|
+
return declinedObservation('HBCLI_HOTEL_SEARCH', itp.trace);
|
|
818
885
|
const resp = itp.result;
|
|
819
886
|
const dir = await ensureStateDir(config.stateRoot);
|
|
820
887
|
const isLive = resp.via === 'hbcli-realtime';
|
|
821
888
|
const evidence = isLive ? resp.evidence : '[静态包:估算]';
|
|
822
|
-
await recordLatency(join(dir, 'bridge-latency.jsonl'), Date.now() - started, `hotel_search:${resp.via}`).catch(()=>{});
|
|
889
|
+
await recordLatency(join(dir, 'bridge-latency.jsonl'), Date.now() - started, `hotel_search:${resp.via}`).catch(() => { });
|
|
823
890
|
const payload = {
|
|
824
891
|
ok: true,
|
|
825
892
|
hotels: resp.hotels ?? null,
|
|
@@ -829,1431 +896,1160 @@ export function apply(ctx, config) {
|
|
|
829
896
|
latency_ms: Date.now() - started,
|
|
830
897
|
summary: resp.summary,
|
|
831
898
|
error: resp.error,
|
|
832
|
-
...dateNotes.length ? {
|
|
833
|
-
date_notes: dateNotes
|
|
834
|
-
} : {}
|
|
899
|
+
...(dateNotes.length ? { date_notes: dateNotes } : {}),
|
|
835
900
|
};
|
|
836
901
|
return JSON.parse(JSON.stringify(payload));
|
|
837
902
|
},
|
|
838
|
-
presentCall:
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
presentResult: (_args, value)=>{
|
|
903
|
+
presentCall: args => ({ card: 'generic', title: `酒店搜索:${args.destination ?? ''}`, kind: 'search', rawInput: args }),
|
|
904
|
+
presentResult: (_args, value) => {
|
|
905
|
+
// 闸失败:渲染为「input_required」行动卡,引导模型向用户追问日期;
|
|
906
|
+
// 不再退化为「无结果」空卡——闸拒绝的可行动语义必须显式进 UI。
|
|
907
|
+
// UI 面禁用内部工程词(check_in_blank / 闸拒绝 / ISO 字符串),只露「入住日/退房日」
|
|
908
|
+
// 与具体日期示例供模型追问。
|
|
845
909
|
const r = value;
|
|
910
|
+
if (r.verdict === 'input_required') {
|
|
911
|
+
const missingLabels = (r.missing ?? []).map(m => m === 'checkIn' ? '入住日' : '退房日');
|
|
912
|
+
const fieldText = missingLabels.length ? missingLabels.join('和') : '入住日和退房日';
|
|
913
|
+
const dateHint = !missingLabels.length || missingLabels.length === 2
|
|
914
|
+
? '例如 2026-09-18 / 2026-09-20'
|
|
915
|
+
: missingLabels[0] === '入住日' ? '例如 2026-09-18' : '例如 2026-09-20';
|
|
916
|
+
return {
|
|
917
|
+
card: 'generic',
|
|
918
|
+
title: `酒店:${r.destination ?? ''} 需要${fieldText}`,
|
|
919
|
+
// P2 措辞修正:直接对用户说话的句式,不再绕一道「请告诉.../请向用户确认」转给模型;
|
|
920
|
+
// gate.message 里残留的字段指引也归一为直接对用户措辞,保留日期示例等可行动细节。
|
|
921
|
+
content: [{ type: 'text', text: `请补充 ${fieldText}(${dateHint})后再次查询。\n${normalizeHotelGateHandoff(r.message)}` }],
|
|
922
|
+
};
|
|
923
|
+
}
|
|
846
924
|
const h = r.hotels;
|
|
847
925
|
const liveCount = Array.isArray(h) ? h.length : 0;
|
|
926
|
+
// 静态包是 {meta, stays} 对象而非数组(issue #24:此前计数恒 0 → UI 显示「无结果」)
|
|
848
927
|
const stays = !Array.isArray(h) && h && typeof h === 'object' ? h.stays : undefined;
|
|
849
928
|
const staticCount = Array.isArray(stays) ? stays.length : 0;
|
|
850
|
-
const tag = r.via === 'hbcli-realtime'
|
|
929
|
+
const tag = r.via === 'hbcli-realtime'
|
|
930
|
+
? (liveCount ? `实时 ${liveCount} 家` : '实时')
|
|
931
|
+
: staticCount ? `静态包 ${staticCount} 块` : (liveCount ? `${liveCount} 家` : '无结果');
|
|
851
932
|
return {
|
|
852
933
|
card: 'generic',
|
|
853
934
|
title: `酒店:${r.destination ?? ''} ${tag}`,
|
|
854
|
-
content: [
|
|
855
|
-
{
|
|
856
|
-
type: 'text',
|
|
857
|
-
text: String(r.summary ?? '')
|
|
858
|
-
}
|
|
859
|
-
]
|
|
935
|
+
content: [{ type: 'text', text: String(r.summary ?? '') }],
|
|
860
936
|
};
|
|
861
|
-
}
|
|
937
|
+
},
|
|
862
938
|
}));
|
|
863
939
|
registerGuarded(defineTool({
|
|
864
940
|
name: 'gotry_skeleton_check',
|
|
865
|
-
description: 'Check flight connectivity between two airports against the OpenFlights skeleton (free tier). '
|
|
941
|
+
description: 'Check flight connectivity between two airports against the OpenFlights skeleton (free tier). '
|
|
942
|
+
+ 'Three-valued: found = strong positive (airlines returned); hub-to-hub absence = downgrade signal, NEVER disproof '
|
|
943
|
+
+ '(skeleton lags reality); outside hub set = no conclusion. Use BEFORE recommending a route.',
|
|
866
944
|
parameters: {
|
|
867
|
-
from: {
|
|
868
|
-
|
|
869
|
-
required: true,
|
|
870
|
-
description: 'IATA, e.g. HKG'
|
|
871
|
-
},
|
|
872
|
-
to: {
|
|
873
|
-
type: 'string',
|
|
874
|
-
required: true,
|
|
875
|
-
description: 'IATA, e.g. HKT'
|
|
876
|
-
}
|
|
945
|
+
from: { type: 'string', required: true, description: 'IATA, e.g. HKG' },
|
|
946
|
+
to: { type: 'string', required: true, description: 'IATA, e.g. HKT' },
|
|
877
947
|
},
|
|
878
948
|
output: {
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
},
|
|
882
|
-
render: (_args, value)=>[
|
|
883
|
-
{
|
|
884
|
-
type: 'text',
|
|
885
|
-
text: String(value.evidence ?? JSON.stringify(value))
|
|
886
|
-
}
|
|
887
|
-
]
|
|
949
|
+
// loose json(与兄弟工具一致):strict additionalProperties:false 会拒 guard 的
|
|
950
|
+
// 错误兜底字段(ok/summary),校验错掩盖真实错误
|
|
951
|
+
schema: { type: 'json' },
|
|
952
|
+
render: (_args, value) => [{ type: 'text', text: String(value.evidence ?? JSON.stringify(value)) }],
|
|
888
953
|
},
|
|
889
|
-
async execute
|
|
954
|
+
async execute(args, _exec) {
|
|
955
|
+
// 平铺参数的工具也会被 LLM 包进 query(#12/#13 同款形态),unwrapQuery 兜住
|
|
890
956
|
const q = unwrapQuery(args);
|
|
891
957
|
const verdict = await checkConnectivity(String(q.from ?? ''), String(q.to ?? ''));
|
|
892
|
-
return JSON.parse(JSON.stringify({
|
|
893
|
-
ok: true,
|
|
894
|
-
...verdict
|
|
895
|
-
}));
|
|
958
|
+
return JSON.parse(JSON.stringify({ ok: true, ...verdict }));
|
|
896
959
|
},
|
|
897
|
-
presentCall:
|
|
898
|
-
card: 'generic',
|
|
899
|
-
title: `骨架校验:${args.from}-${args.to}`,
|
|
900
|
-
kind: 'execute',
|
|
901
|
-
rawInput: args
|
|
902
|
-
})
|
|
960
|
+
presentCall: args => ({ card: 'generic', title: `骨架校验:${args.from}-${args.to}`, kind: 'execute', rawInput: args }),
|
|
903
961
|
}));
|
|
904
962
|
registerGuarded(defineTool({
|
|
905
963
|
name: 'gotry_weather_check',
|
|
906
|
-
description: routed('gotry_weather_check', 'Check weather for a destination: forecast (≤16 days) or historical climate (seasonality baseline). '
|
|
964
|
+
description: routed('gotry_weather_check', 'Check weather for a destination: forecast (≤16 days) or historical climate (seasonality baseline). '
|
|
965
|
+
+ 'Free Open-Meteo API, no key required. Input: place name (Chinese ok) or lat/lng. '
|
|
966
|
+
+ 'Returns daily temp range, precipitation probability, weather code — with evidence chain tagging '
|
|
967
|
+
+ '[实时API:open-meteo@ts]. Use to ground seasonal advice in real data instead of LLM guessing.'),
|
|
907
968
|
parameters: {
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
},
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
},
|
|
916
|
-
lng: {
|
|
917
|
-
type: 'number',
|
|
918
|
-
description: '经度(与 lat 成对)'
|
|
919
|
-
},
|
|
920
|
-
month: {
|
|
921
|
-
type: 'integer',
|
|
922
|
-
description: '气候基线月份 1-12(mode=climate 或给了 month 未指 mode 时用)'
|
|
923
|
-
},
|
|
924
|
-
mode: {
|
|
925
|
-
type: 'string',
|
|
926
|
-
enum: [
|
|
927
|
-
'forecast',
|
|
928
|
-
'climate'
|
|
929
|
-
],
|
|
930
|
-
description: '默认 forecast'
|
|
931
|
-
},
|
|
932
|
-
days: {
|
|
933
|
-
type: 'integer',
|
|
934
|
-
description: '预报天数(≤16,默认 7)'
|
|
935
|
-
}
|
|
969
|
+
// D-30 第二刀(issue #112):query blob → 平铺 typed 契约(同 flyai 刀法)。
|
|
970
|
+
place: { type: 'string', description: '城市名(中文可),如 大理;与 lat/lng 二选一,都没给 execute 结构化报错' },
|
|
971
|
+
lat: { type: 'number', description: '纬度(与 lng 成对;给了则跳过地理编码)' },
|
|
972
|
+
lng: { type: 'number', description: '经度(与 lat 成对)' },
|
|
973
|
+
month: { type: 'integer', description: '气候基线月份 1-12(mode=climate 或给了 month 未指 mode 时用)' },
|
|
974
|
+
mode: { type: 'string', enum: ['forecast', 'climate'], description: '默认 forecast' },
|
|
975
|
+
days: { type: 'integer', description: '预报天数(≤16,默认 7)' },
|
|
936
976
|
},
|
|
937
977
|
output: {
|
|
938
|
-
schema: {
|
|
939
|
-
|
|
940
|
-
},
|
|
941
|
-
render: (_args, value)=>[
|
|
942
|
-
{
|
|
943
|
-
type: 'text',
|
|
944
|
-
text: String(value.summary ?? JSON.stringify(value).slice(0, 600))
|
|
945
|
-
}
|
|
946
|
-
]
|
|
978
|
+
schema: { type: 'json' },
|
|
979
|
+
render: (_args, value) => [{ type: 'text', text: String(value.summary ?? JSON.stringify(value).slice(0, 600)) }],
|
|
947
980
|
},
|
|
948
|
-
async execute
|
|
981
|
+
async execute(args, _exec) {
|
|
949
982
|
const q = args;
|
|
950
983
|
const started = Date.now();
|
|
951
984
|
let lat = q.lat, lng = q.lng;
|
|
952
985
|
let placeLabel = q.place ?? `${q.lat},${q.lng}`;
|
|
953
986
|
if (lat === undefined || lng === undefined) {
|
|
954
|
-
if (!q.place)
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
}
|
|
960
|
-
});
|
|
961
|
-
if (!geoItp.result) return declinedObservation('WEATHER_GEOCODE', geoItp.trace);
|
|
987
|
+
if (!q.place)
|
|
988
|
+
throw new Error('gotry_weather_check requires place name or lat/lng');
|
|
989
|
+
const geoItp = await interpretEffect({ effect: 'WEATHER_GEOCODE', params: { name: q.place } });
|
|
990
|
+
if (!geoItp.result)
|
|
991
|
+
return declinedObservation('WEATHER_GEOCODE', geoItp.trace);
|
|
962
992
|
const geo = geoItp.result;
|
|
963
993
|
if (!geo.ok || geo.results.length === 0) {
|
|
964
|
-
return JSON.parse(JSON.stringify({
|
|
965
|
-
ok: false,
|
|
966
|
-
summary: `地点「${q.place}」地理编码失败:${geo.error ?? '无结果'}`,
|
|
967
|
-
evidence: geo.evidence
|
|
968
|
-
}));
|
|
994
|
+
return JSON.parse(JSON.stringify({ ok: false, summary: `地点「${q.place}」地理编码失败:${geo.error ?? '无结果'}`, evidence: geo.evidence }));
|
|
969
995
|
}
|
|
970
996
|
const hit = geo.results[0];
|
|
971
997
|
lat = hit.latitude;
|
|
972
998
|
lng = hit.longitude;
|
|
973
999
|
placeLabel = `${hit.name}(${hit.admin1 ?? hit.country ?? ''})`;
|
|
974
1000
|
}
|
|
975
|
-
const isClimate = q.mode === 'climate' || q.month !== undefined && q.mode !== 'forecast';
|
|
976
|
-
const wxItp = isClimate
|
|
977
|
-
effect: 'WEATHER_CLIMATE',
|
|
978
|
-
params: {
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
month: q.month ?? new Date().getMonth() + 1
|
|
982
|
-
}
|
|
983
|
-
}) : await interpretEffect({
|
|
984
|
-
effect: 'WEATHER_FORECAST',
|
|
985
|
-
params: {
|
|
986
|
-
latitude: lat,
|
|
987
|
-
longitude: lng,
|
|
988
|
-
days: q.days
|
|
989
|
-
}
|
|
990
|
-
});
|
|
991
|
-
if (!wxItp.result) return declinedObservation(isClimate ? 'WEATHER_CLIMATE' : 'WEATHER_FORECAST', wxItp.trace);
|
|
1001
|
+
const isClimate = q.mode === 'climate' || (q.month !== undefined && q.mode !== 'forecast');
|
|
1002
|
+
const wxItp = isClimate
|
|
1003
|
+
? await interpretEffect({ effect: 'WEATHER_CLIMATE', params: { latitude: lat, longitude: lng, month: q.month ?? new Date().getMonth() + 1 } })
|
|
1004
|
+
: await interpretEffect({ effect: 'WEATHER_FORECAST', params: { latitude: lat, longitude: lng, days: q.days } });
|
|
1005
|
+
if (!wxItp.result)
|
|
1006
|
+
return declinedObservation(isClimate ? 'WEATHER_CLIMATE' : 'WEATHER_FORECAST', wxItp.trace);
|
|
992
1007
|
const r = wxItp.result;
|
|
993
1008
|
const dir = await ensureStateDir(config.stateRoot);
|
|
994
|
-
await recordLatency(join(dir, 'bridge-latency.jsonl'), Date.now() - started, `weather:${r.via}`).catch(()=>{});
|
|
995
|
-
const dailyLines = (r.daily ?? []).slice(0, 7).map(
|
|
996
|
-
const summary = r.ok
|
|
1009
|
+
await recordLatency(join(dir, 'bridge-latency.jsonl'), Date.now() - started, `weather:${r.via}`).catch(() => { });
|
|
1010
|
+
const dailyLines = (r.daily ?? []).slice(0, 7).map(d => `${d.date} ${d.tempMinC.toFixed(0)}–${d.tempMaxC.toFixed(0)}°C ${wmoLabel(d.weatherCode)}${d.precipProbMaxPct !== null ? ` 降水概率${d.precipProbMaxPct}%` : ''}`);
|
|
1011
|
+
const summary = r.ok
|
|
1012
|
+
? `${placeLabel}:${isClimate ? '历史气候' : `${q.days ?? 7} 天预报`}\n${dailyLines.join('\n')}\n${r.evidence}`
|
|
1013
|
+
: `${placeLabel}:天气查询失败(${r.error})${r.evidence}`;
|
|
997
1014
|
return JSON.parse(JSON.stringify({
|
|
998
|
-
ok: r.ok,
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
daily: r.daily,
|
|
1002
|
-
evidence: r.evidence,
|
|
1003
|
-
summary,
|
|
1004
|
-
latency_ms: Date.now() - started
|
|
1015
|
+
ok: r.ok, place: placeLabel, mode: isClimate ? 'climate' : 'forecast',
|
|
1016
|
+
daily: r.daily, evidence: r.evidence, summary,
|
|
1017
|
+
latency_ms: Date.now() - started,
|
|
1005
1018
|
}));
|
|
1006
1019
|
},
|
|
1007
|
-
presentCall:
|
|
1008
|
-
|
|
1009
|
-
title: `天气:${args.place ?? `${args.lat ?? ''},${args.lng ?? ''}`}`,
|
|
1010
|
-
kind: 'fetch',
|
|
1011
|
-
rawInput: args
|
|
1012
|
-
}),
|
|
1013
|
-
presentResult: (args, value)=>{
|
|
1020
|
+
presentCall: args => ({ card: 'generic', title: `天气:${args.place ?? `${args.lat ?? ''},${args.lng ?? ''}`}`, kind: 'fetch', rawInput: args }),
|
|
1021
|
+
presentResult: (args, value) => {
|
|
1014
1022
|
const r = value;
|
|
1015
1023
|
const place = String(args.place ?? '');
|
|
1016
1024
|
const failed = String(r.summary ?? '').includes('降级') || String(r.summary ?? '').includes('unavailable');
|
|
1017
1025
|
return {
|
|
1018
1026
|
card: 'generic',
|
|
1019
1027
|
title: `天气:${place} ${failed ? '降级' : 'ok'}`,
|
|
1020
|
-
content: [
|
|
1021
|
-
{
|
|
1022
|
-
type: 'text',
|
|
1023
|
-
text: String(r.summary ?? '')
|
|
1024
|
-
}
|
|
1025
|
-
]
|
|
1028
|
+
content: [{ type: 'text', text: String(r.summary ?? '') }],
|
|
1026
1029
|
};
|
|
1027
|
-
}
|
|
1030
|
+
},
|
|
1028
1031
|
}));
|
|
1029
1032
|
registerGuarded(defineTool({
|
|
1030
1033
|
name: 'gotry_flight_verify',
|
|
1031
|
-
description: routed('gotry_flight_verify', 'Verify whether a flight callsign is currently observable on the OpenSky ADS-B network. '
|
|
1034
|
+
description: routed('gotry_flight_verify', 'Verify whether a flight callsign is currently observable on the OpenSky ADS-B network. '
|
|
1035
|
+
+ 'Free anonymous API (~400 credits/day, 4 req/s burst). Three-valued semantics: '
|
|
1036
|
+
+ 'observed = strong positive (the aircraft is currently being broadcast); '
|
|
1037
|
+
+ 'not_observed = no conclusion (ADS-B coverage is limited by geography/altitude — '
|
|
1038
|
+
+ 'a missing signal does NOT disprove the flight); '
|
|
1039
|
+
+ 'unavailable = API failure, gracefully degraded. '
|
|
1040
|
+
+ 'Use to ground "is this flight actually flying right now?" in real data, complementing '
|
|
1041
|
+
+ 'the OpenFlights skeleton (historical connectivity) and the static flight pack (planned schedule).'),
|
|
1042
|
+
// D-30 第四刀(issue #112):平铺 typed;callsign required → 宿主权即拒
|
|
1032
1043
|
parameters: {
|
|
1033
|
-
callsign: {
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
description: '航班呼号,如 EK329'
|
|
1037
|
-
},
|
|
1038
|
-
airport: {
|
|
1039
|
-
type: 'string',
|
|
1040
|
-
description: '预期机场 IATA,如 OMDB(观测命中时展示)'
|
|
1041
|
-
},
|
|
1042
|
-
timeoutMs: {
|
|
1043
|
-
type: 'integer',
|
|
1044
|
-
description: '超时毫秒,默认 10000'
|
|
1045
|
-
}
|
|
1044
|
+
callsign: { type: 'string', required: true, description: '航班呼号,如 EK329' },
|
|
1045
|
+
airport: { type: 'string', description: '预期机场 IATA,如 OMDB(观测命中时展示)' },
|
|
1046
|
+
timeoutMs: { type: 'integer', description: '超时毫秒,默认 10000' },
|
|
1046
1047
|
},
|
|
1047
1048
|
output: {
|
|
1048
|
-
schema: {
|
|
1049
|
-
|
|
1050
|
-
},
|
|
1051
|
-
render: (_args, value)=>[
|
|
1052
|
-
{
|
|
1053
|
-
type: 'text',
|
|
1054
|
-
text: String(value.summary ?? JSON.stringify(value).slice(0, 500))
|
|
1055
|
-
}
|
|
1056
|
-
]
|
|
1049
|
+
schema: { type: 'json' },
|
|
1050
|
+
render: (_args, value) => [{ type: 'text', text: String(value.summary ?? JSON.stringify(value).slice(0, 500)) }],
|
|
1057
1051
|
},
|
|
1058
|
-
async execute
|
|
1052
|
+
async execute(args, _exec) {
|
|
1059
1053
|
const q = args;
|
|
1060
1054
|
const started = Date.now();
|
|
1061
1055
|
if (!q.callsign) {
|
|
1062
|
-
return JSON.parse(JSON.stringify({
|
|
1063
|
-
verdict: 'unavailable',
|
|
1064
|
-
evidence: '[校验不可用:无 callsign]',
|
|
1065
|
-
summary: 'callsign 必填'
|
|
1066
|
-
}));
|
|
1056
|
+
return JSON.parse(JSON.stringify({ verdict: 'unavailable', evidence: '[校验不可用:无 callsign]', summary: 'callsign 必填' }));
|
|
1067
1057
|
}
|
|
1068
|
-
const itp = await interpretEffect({
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
callsign: q.callsign,
|
|
1072
|
-
airport: q.airport,
|
|
1073
|
-
timeoutMs: q.timeoutMs
|
|
1074
|
-
}
|
|
1075
|
-
});
|
|
1076
|
-
if (!itp.result) return declinedObservation('OPENSKY_FLIGHT_VERIFY', itp.trace);
|
|
1058
|
+
const itp = await interpretEffect({ effect: 'OPENSKY_FLIGHT_VERIFY', params: { callsign: q.callsign, airport: q.airport, timeoutMs: q.timeoutMs } });
|
|
1059
|
+
if (!itp.result)
|
|
1060
|
+
return declinedObservation('OPENSKY_FLIGHT_VERIFY', itp.trace);
|
|
1077
1061
|
const r = itp.result;
|
|
1078
1062
|
const dir = await ensureStateDir(config.stateRoot);
|
|
1079
|
-
await recordLatency(join(dir, 'bridge-latency.jsonl'), Date.now() - started, `flight_verify:${r.via}`).catch(()=>{});
|
|
1080
|
-
const summary = r.verdict === 'observed'
|
|
1063
|
+
await recordLatency(join(dir, 'bridge-latency.jsonl'), Date.now() - started, `flight_verify:${r.via}`).catch(() => { });
|
|
1064
|
+
const summary = r.verdict === 'observed'
|
|
1065
|
+
? `${r.callsign} 当前 ADS-B 观测命中 (${r.hits?.length ?? 0} 架)${r.airport ? ` 在 ${r.airport}` : ''}\n${r.evidence}`
|
|
1066
|
+
: r.verdict === 'not_observed'
|
|
1067
|
+
? `${r.callsign} 当前观测列表未见(ADS-B 覆盖有限,不否定该航班存在)\n${r.evidence}`
|
|
1068
|
+
: `${r.callsign} OpenSky 不可用:${r.error}\n${r.evidence}`;
|
|
1081
1069
|
return JSON.parse(JSON.stringify({
|
|
1082
1070
|
ok: true,
|
|
1083
|
-
verdict: r.verdict,
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
sample_size: r.sampleSize,
|
|
1087
|
-
hits: r.hits,
|
|
1088
|
-
evidence: r.evidence,
|
|
1089
|
-
summary,
|
|
1090
|
-
latency_ms: Date.now() - started
|
|
1071
|
+
verdict: r.verdict, callsign: r.callsign, airport: r.airport,
|
|
1072
|
+
sample_size: r.sampleSize, hits: r.hits, evidence: r.evidence, summary,
|
|
1073
|
+
latency_ms: Date.now() - started,
|
|
1091
1074
|
}));
|
|
1092
1075
|
},
|
|
1093
|
-
presentCall:
|
|
1094
|
-
card: 'generic',
|
|
1095
|
-
title: `飞行校验:${args.callsign ?? ''}`,
|
|
1096
|
-
kind: 'fetch',
|
|
1097
|
-
rawInput: args
|
|
1098
|
-
})
|
|
1076
|
+
presentCall: args => ({ card: 'generic', title: `飞行校验:${args.callsign ?? ''}`, kind: 'fetch', rawInput: args }),
|
|
1099
1077
|
}));
|
|
1100
1078
|
registerGuarded(defineTool({
|
|
1101
1079
|
name: 'gotry_flyai_search',
|
|
1102
|
-
description: routed('gotry_flyai_search', 'Live travel search through the Fliggy official FlyAI channel (read-only, no key; booking/comparison happens by the HUMAN on the jumpUrl page). '
|
|
1080
|
+
description: routed('gotry_flyai_search', 'Live travel search through the Fliggy official FlyAI channel (read-only, no key; booking/comparison happens by the HUMAN on the jumpUrl page). '
|
|
1081
|
+
+ 'kind="flight"|"train": from/to 中文城市名 + date YYYY-MM-DD — real schedules & prices, split 直达/中转 in results. '
|
|
1082
|
+
+ 'kind="hotel": to=目的地中文(如 大理), checkIn/checkOut (YYYY-MM-DD,成对可选——未定档期可不填先摸底), keyWords?. '
|
|
1083
|
+
+ 'Hotel prices may be masked upstream (priceRaw like "¥7xx"): always present the mask as a range, and let the human open jumpUrl for the real price. '
|
|
1084
|
+
+ 'Evidence [实时API:flyai@ts]. verdict=needs-setup (anonymous trial quota exhausted, upstream 429) is a CONFIG issue, not a search failure: surface the setup hint once, do NOT retry this tool this session — switch to gotry_session_search or web search. '
|
|
1085
|
+
+ 'Errors (rate-limit Sentinel / invalid dates) degrade as structured errors with the upstream message — surface them, never guess.'),
|
|
1086
|
+
// D-30 第一刀(issue #112):query json blob → 平铺 typed 契约。逐字段 schema 由 dsh
|
|
1087
|
+
// parameterSchemaSpecToJsonSchema 投影为模型可见 JSON Schema,validateArgs 在 execute 前
|
|
1088
|
+
// 宿主权校验——畸形参数(缺 kind/枚举外值/类型错/legacy blob 包裹)在入口即被结构化拒绝
|
|
1089
|
+
// (ToolArgsError → guardToolExecute 兜成 ADR-13 ToolFailure,形状由迁移测试锁死)。
|
|
1090
|
+
// 条件必填(机/火要 from/to/date,酒店要 to)不在 schema 强制,仍由 execute 结构化报错给方向。
|
|
1103
1091
|
parameters: {
|
|
1104
|
-
kind: {
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
from: {
|
|
1115
|
-
type: 'string',
|
|
1116
|
-
description: '出发城市中文,如 上海——kind=flight|train 必填'
|
|
1117
|
-
},
|
|
1118
|
-
to: {
|
|
1119
|
-
type: 'string',
|
|
1120
|
-
description: '到达城市中文,如 丽江;kind=hotel 时为目的地,如 大理(必填)'
|
|
1121
|
-
},
|
|
1122
|
-
date: {
|
|
1123
|
-
type: 'string',
|
|
1124
|
-
description: '出发日期 YYYY-MM-DD——kind=flight|train 必填,须为今天或未来'
|
|
1125
|
-
},
|
|
1126
|
-
checkIn: {
|
|
1127
|
-
type: 'string',
|
|
1128
|
-
description: '入住日期 YYYY-MM-DD,仅 kind=hotel;与 checkOut 成对可选(未定档期可不填先摸底)'
|
|
1129
|
-
},
|
|
1130
|
-
checkOut: {
|
|
1131
|
-
type: 'string',
|
|
1132
|
-
description: '退房日期 YYYY-MM-DD,仅 kind=hotel;与 checkIn 成对'
|
|
1133
|
-
},
|
|
1134
|
-
keyWords: {
|
|
1135
|
-
type: 'string',
|
|
1136
|
-
description: '酒店关键词,如 洱海——仅 kind=hotel 可选'
|
|
1137
|
-
}
|
|
1138
|
-
},
|
|
1139
|
-
output: {
|
|
1140
|
-
schema: {
|
|
1141
|
-
type: 'json'
|
|
1142
|
-
},
|
|
1143
|
-
render: (_a, v)=>[
|
|
1144
|
-
{
|
|
1145
|
-
type: 'text',
|
|
1146
|
-
text: String(v.summary ?? JSON.stringify(v).slice(0, 600))
|
|
1147
|
-
}
|
|
1148
|
-
]
|
|
1149
|
-
},
|
|
1150
|
-
async execute (args, _exec) {
|
|
1092
|
+
kind: { type: 'string', enum: ['flight', 'train', 'hotel'], required: true, description: '检索类型:flight 机票 / train 火车 / hotel 酒店' },
|
|
1093
|
+
from: { type: 'string', description: '出发城市中文,如 上海——kind=flight|train 必填' },
|
|
1094
|
+
to: { type: 'string', description: '到达城市中文,如 丽江;kind=hotel 时为目的地,如 大理(必填)' },
|
|
1095
|
+
date: { type: 'string', description: '出发日期 YYYY-MM-DD——kind=flight|train 必填,须为今天或未来' },
|
|
1096
|
+
checkIn: { type: 'string', description: '入住日期 YYYY-MM-DD,仅 kind=hotel;与 checkOut 成对可选(未定档期可不填先摸底)' },
|
|
1097
|
+
checkOut: { type: 'string', description: '退房日期 YYYY-MM-DD,仅 kind=hotel;与 checkIn 成对' },
|
|
1098
|
+
keyWords: { type: 'string', description: '酒店关键词,如 洱海——仅 kind=hotel 可选' },
|
|
1099
|
+
},
|
|
1100
|
+
output: { schema: { type: 'json' }, render: (_a, v) => [{ type: 'text', text: String(v.summary ?? JSON.stringify(v).slice(0, 600)) }] },
|
|
1101
|
+
async execute(args, _exec) {
|
|
1151
1102
|
const q = args;
|
|
1152
1103
|
if (q.kind === 'hotel') {
|
|
1153
1104
|
const dest = (q.to ?? '').trim();
|
|
1154
|
-
if (!dest)
|
|
1155
|
-
ok: false,
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
if ((q.checkIn ? 1 : 0) !== (q.checkOut ? 1 : 0) || q.checkIn && !/^\d{4}-\d{2}-\d{2}$/.test(q.checkIn)) {
|
|
1159
|
-
return {
|
|
1160
|
-
ok: false,
|
|
1161
|
-
summary: '酒店 checkIn/checkOut 须成对且为 YYYY-MM-DD(未定档期可不填,先摸底)'
|
|
1162
|
-
};
|
|
1105
|
+
if (!dest)
|
|
1106
|
+
return { ok: false, summary: 'kind=hotel 需要 to(目的地中文,如 大理)' };
|
|
1107
|
+
if ((q.checkIn ? 1 : 0) !== (q.checkOut ? 1 : 0) || (q.checkIn && !/^\d{4}-\d{2}-\d{2}$/.test(q.checkIn))) {
|
|
1108
|
+
return { ok: false, summary: '酒店 checkIn/checkOut 须成对且为 YYYY-MM-DD(未定档期可不填,先摸底)' };
|
|
1163
1109
|
}
|
|
1110
|
+
// 过去入住日同机/火过去日期闸(issue #24 同构;分层纪律:日期算术在代码层)
|
|
1164
1111
|
if (q.checkIn) {
|
|
1165
1112
|
const now = new Date();
|
|
1166
1113
|
const todayYmd = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
|
|
1167
1114
|
if (q.checkIn < todayYmd || (q.checkOut ?? '') < q.checkIn) {
|
|
1168
1115
|
return JSON.parse(JSON.stringify({
|
|
1169
|
-
ok: false,
|
|
1170
|
-
|
|
1171
|
-
kind: 'hotel',
|
|
1172
|
-
summary: `未发起查询:入住 ${q.checkIn}/退房 ${q.checkOut ?? ''} 不是未来合法区间(今天 ${todayYmd})。向用户确认日期后再查。`
|
|
1116
|
+
ok: false, verdict: 'error', kind: 'hotel',
|
|
1117
|
+
summary: `未发起查询:入住 ${q.checkIn}/退房 ${q.checkOut ?? ''} 不是未来合法区间(今天 ${todayYmd})。向用户确认日期后再查。`,
|
|
1173
1118
|
}));
|
|
1174
1119
|
}
|
|
1175
1120
|
if ((q.checkOut ?? '').length !== 10) {
|
|
1176
|
-
return {
|
|
1177
|
-
ok: false,
|
|
1178
|
-
summary: 'checkOut 需 YYYY-MM-DD(与 checkIn 成对)'
|
|
1179
|
-
};
|
|
1121
|
+
return { ok: false, summary: 'checkOut 需 YYYY-MM-DD(与 checkIn 成对)' };
|
|
1180
1122
|
}
|
|
1181
1123
|
}
|
|
1182
|
-
const itp = await
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
kind: 'hotel',
|
|
1186
|
-
destName: dest,
|
|
1187
|
-
checkInDate: q.checkIn,
|
|
1188
|
-
checkOutDate: q.checkOut,
|
|
1189
|
-
keyWords: q.keyWords
|
|
1190
|
-
}
|
|
1191
|
-
});
|
|
1192
|
-
if (!itp.result) return declinedObservation('FLYAI_SEARCH', itp.trace);
|
|
1124
|
+
const itp = await runFlyaiEffect({ effect: 'FLYAI_SEARCH', params: { kind: 'hotel', destName: dest, checkInDate: q.checkIn, checkOutDate: q.checkOut, keyWords: q.keyWords } });
|
|
1125
|
+
if (!itp.result)
|
|
1126
|
+
return declinedObservation('FLYAI_SEARCH', itp.trace);
|
|
1193
1127
|
const r = itp.result;
|
|
1194
1128
|
await noteChannel('flyai', r.verdict);
|
|
1129
|
+
// D-26(issue #118):exact-date 酒店检索 hit/miss 落账(摸底无档期不落,传输失败不落负事实)
|
|
1195
1130
|
await appendFacts(config.stateRoot ?? '.', factsFromHotel({
|
|
1196
|
-
source: 'flyai-hotel',
|
|
1197
|
-
|
|
1198
|
-
checkIn: q.checkIn,
|
|
1199
|
-
checkOut: q.checkOut,
|
|
1200
|
-
verdict: r.verdict,
|
|
1201
|
-
options: r.hotels?.length ?? 0,
|
|
1202
|
-
evidence: r.evidence,
|
|
1203
|
-
fetchedAt: new Date().toISOString()
|
|
1204
|
-
}));
|
|
1205
|
-
const top = (r.hotels ?? []).slice(0, 8).map((o)=>`${o.name}${o.star ? `(${o.star})` : ''} ${o.priceRaw ?? '价待询'}${o.poi ? ` · ${o.poi}` : ''}`);
|
|
1206
|
-
const summary = r.verdict === 'hit' ? `${dest} 酒店(飞猪官方只读)前 ${top.length} 家(价格多为打码,真实价以 jumpUrl 为准):\n${top.join('\n')}\n${r.evidence}` : r.verdict === 'needs-setup' ? `${dest} 酒店检索未发起(配置问题,非检索失败):${r.error ?? ''}\n${r.setup ?? ''}\n状态体检可调 gotry_doctor。${r.evidence}` : `${dest} 酒店无结果或失败:${r.error ?? 'miss'} ${r.evidence}`;
|
|
1207
|
-
return JSON.parse(JSON.stringify({
|
|
1208
|
-
...r,
|
|
1209
|
-
summary,
|
|
1210
|
-
...r.verdict !== 'hit' ? routingField('search-hotel', 'flyai') : {}
|
|
1131
|
+
source: 'flyai-hotel', destination: dest, checkIn: q.checkIn, checkOut: q.checkOut,
|
|
1132
|
+
verdict: r.verdict, options: r.hotels?.length ?? 0, evidence: r.evidence, fetchedAt: new Date().toISOString(),
|
|
1211
1133
|
}));
|
|
1134
|
+
const top = (r.hotels ?? []).slice(0, 8).map(o => `${o.name}${o.star ? `(${o.star})` : ''} ${o.priceRaw ?? '价待询'}${o.poi ? ` · ${o.poi}` : ''}`);
|
|
1135
|
+
const summary = r.verdict === 'hit'
|
|
1136
|
+
? `${dest} 酒店(飞猪官方只读)前 ${top.length} 家(价格多为打码,真实价以 jumpUrl 为准):\n${top.join('\n')}\n${r.evidence}`
|
|
1137
|
+
: r.verdict === 'needs-setup'
|
|
1138
|
+
? `${dest} 酒店检索未发起(配置问题,非检索失败):${r.error ?? ''}\n${r.setup ?? ''}\n状态体检可调 gotry_doctor。${r.evidence}`
|
|
1139
|
+
: `${dest} 酒店无结果或失败:${r.error ?? 'miss'} ${r.evidence}`;
|
|
1140
|
+
return JSON.parse(JSON.stringify({ ...r, summary, ...(r.verdict !== 'hit' ? routingField('search-hotel', 'flyai') : {}) }));
|
|
1212
1141
|
}
|
|
1213
1142
|
const kind = q.kind === 'train' ? 'train' : 'flight';
|
|
1214
1143
|
if (!q.from || !q.to || !q.date) {
|
|
1215
|
-
return {
|
|
1216
|
-
ok: false,
|
|
1217
|
-
summary: '需要 from/to(中文城市名)与 date(YYYY-MM-DD)'
|
|
1218
|
-
};
|
|
1144
|
+
return { ok: false, summary: '需要 from/to(中文城市名)与 date(YYYY-MM-DD)' };
|
|
1219
1145
|
}
|
|
1146
|
+
// 过去日期预校验(issue #24,代码层算术;分层纪律):用户说「7.18」未带年份时模型会落到当前年,
|
|
1147
|
+
// 而今天可能已在 8 月——过去日期上游必拒(「出发日期非法」)。拦在发查询之前并指明修正方向,
|
|
1148
|
+
// 不让模型从 miss 里猜因。
|
|
1220
1149
|
const now = new Date();
|
|
1221
1150
|
const todayYmd = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
|
|
1222
1151
|
if (q.date < todayYmd) {
|
|
1223
1152
|
return JSON.parse(JSON.stringify({
|
|
1224
|
-
ok: false,
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
summary: `未发起查询:日期 ${q.date} 已是过去(今天 ${todayYmd}),过去不存在在售机/火车票。` + `多为用户时间表达未带年份所致——向用户确认年份(或按未来最近的同月日修正)后再查。`
|
|
1153
|
+
ok: false, verdict: 'error', kind,
|
|
1154
|
+
summary: `未发起查询:日期 ${q.date} 已是过去(今天 ${todayYmd}),过去不存在在售机/火车票。`
|
|
1155
|
+
+ `多为用户时间表达未带年份所致——向用户确认年份(或按未来最近的同月日修正)后再查。`,
|
|
1228
1156
|
}));
|
|
1229
1157
|
}
|
|
1230
|
-
const itp = await
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
kind,
|
|
1234
|
-
origin: q.from,
|
|
1235
|
-
destination: q.to,
|
|
1236
|
-
depDate: q.date
|
|
1237
|
-
}
|
|
1238
|
-
});
|
|
1239
|
-
if (!itp.result) return declinedObservation('FLYAI_SEARCH', itp.trace);
|
|
1158
|
+
const itp = await runFlyaiEffect({ effect: 'FLYAI_SEARCH', params: { kind, origin: q.from, destination: q.to, depDate: q.date } });
|
|
1159
|
+
if (!itp.result)
|
|
1160
|
+
return declinedObservation('FLYAI_SEARCH', itp.trace);
|
|
1240
1161
|
const r = itp.result;
|
|
1241
1162
|
await noteChannel('flyai', r.verdict);
|
|
1163
|
+
// issue #46 事实落账(ADR-19):exact-date 检索结论(hit 正事实 / miss 负事实)追加进
|
|
1164
|
+
// bookable-facts 侧车——产物事实闸(gotry_fact_gate)的唯一事实源;落盘失败不阻塞检索
|
|
1242
1165
|
const avMap = await loadAirlineAirportMap();
|
|
1243
|
-
await appendFacts(config.stateRoot ?? '.', factsFromFlyai({
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
date: q.date
|
|
1248
|
-
}, r, new Date().toISOString(), avMap?.city_alias));
|
|
1249
|
-
const top = (r.options ?? []).slice(0, 8).map((o)=>`${o.no} ${o.name} ${o.depDateTime.slice(11, 16)}→${o.arrDateTime.slice(11, 16)} ¥${o.price}`);
|
|
1166
|
+
await appendFacts(config.stateRoot ?? '.', factsFromFlyai({ kind, origin: q.from, destination: q.to, date: q.date }, r, new Date().toISOString(), avMap?.city_alias));
|
|
1167
|
+
const top = (r.options ?? []).slice(0, 8).map(o => `${o.no} ${o.name} ${o.depDateTime.slice(11, 16)}→${o.arrDateTime.slice(11, 16)} ¥${o.price}`);
|
|
1168
|
+
// issue #24:miss(上游正常返回 0 条)与 error(限流/网络)分开陈述,不再混写「无结果或失败」
|
|
1169
|
+
// (过去日期已被上方预校验拦下,此处不会出现过期查询)
|
|
1250
1170
|
const label = kind === 'flight' ? '机票' : '火车票';
|
|
1251
|
-
const summary = r.verdict === 'hit'
|
|
1171
|
+
const summary = r.verdict === 'hit'
|
|
1172
|
+
? `${q.from}→${q.to} ${q.date} ${label}(飞猪官方只读)前 ${top.length} 条:\n${top.join('\n')}\n${r.evidence}`
|
|
1173
|
+
: r.verdict === 'miss'
|
|
1174
|
+
? `${q.from}→${q.to} ${q.date} ${label}官方通道正常返回 0 条(常见原因:航线未开放/当日售罄)。${r.evidence}`
|
|
1175
|
+
: r.verdict === 'needs-setup'
|
|
1176
|
+
? `${q.from}→${q.to} ${q.date} ${label}检索未发起(配置问题,非检索失败):${r.error ?? ''}\n${r.setup ?? ''}\n状态体检可调 gotry_doctor。${r.evidence}`
|
|
1177
|
+
: `${q.from}→${q.to} ${q.date} ${label}检索失败(可能限流/网络):${r.error ?? ''} ${r.evidence}`;
|
|
1252
1178
|
return JSON.parse(JSON.stringify({
|
|
1253
|
-
...r,
|
|
1254
|
-
kind,
|
|
1255
|
-
summary,
|
|
1256
|
-
...r.verdict !== 'hit' ? routingField(kind === 'train' ? 'search-train' : 'search-flight', 'flyai') : {}
|
|
1179
|
+
...r, kind, summary,
|
|
1180
|
+
...(r.verdict !== 'hit' ? routingField(kind === 'train' ? 'search-train' : 'search-flight', 'flyai') : {}),
|
|
1257
1181
|
}));
|
|
1258
1182
|
},
|
|
1259
|
-
presentCall:
|
|
1260
|
-
|
|
1261
|
-
title: `官方检索:${args.kind}`,
|
|
1262
|
-
kind: 'fetch',
|
|
1263
|
-
rawInput: args
|
|
1264
|
-
}),
|
|
1265
|
-
presentResult: (args, value)=>{
|
|
1183
|
+
presentCall: args => ({ card: 'generic', title: `官方检索:${args.kind}`, kind: 'fetch', rawInput: args }),
|
|
1184
|
+
presentResult: (args, value) => {
|
|
1266
1185
|
const isHotel = args.kind === 'hotel';
|
|
1267
1186
|
const r = value;
|
|
1268
1187
|
const n = Math.max((r.options ?? []).length, (r.hotels ?? []).length);
|
|
1269
|
-
return {
|
|
1270
|
-
|
|
1271
|
-
title: `${isHotel ? '飞猪酒店' : '飞猪检索'}:${r.ok && n > 0 ? `${n} 条` : '降级'}`,
|
|
1272
|
-
content: [
|
|
1273
|
-
{
|
|
1274
|
-
type: 'text',
|
|
1275
|
-
text: String(value.summary ?? '')
|
|
1276
|
-
}
|
|
1277
|
-
]
|
|
1278
|
-
};
|
|
1279
|
-
}
|
|
1188
|
+
return { card: 'generic', title: `${isHotel ? '飞猪酒店' : '飞猪检索'}:${r.ok && n > 0 ? `${n} 条` : '降级'}`, content: [{ type: 'text', text: String(value.summary ?? '') }] };
|
|
1189
|
+
},
|
|
1280
1190
|
}));
|
|
1281
1191
|
registerGuarded(defineTool({
|
|
1282
1192
|
name: 'gotry_session_login',
|
|
1283
|
-
description: 'Productized login bootstrap for the account session channel (call this when gotry_session_search returns needs-login — the user never needs a terminal). ' + 'AUTO-DETECTION FIRST: it reads ticket-cookie NAMES before anything else — if the user already logged in (on the external site) it confirms instantly WITHOUT opening any page. '
|
|
1193
|
+
description: 'Productized login bootstrap for the account session channel (call this when gotry_session_search returns needs-login — the user never needs a terminal). ' + 'AUTO-DETECTION FIRST: it reads ticket-cookie NAMES before anything else — if the user already logged in (on the external site) it confirms instantly WITHOUT opening any page. '
|
|
1194
|
+
+ 'OPENS the site login entry in the USER\'S OWN Chrome (foreground tab, left open for the user) and waits for the user to finish logging in on the external site. '
|
|
1195
|
+
+ 'GoTry NEVER collects, stores, or transmits credentials: no passwords, no SMS codes, no cookie values — it only checks the boolean fact "already logged in" (reads cookie NAMES only, zero values). '
|
|
1196
|
+
+ 'Transport: the GoTry Session Bridge browser extension (one-time install, ZERO Chrome system dialogs). '
|
|
1197
|
+
+ `verdict logged-in (tickets detected) | pending (login tab opened, user not done yet — offer to re-check later) | needs-extension (one-time extension install: the verdict surfaces the Chrome Web Store installUrl as a clickable link for dsh UI to render — installation is a browser concern, not gotry's). `
|
|
1198
|
+
+ 'Evidence [会话:<site>-login@ts].',
|
|
1199
|
+
// D-30 第三刀(issue #112):query blob → 平铺 typed;全字段可选 → interpretArgs 容忍层
|
|
1284
1200
|
parameters: {
|
|
1285
|
-
waitSeconds: {
|
|
1286
|
-
type: 'integer',
|
|
1287
|
-
description: '等待用户完成登录的上限秒数,默认 90,至多 300;不传用默认'
|
|
1288
|
-
}
|
|
1289
|
-
},
|
|
1290
|
-
output: {
|
|
1291
|
-
schema: {
|
|
1292
|
-
type: 'json'
|
|
1293
|
-
},
|
|
1294
|
-
render: (_a, v)=>[
|
|
1295
|
-
{
|
|
1296
|
-
type: 'text',
|
|
1297
|
-
text: String(v.evidence ?? JSON.stringify(v).slice(0, 600))
|
|
1298
|
-
}
|
|
1299
|
-
]
|
|
1201
|
+
waitSeconds: { type: 'integer', description: '等待用户完成登录的上限秒数,默认 90,至多 300;不传用默认' },
|
|
1300
1202
|
},
|
|
1301
|
-
|
|
1203
|
+
output: { schema: { type: 'json' }, render: (_a, v) => [{ type: 'text', text: String(v.evidence ?? JSON.stringify(v).slice(0, 600)) }] },
|
|
1204
|
+
async execute(args, _exec) {
|
|
1302
1205
|
const q = unwrapQuery(args, 'waitSeconds');
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
}
|
|
1308
|
-
});
|
|
1309
|
-
if (!itpL.result) return declinedObservation('SESSION_LOGIN', itpL.trace);
|
|
1206
|
+
// D-23 收编(issue #115):登录引导入效应注册表(SESSION_LOGIN,浏览器族永不重试不熔断)
|
|
1207
|
+
const itpL = await interpretEffect({ effect: 'SESSION_LOGIN', params: { waitSeconds: q?.waitSeconds } });
|
|
1208
|
+
if (!itpL.result)
|
|
1209
|
+
return declinedObservation('SESSION_LOGIN', itpL.trace);
|
|
1310
1210
|
const r = itpL.result;
|
|
1311
|
-
const summary = r.verdict === 'logged-in'
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
presentResult: (_args, value)=>{
|
|
1211
|
+
const summary = r.verdict === 'logged-in'
|
|
1212
|
+
? `${r.site} 登录完成确认(票据 cookie 名已检出,只读名字)。说明:登录是在携程官网、用你自己的浏览器完成的——gotry 全程未接触任何密码/验证码/cookie 值。现在可以继续会话检索了。${r.evidence}`
|
|
1213
|
+
: r.verdict === 'pending'
|
|
1214
|
+
? `登录入口已在你的 Chrome 打开;请在弹出的标签页里正常登录携程(登录由你在官网完成,不属于 gotry)。完成后说一声"继续",我再确认。gotry 只检查"是否已登录",永不收集你的账号信息。${r.evidence}`
|
|
1215
|
+
: r.verdict === 'needs-extension'
|
|
1216
|
+
? `需要一次性安装 GoTry Session Bridge 浏览器扩展:这是浏览器的事,gotry 不弹面板不开剪贴板——请直接在 Chrome 应用商店一键装(add-to-chrome 自动更新)。storeUrl=${EXTENSION_STORE_URL}. 装好即生效,装完后告诉我「重试」即可。${r.evidence}`
|
|
1217
|
+
: r.verdict === 'needs-attach'
|
|
1218
|
+
? `cdp 车道需要一次性开启你 Chrome 的远程调试开关:在你的 Chrome 地址栏打开 chrome://inspect/#remote-debugging 并打开开关,然后说一声"重试"(默认走扩展车道,无需此步)。${r.evidence}`
|
|
1219
|
+
: `登录引导未完成:${r.error ?? '未知原因'} ${r.evidence}`;
|
|
1220
|
+
return JSON.parse(JSON.stringify({ ...r, summary }));
|
|
1221
|
+
},
|
|
1222
|
+
presentCall: _args => ({ card: 'generic', title: '登录携程(用你自己的浏览器,不在 gotry 输入)', kind: 'fetch', rawInput: {} }),
|
|
1223
|
+
presentResult: (_args, value) => {
|
|
1324
1224
|
const r = value;
|
|
1325
1225
|
const needsExt = r.verdict === 'needs-extension';
|
|
1326
|
-
const label = r.verdict === 'logged-in'
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
content
|
|
1341
|
-
};
|
|
1342
|
-
}
|
|
1226
|
+
const label = r.verdict === 'logged-in'
|
|
1227
|
+
? `已登录(${(r.tickets ?? []).length} 票据)`
|
|
1228
|
+
: r.verdict === 'pending'
|
|
1229
|
+
? '等待你在携程页面完成登录'
|
|
1230
|
+
: needsExt
|
|
1231
|
+
? '🧩 需装 GoTry Session Bridge 扩展(浏览器商店一键装,自动更新)'
|
|
1232
|
+
: r.verdict ?? '降级';
|
|
1233
|
+
const content = [{ type: 'text', text: String(value.summary ?? '') }];
|
|
1234
|
+
// needs-extension:把商店 URL 渲成可点链接(浏览器自己当安装器,gotry 不插手)
|
|
1235
|
+
// dsh presentResult 若支持 actions 字段,URL 可被原生渲染;否则 content 里也保留文本兜底
|
|
1236
|
+
if (needsExt && r.installUrl)
|
|
1237
|
+
content.push({ type: 'text', text: `安装链接:${r.installUrl}` });
|
|
1238
|
+
return { card: 'generic', title: `账号登录:${label}`, content };
|
|
1239
|
+
},
|
|
1343
1240
|
}));
|
|
1344
1241
|
registerGuarded(defineTool({
|
|
1345
1242
|
name: 'gotry_session_search',
|
|
1346
|
-
description: routed('gotry_session_search', 'Search on the USER\'S OWN browser session (Ctrip kind="flight"(default)/"hotel"; 12306 kind="train" — public query face, no login needed). '
|
|
1243
|
+
description: routed('gotry_session_search', 'Search on the USER\'S OWN browser session (Ctrip kind="flight"(default)/"hotel"; 12306 kind="train" — public query face, no login needed). '
|
|
1244
|
+
+ 'Consent gate: the FIRST call in a session asks the user via the runtime approval card; once granted it holds for the session, a refusal revokes it for the session (no repeat prompting). '
|
|
1245
|
+
+ '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). '
|
|
1246
|
+
+ 'kind="flight": from/to 中文城市名 + date YYYY-MM-DD — sniffs the site search API for structured options. Evidence [会话:ctrip-flight@ts]. '
|
|
1247
|
+
+ '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]. '
|
|
1248
|
+
+ '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]. '
|
|
1249
|
+
+ '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]. '
|
|
1250
|
+
+ '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); '
|
|
1251
|
+
+ `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. `
|
|
1252
|
+
+ 'Rate-limited (≥30s between same-site calls; a challenged/timeout verdict means STOP — never retry, fall back to other tools).'),
|
|
1253
|
+
// D-30 第二刀(issue #112):query blob → 平铺 typed 契约(逐字段 schema 模型可见)。
|
|
1254
|
+
// 与 flyai 刀的差异:本工具三意图无公共 required 字段(kind 缺省=flight),根 schema 是
|
|
1255
|
+
// 隐式开放对象,宿主权无法用 required 拒 legacy blob——故保留 interpretArgs 容忍层
|
|
1256
|
+
// (docs/design/tool-orchestration-design.md §4③「interpretArgs 留作旧形态容忍层」),blob 调用在
|
|
1257
|
+
// execute 内归一后走原条件闸,结构化报错不崩。flyai 因 kind required 仍在宿主权即拒。
|
|
1347
1258
|
parameters: {
|
|
1348
|
-
kind: {
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
},
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
type: 'string',
|
|
1391
|
-
description: '到达站三位电报码,仅 kind=train,同上'
|
|
1259
|
+
kind: { type: 'string', enum: ['flight', 'hotel', 'train', 'dida'], description: '默认 flight 机票;hotel 携程酒店(用户登录态真实价);train 12306 余票(公开面);dida 供应商门户实时价(员工登录态)' },
|
|
1260
|
+
from: { type: 'string', description: '出发城市中文(词表内),如 上海——kind=flight|train 必填' },
|
|
1261
|
+
to: { type: 'string', description: '到达城市中文——kind=flight|train 必填;kind=hotel 时为目的地(必填)' },
|
|
1262
|
+
date: { type: 'string', description: '出发日期 YYYY-MM-DD——kind=flight|train 必填,须为今天或未来' },
|
|
1263
|
+
cityId: { type: 'integer', description: '携程酒店 city= 数字,仅 kind=hotel;目的地码表外时 web 搜 hotels.ctrip.com list URL 取 city=' },
|
|
1264
|
+
checkIn: { type: 'string', description: '入住日 YYYY-MM-DD,仅 kind=hotel 可选' },
|
|
1265
|
+
checkOut: { type: 'string', description: '退房日 YYYY-MM-DD,仅 kind=hotel 可选' },
|
|
1266
|
+
adults: { type: 'integer', description: '成人数,仅 kind=hotel 可选' },
|
|
1267
|
+
fromStationTelecode: { type: 'string', description: '出发站三位电报码,仅 kind=train;城市码表外时填(kyfw 查询页 URL 里)' },
|
|
1268
|
+
toStationTelecode: { type: 'string', description: '到达站三位电报码,仅 kind=train,同上' },
|
|
1269
|
+
},
|
|
1270
|
+
output: { schema: { type: 'json' }, render: (_a, v) => [{ type: 'text', text: String(v.summary ?? JSON.stringify(v).slice(0, 600)) }] },
|
|
1271
|
+
async execute(args, _exec) {
|
|
1272
|
+
const selectedKind = resolveSessionSearchKind(args);
|
|
1273
|
+
if (!selectedKind.ok)
|
|
1274
|
+
return { ok: false, summary: selectedKind.reason };
|
|
1275
|
+
const q = { ...unwrapQuery(args), kind: selectedKind.kind };
|
|
1276
|
+
const runSessionEffect = seams.effect ?? interpretEffect;
|
|
1277
|
+
// ---- Dida 供应商门户(2026-09-09 实装;hotel-be portal integration 迁移线)----
|
|
1278
|
+
if (q.kind === 'dida') {
|
|
1279
|
+
const itpD = await runSessionEffect({
|
|
1280
|
+
effect: 'SESSION_DIDA_SEARCH',
|
|
1281
|
+
params: {
|
|
1282
|
+
auditPath: join(config.stateRoot ?? '.', 'gotry-state', 'session-incidents.jsonl'),
|
|
1283
|
+
},
|
|
1284
|
+
});
|
|
1285
|
+
if (!itpD.result)
|
|
1286
|
+
return declinedObservation('SESSION_DIDA_SEARCH', itpD.trace);
|
|
1287
|
+
const rD = itpD.result;
|
|
1288
|
+
await noteChannel('session:dida-portal', rD.verdict);
|
|
1289
|
+
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)}…` : ''}`);
|
|
1290
|
+
const summaryD = rD.verdict === 'hit'
|
|
1291
|
+
? `Dida 门户实时价(员工本人登录态,被动嗅探)前 ${topD.length} 条(预订由服务端适配器凭 referenceNo 续链,gotry 不碰下单):\n${topD.join('\n')}\n${rD.evidence}`
|
|
1292
|
+
: rD.verdict === 'cooldown'
|
|
1293
|
+
? `会话检索节律闸冷却中(两次会话检索需 ≥30s 间隔)——稍候重试或先用其他工具推进。${rD.evidence}`
|
|
1294
|
+
: rD.verdict === 'needs-login'
|
|
1295
|
+
? `${rD.error ?? ''} ${rD.evidence}`
|
|
1296
|
+
: `Dida 门户会话检索未取回(${rD.verdict}):${rD.error ?? ''} ${rD.evidence}`;
|
|
1297
|
+
return JSON.parse(JSON.stringify({
|
|
1298
|
+
...rD, summary: summaryD,
|
|
1299
|
+
...(rD.verdict !== 'hit' ? routingField('search-hotel', 'session:dida-portal') : {}),
|
|
1300
|
+
}));
|
|
1392
1301
|
}
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
schema: {
|
|
1396
|
-
type: 'json'
|
|
1397
|
-
},
|
|
1398
|
-
render: (_a, v)=>[
|
|
1399
|
-
{
|
|
1400
|
-
type: 'text',
|
|
1401
|
-
text: String(v.summary ?? JSON.stringify(v).slice(0, 600))
|
|
1402
|
-
}
|
|
1403
|
-
]
|
|
1404
|
-
},
|
|
1405
|
-
async execute (args, _exec) {
|
|
1406
|
-
const q = unwrapQuery(args);
|
|
1302
|
+
// ---- 会话酒店(2026-09-03 实装;2026-09-02 迪拜 session:用户要携程找酒店,会话面却只有机票)----
|
|
1303
|
+
// ---- 会话火车(2026-09-03 实装;12306 公开查询面,无登录闸)----
|
|
1407
1304
|
if (q.kind === 'train') {
|
|
1408
1305
|
if (!q.from || !q.to || !q.date) {
|
|
1409
|
-
return {
|
|
1410
|
-
ok: false,
|
|
1411
|
-
summary: 'kind=train 需要 from/to(中文城市/车站名)与 date(YYYY-MM-DD);城市电报码表外带 fromStationTelecode/toStationTelecode(kyfw 查询页 URL 里的三位码)'
|
|
1412
|
-
};
|
|
1306
|
+
return { ok: false, summary: 'kind=train 需要 from/to(中文城市/车站名)与 date(YYYY-MM-DD);城市电报码表外带 fromStationTelecode/toStationTelecode(kyfw 查询页 URL 里的三位码)' };
|
|
1413
1307
|
}
|
|
1414
1308
|
const nowT = new Date();
|
|
1415
1309
|
const todayT = `${nowT.getFullYear()}-${String(nowT.getMonth() + 1).padStart(2, '0')}-${String(nowT.getDate()).padStart(2, '0')}`;
|
|
1416
1310
|
if (q.date < todayT) {
|
|
1417
1311
|
return JSON.parse(JSON.stringify({
|
|
1418
|
-
ok: false,
|
|
1419
|
-
|
|
1420
|
-
summary: `未发起查询:日期 ${q.date} 已是过去(今天 ${todayT}),过去不存在在售火车票。向用户确认日期后再查。`
|
|
1312
|
+
ok: false, verdict: 'error',
|
|
1313
|
+
summary: `未发起查询:日期 ${q.date} 已是过去(今天 ${todayT}),过去不存在在售火车票。向用户确认日期后再查。`,
|
|
1421
1314
|
}));
|
|
1422
1315
|
}
|
|
1423
|
-
const itpT = await
|
|
1316
|
+
const itpT = await runSessionEffect({
|
|
1424
1317
|
effect: 'SESSION_TRAIN_SEARCH',
|
|
1425
1318
|
params: {
|
|
1426
|
-
from: q.from,
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
toStationTelecode: q.toStationTelecode,
|
|
1431
|
-
auditPath: join(config.stateRoot ?? '.', 'gotry-state', 'session-incidents.jsonl')
|
|
1432
|
-
}
|
|
1319
|
+
from: q.from, to: q.to, date: q.date,
|
|
1320
|
+
fromStationTelecode: q.fromStationTelecode, toStationTelecode: q.toStationTelecode,
|
|
1321
|
+
auditPath: join(config.stateRoot ?? '.', 'gotry-state', 'session-incidents.jsonl'),
|
|
1322
|
+
},
|
|
1433
1323
|
});
|
|
1434
|
-
if (!itpT.result)
|
|
1324
|
+
if (!itpT.result)
|
|
1325
|
+
return declinedObservation('SESSION_TRAIN_SEARCH', itpT.trace);
|
|
1435
1326
|
const rT = itpT.result;
|
|
1436
1327
|
await noteChannel('session:12306-train', rT.verdict);
|
|
1437
|
-
|
|
1438
|
-
|
|
1328
|
+
// #355:only the invocation-produced binding may enter the fact log;
|
|
1329
|
+
// malformed/transport/challenge states remain no-fact, and Y alone is
|
|
1330
|
+
// not rendered as bookable without a recognized available seat token.
|
|
1331
|
+
await appendFacts(config.stateRoot ?? '.', factsFromSessionTrain({ from: q.from, to: q.to, date: q.date }, rT, clock()));
|
|
1332
|
+
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(' ')}`);
|
|
1333
|
+
const summaryT = rT.verdict === 'hit'
|
|
1334
|
+
? `${q.from}→${q.to} ${q.date} 余票(12306 公开查询面,${(rT.trains ?? []).length} 趟)前 ${topT.length} 条(列表接口不含票价,票价以 12306 落地页为准):\n${topT.join('\n')}\n${rT.evidence}`
|
|
1335
|
+
: rT.verdict === 'cooldown'
|
|
1336
|
+
? `会话火车检索节律闸冷却中(两次会话检索需 ≥30s 间隔)——稍候重试或先用其他工具推进。${rT.evidence}`
|
|
1337
|
+
: `会话火车检索未取回(${rT.verdict}):${rT.error ?? ''} ${rT.evidence}`;
|
|
1439
1338
|
return JSON.parse(JSON.stringify({
|
|
1440
|
-
...rT,
|
|
1441
|
-
|
|
1442
|
-
...rT.verdict !== 'hit' ? routingField('search-train', 'session:12306-train') : {}
|
|
1339
|
+
...rT, summary: summaryT,
|
|
1340
|
+
...(rT.verdict !== 'hit' ? routingField('search-train', 'session:12306-train') : {}),
|
|
1443
1341
|
}));
|
|
1444
1342
|
}
|
|
1445
1343
|
if (q.kind === 'hotel') {
|
|
1446
1344
|
if (!q.to) {
|
|
1447
|
-
return {
|
|
1448
|
-
ok: false,
|
|
1449
|
-
summary: 'kind=hotel 需要 to(目的地中文;城市码表外带 cityId=携程酒店 list 页 URL 里的 city= 数字)'
|
|
1450
|
-
};
|
|
1345
|
+
return { ok: false, summary: 'kind=hotel 需要 to(目的地中文;城市码表外带 cityId=携程酒店 list 页 URL 里的 city= 数字)' };
|
|
1451
1346
|
}
|
|
1452
|
-
const itp = await
|
|
1347
|
+
const itp = await runSessionEffect({
|
|
1453
1348
|
effect: 'SESSION_HOTEL_SEARCH',
|
|
1454
1349
|
params: {
|
|
1455
|
-
to: q.to,
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
checkOut: q.checkOut,
|
|
1459
|
-
adults: q.adults,
|
|
1460
|
-
auditPath: join(config.stateRoot ?? '.', 'gotry-state', 'session-incidents.jsonl')
|
|
1461
|
-
}
|
|
1350
|
+
to: q.to, cityId: q.cityId, checkIn: q.checkIn, checkOut: q.checkOut, adults: q.adults,
|
|
1351
|
+
auditPath: join(config.stateRoot ?? '.', 'gotry-state', 'session-incidents.jsonl'),
|
|
1352
|
+
},
|
|
1462
1353
|
});
|
|
1463
|
-
if (!itp.result)
|
|
1354
|
+
if (!itp.result)
|
|
1355
|
+
return declinedObservation('SESSION_HOTEL_SEARCH', itp.trace);
|
|
1464
1356
|
const r = itp.result;
|
|
1465
1357
|
await noteChannel('session:ctrip-hotel', r.verdict);
|
|
1358
|
+
// D-26(issue #118):exact-date 会话酒店 hit/miss 落账(摸底无档期不落;cooldown 是节律闸非结论,不落)
|
|
1466
1359
|
if (r.verdict === 'hit' || r.verdict === 'miss') {
|
|
1467
1360
|
await appendFacts(config.stateRoot ?? '.', factsFromHotel({
|
|
1468
|
-
source: 'session:ctrip-hotel',
|
|
1469
|
-
|
|
1470
|
-
checkIn: q.checkIn,
|
|
1471
|
-
checkOut: q.checkOut,
|
|
1472
|
-
verdict: r.verdict,
|
|
1473
|
-
options: r.hotels?.length ?? 0,
|
|
1474
|
-
evidence: r.evidence,
|
|
1475
|
-
fetchedAt: new Date().toISOString()
|
|
1361
|
+
source: 'session:ctrip-hotel', destination: q.to ?? '', checkIn: q.checkIn, checkOut: q.checkOut,
|
|
1362
|
+
verdict: r.verdict, options: r.hotels?.length ?? 0, evidence: r.evidence, fetchedAt: new Date().toISOString(),
|
|
1476
1363
|
}));
|
|
1477
1364
|
}
|
|
1478
|
-
const top = (r.hotels ?? []).slice(0, 8).map(
|
|
1479
|
-
const summary = r.verdict === 'hit'
|
|
1365
|
+
const top = (r.hotels ?? []).slice(0, 8).map(h => `${h.name}${h.star ? `(${h.star}星)` : ''} ${h.priceRaw ?? (h.price > 0 ? `¥${h.price}` : '价待询')}${h.score ? ` 评分${h.score}` : ''}`);
|
|
1366
|
+
const summary = r.verdict === 'hit'
|
|
1367
|
+
? `${q.to} 酒店(携程,用户本人登录态,真实价)前 ${top.length} 家(预订以 jumpUrl 落地页为准):\n${top.join('\n')}\n${r.evidence}`
|
|
1368
|
+
: r.verdict === 'cooldown'
|
|
1369
|
+
? `会话酒店检索节律闸冷却中(两次会话检索需 ≥30s 间隔)——稍候重试或先用其他工具推进。${r.evidence}`
|
|
1370
|
+
: `会话酒店检索未取回(${r.verdict}):${r.error ?? ''} ${r.evidence}`;
|
|
1480
1371
|
return JSON.parse(JSON.stringify({
|
|
1481
|
-
...r,
|
|
1482
|
-
|
|
1483
|
-
...r.verdict !== 'hit' ? routingField('search-hotel', 'session:ctrip-hotel') : {}
|
|
1372
|
+
...r, summary,
|
|
1373
|
+
...(r.verdict !== 'hit' ? routingField('search-hotel', 'session:ctrip-hotel') : {}),
|
|
1484
1374
|
}));
|
|
1485
1375
|
}
|
|
1486
1376
|
if (!q.from || !q.to || !q.date) {
|
|
1487
|
-
return {
|
|
1488
|
-
ok: false,
|
|
1489
|
-
summary: '需要 from/to(中文城市名,词表内)与 date(YYYY-MM-DD);酒店/火车检索分别用 kind=hotel / kind=train'
|
|
1490
|
-
};
|
|
1377
|
+
return { ok: false, summary: '需要 from/to(中文城市名,词表内)与 date(YYYY-MM-DD);酒店/火车检索分别用 kind=hotel / kind=train' };
|
|
1491
1378
|
}
|
|
1492
|
-
|
|
1379
|
+
// 效应解译层(ADR-18):SESSION 通道策略=永不重试/不熔断,节律闸在渠道内;
|
|
1380
|
+
// 解译器只做分发与证据拼装,verdict 语义(risk 型 needs-login/challenged)原样透传
|
|
1381
|
+
const itp = await runSessionEffect({
|
|
1493
1382
|
effect: 'SESSION_FLIGHT_SEARCH',
|
|
1494
1383
|
params: {
|
|
1495
|
-
from: q.from,
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
}
|
|
1384
|
+
from: q.from, to: q.to, date: q.date,
|
|
1385
|
+
// ADR-15 收尾:ReadGuard 审计在生产工具路径同样落盘(此前仅测试传隔离 stateRoot 才有 JSONL)
|
|
1386
|
+
auditPath: join(config.stateRoot ?? '.', 'gotry-state', 'session-incidents.jsonl'),
|
|
1387
|
+
},
|
|
1500
1388
|
});
|
|
1501
|
-
if (!itp.result)
|
|
1389
|
+
if (!itp.result)
|
|
1390
|
+
return declinedObservation('SESSION_FLIGHT_SEARCH', itp.trace);
|
|
1502
1391
|
const r = itp.result;
|
|
1503
1392
|
await noteChannel('session:ctrip-flight', r.verdict);
|
|
1393
|
+
// issue #46 事实落账(ADR-19):会话面 exact-date 结论同样进事实注册表(独立 source 并列标注)
|
|
1504
1394
|
const avMapS = await loadAirlineAirportMap();
|
|
1505
|
-
await appendFacts(config.stateRoot ?? '.', factsFromSession({
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
const top = (r.options ?? []).slice(0, 8).map((o)=>`${o.flightNo} ${o.airline} ${o.depDateTime.slice(11, 16)}→${o.arrDateTime.slice(11, 16)} ¥${o.price}`);
|
|
1511
|
-
const summary = r.verdict === 'hit' ? `${q.from}→${q.to} ${q.date} 会话检索(携程,用户本人登录态)前 ${top.length} 条:\n${top.join('\n')}\n${r.evidence}` : `会话检索未取回(${r.verdict}):${r.error ?? ''} ${r.evidence}`;
|
|
1395
|
+
await appendFacts(config.stateRoot ?? '.', factsFromSession({ origin: q.from, destination: q.to, date: q.date }, r, new Date().toISOString(), avMapS?.city_alias));
|
|
1396
|
+
const top = (r.options ?? []).slice(0, 8).map(o => `${o.flightNo} ${o.airline} ${o.depDateTime.slice(11, 16)}→${o.arrDateTime.slice(11, 16)} ¥${o.price}`);
|
|
1397
|
+
const summary = r.verdict === 'hit'
|
|
1398
|
+
? `${q.from}→${q.to} ${q.date} 会话检索(携程,用户本人登录态)前 ${top.length} 条:\n${top.join('\n')}\n${r.evidence}`
|
|
1399
|
+
: `会话检索未取回(${r.verdict}):${r.error ?? ''} ${r.evidence}`;
|
|
1512
1400
|
return JSON.parse(JSON.stringify({
|
|
1513
|
-
...r,
|
|
1514
|
-
|
|
1515
|
-
...r.verdict !== 'hit' ? routingField('search-flight', 'session:ctrip-flight') : {}
|
|
1401
|
+
...r, summary,
|
|
1402
|
+
...(r.verdict !== 'hit' ? routingField('search-flight', 'session:ctrip-flight') : {}),
|
|
1516
1403
|
}));
|
|
1517
1404
|
},
|
|
1518
|
-
presentCall:
|
|
1519
|
-
const
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
rawInput: args
|
|
1525
|
-
};
|
|
1405
|
+
presentCall: args => {
|
|
1406
|
+
const selected = resolveSessionSearchKind(args);
|
|
1407
|
+
const kind = selected.ok ? selected.kind : undefined;
|
|
1408
|
+
const raw = args;
|
|
1409
|
+
const callTitle = kind === 'hotel' ? `会话酒店:${raw.to ?? ''}` : kind === 'train' ? `会话火车:${raw.from ?? ''}` : kind === 'dida' ? '会话酒店:Dida 门户实时价' : `会话检索:${raw.from ?? ''}`;
|
|
1410
|
+
return { card: 'generic', title: callTitle, kind: 'fetch', rawInput: args };
|
|
1526
1411
|
},
|
|
1527
|
-
presentResult: (args, value)=>{
|
|
1412
|
+
presentResult: (args, value) => {
|
|
1413
|
+
const selected = resolveSessionSearchKind(args);
|
|
1414
|
+
const kind = selected.ok ? selected.kind : undefined;
|
|
1528
1415
|
const r = value;
|
|
1529
|
-
const isHotel =
|
|
1416
|
+
const isHotel = kind === 'hotel';
|
|
1530
1417
|
const n = Math.max((r.options ?? []).length, (r.hotels ?? []).length);
|
|
1531
1418
|
const needsExt = r.verdict === 'needs-extension';
|
|
1532
|
-
const label = r.verdict === 'hit'
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
text: `安装链接:${r.installUrl}`
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
card: 'generic',
|
|
1546
|
-
title: `${face}:${label}`,
|
|
1547
|
-
content
|
|
1548
|
-
};
|
|
1549
|
-
}
|
|
1419
|
+
const label = r.verdict === 'hit'
|
|
1420
|
+
? `会话 ${n} 条`
|
|
1421
|
+
: r.verdict === 'needs-login'
|
|
1422
|
+
? '需登录'
|
|
1423
|
+
: needsExt
|
|
1424
|
+
? '🧩 需装 GoTry Session Bridge 扩展(浏览器商店一键装,自动更新)'
|
|
1425
|
+
: r.verdict ?? '降级';
|
|
1426
|
+
const content = [{ type: 'text', text: String(value.summary ?? '') }];
|
|
1427
|
+
if (needsExt && r.installUrl)
|
|
1428
|
+
content.push({ type: 'text', text: `安装链接:${r.installUrl}` });
|
|
1429
|
+
const face = kind === 'hotel' ? '会话酒店' : kind === 'train' ? '会话火车' : kind === 'dida' ? '会话酒店:Dida' : '会话检索';
|
|
1430
|
+
return { card: 'generic', title: `${face}:${label}`, content };
|
|
1431
|
+
},
|
|
1550
1432
|
}));
|
|
1551
1433
|
registerGuarded(defineTool({
|
|
1552
1434
|
name: 'gotry_anything_search',
|
|
1553
|
-
description: routed('gotry_anything_search', 'Travel-domain search via hotel-byte CLI → hotel-be Anything (cities/hotels/destinations) — NOT general web search; for general internet facts use gotry_agent_reach. ' +
|
|
1435
|
+
description: routed('gotry_anything_search', 'Travel-domain search via hotel-byte CLI → hotel-be Anything (cities/hotels/destinations) — NOT general web search; for general internet facts use gotry_agent_reach. ' +
|
|
1436
|
+
'Mixed destinations (cities / metropolitan areas / high-level regions) + hotels in one call. ' +
|
|
1437
|
+
'Returns candidates with type, name, optional coordinates and hotel-id. ' +
|
|
1438
|
+
'Three-valued semantics: hit = ≥1 candidate; miss = 0 candidates (try synonyms or contentType=city/hotel); ' +
|
|
1439
|
+
'unavailable = hbcli failed (degraded, never blocks). ' +
|
|
1440
|
+
'Use as the first stop when the user mentions a place/city/hotel name and you need to ground it in real catalog data ' +
|
|
1441
|
+
'(OpenFlights skeleton tells you connectivity; Anything tells you what EXISTS at a city/region).'),
|
|
1442
|
+
// D-30 第四刀(issue #112):平铺 typed;keyword required → 宿主权即拒
|
|
1554
1443
|
parameters: {
|
|
1555
|
-
keyword: {
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
},
|
|
1560
|
-
contentType: {
|
|
1561
|
-
type: 'string',
|
|
1562
|
-
enum: [
|
|
1563
|
-
'city',
|
|
1564
|
-
'hotel'
|
|
1565
|
-
],
|
|
1566
|
-
description: '内容类型(缺省由上游推断)'
|
|
1567
|
-
},
|
|
1568
|
-
parentDestinationId: {
|
|
1569
|
-
type: 'string',
|
|
1570
|
-
description: '父目的地 id(上游形态)'
|
|
1571
|
-
},
|
|
1572
|
-
timeoutMs: {
|
|
1573
|
-
type: 'integer',
|
|
1574
|
-
description: '超时毫秒,默认 12000'
|
|
1575
|
-
}
|
|
1444
|
+
keyword: { type: 'string', required: true, description: '搜索关键词' },
|
|
1445
|
+
contentType: { type: 'string', enum: ['city', 'hotel'], description: '内容类型(缺省由上游推断)' },
|
|
1446
|
+
parentDestinationId: { type: 'string', description: '父目的地 id(上游形态)' },
|
|
1447
|
+
timeoutMs: { type: 'integer', description: '超时毫秒,默认 12000' },
|
|
1576
1448
|
},
|
|
1577
1449
|
output: {
|
|
1578
|
-
schema: {
|
|
1579
|
-
|
|
1580
|
-
},
|
|
1581
|
-
render: (_args, value)=>[
|
|
1582
|
-
{
|
|
1583
|
-
type: 'text',
|
|
1584
|
-
text: String(value.summary ?? JSON.stringify(value).slice(0, 800))
|
|
1585
|
-
}
|
|
1586
|
-
]
|
|
1450
|
+
schema: { type: 'json' },
|
|
1451
|
+
render: (_args, value) => [{ type: 'text', text: String(value.summary ?? JSON.stringify(value).slice(0, 800)) }],
|
|
1587
1452
|
},
|
|
1588
|
-
async execute
|
|
1453
|
+
async execute(args, _exec) {
|
|
1589
1454
|
const q = args;
|
|
1590
1455
|
const rawQuery = String(q.keyword ?? '');
|
|
1591
1456
|
const started = Date.now();
|
|
1592
1457
|
if (!q.keyword) {
|
|
1593
|
-
return JSON.parse(JSON.stringify({
|
|
1594
|
-
ok: false,
|
|
1595
|
-
verdict: 'error',
|
|
1596
|
-
summary: 'keyword 必填',
|
|
1597
|
-
evidence: '[hbcli-anything@error] empty'
|
|
1598
|
-
}));
|
|
1458
|
+
return JSON.parse(JSON.stringify({ ok: false, verdict: 'error', summary: 'keyword 必填', evidence: '[hbcli-anything@error] empty' }));
|
|
1599
1459
|
}
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
}
|
|
1606
|
-
});
|
|
1607
|
-
if (!itpA.result) return declinedObservation('ANYTHING_SEARCH', itpA.trace);
|
|
1460
|
+
// hbcliBin 直通 config(缺省 'hbcli',能力层按已知安装位回退)——issue #195 教训:
|
|
1461
|
+
// 此前 params 只透 q,config.hbcliBin 永远到不了能力层
|
|
1462
|
+
const itpA = await interpretEffect({ effect: 'ANYTHING_SEARCH', params: { ...q, hbcliBin: config.hbcliBin } });
|
|
1463
|
+
if (!itpA.result)
|
|
1464
|
+
return declinedObservation('ANYTHING_SEARCH', itpA.trace);
|
|
1608
1465
|
const r = itpA.result;
|
|
1609
1466
|
const dir = await ensureStateDir(config.stateRoot);
|
|
1610
|
-
await recordLatency(join(dir, 'bridge-latency.jsonl'), Date.now() - started, `anything:${r.via}`).catch(()=>{});
|
|
1467
|
+
await recordLatency(join(dir, 'bridge-latency.jsonl'), Date.now() - started, `anything:${r.via}`).catch(() => { });
|
|
1611
1468
|
const top5 = (r.hits ?? []).slice(0, 5);
|
|
1612
|
-
const summary = r.verdict === 'hit'
|
|
1469
|
+
const summary = r.verdict === 'hit'
|
|
1470
|
+
? `${q.keyword} → hit (${r.hits?.length ?? 0} 候选项)\n${top5.map((h, i) => ` ${i + 1}. [${h.type}] ${h.name}${h.star ? ` ${h.star}★` : ''}${h.latitude !== undefined && h.longitude !== undefined ? ` @ (${h.latitude.toFixed(3)},${h.longitude.toFixed(3)})` : ''}${h.hotelId ? ` hotelId=${h.hotelId}` : h.destinationId ? ` destinationId=${h.destinationId}` : ''}`).join('\n')}\n${r.evidence}`
|
|
1471
|
+
: r.verdict === 'miss'
|
|
1472
|
+
? `${q.keyword} → miss (酒店-be 一切正常但无候选)\n${r.evidence}`
|
|
1473
|
+
: `${q.keyword} → unavailable (${r.error})\n${r.evidence}`;
|
|
1613
1474
|
return JSON.parse(JSON.stringify({
|
|
1614
|
-
ok: r.ok,
|
|
1615
|
-
verdict: r.verdict,
|
|
1616
|
-
keyword: q.keyword,
|
|
1475
|
+
ok: r.ok, verdict: r.verdict, keyword: q.keyword,
|
|
1617
1476
|
content_type: q.contentType ?? null,
|
|
1618
|
-
total_candidates: r.totalCandidates,
|
|
1619
|
-
|
|
1620
|
-
evidence: r.evidence,
|
|
1621
|
-
summary,
|
|
1622
|
-
latency_ms: Date.now() - started
|
|
1477
|
+
total_candidates: r.totalCandidates, hits: r.hits, evidence: r.evidence, summary,
|
|
1478
|
+
latency_ms: Date.now() - started,
|
|
1623
1479
|
}));
|
|
1624
1480
|
},
|
|
1625
|
-
presentCall:
|
|
1626
|
-
|
|
1627
|
-
title: `Anything search:${args.keyword ?? ''}`,
|
|
1628
|
-
kind: 'search',
|
|
1629
|
-
rawInput: args
|
|
1630
|
-
}),
|
|
1631
|
-
presentResult: (_args, value)=>{
|
|
1481
|
+
presentCall: args => ({ card: 'generic', title: `Anything search:${args.keyword ?? ''}`, kind: 'search', rawInput: args }),
|
|
1482
|
+
presentResult: (_args, value) => {
|
|
1632
1483
|
const r = value;
|
|
1633
|
-
const n = Array.isArray(r.hits) ? r.hits.length : r.total_candidates ?? 0;
|
|
1484
|
+
const n = Array.isArray(r.hits) ? r.hits.length : (r.total_candidates ?? 0);
|
|
1634
1485
|
return {
|
|
1635
1486
|
card: 'generic',
|
|
1636
|
-
title: `Anything:${r.keyword ?? ''} ${r.verdict === 'hit' ? `${n} hits` : r.verdict ?? 'no-result'}`,
|
|
1637
|
-
content: [
|
|
1638
|
-
{
|
|
1639
|
-
type: 'text',
|
|
1640
|
-
text: String(r.summary ?? '')
|
|
1641
|
-
}
|
|
1642
|
-
]
|
|
1487
|
+
title: `Anything:${r.keyword ?? ''} ${r.verdict === 'hit' ? `${n} hits` : (r.verdict ?? 'no-result')}`,
|
|
1488
|
+
content: [{ type: 'text', text: String(r.summary ?? '') }],
|
|
1643
1489
|
};
|
|
1644
|
-
}
|
|
1490
|
+
},
|
|
1645
1491
|
}));
|
|
1646
1492
|
registerGuarded(defineTool({
|
|
1647
1493
|
name: 'gotry_web_search',
|
|
1648
|
-
description: routed('gotry_web_search', 'Read any public URL as markdown (Jina Reader, free, no key). ' +
|
|
1494
|
+
description: routed('gotry_web_search', 'Read any public URL as markdown (Jina Reader, free, no key). ' +
|
|
1495
|
+
'Use as the "last mile" web reader when hotel-be Anything or gotry tools lack the answer. ' +
|
|
1496
|
+
'NOT a general-purpose search engine — only fetches a URL you already know. ' +
|
|
1497
|
+
'Three-valued: ok / error(非法 URL/超时)/not-reachable(r.jina.ai 不可用).' +
|
|
1498
|
+
'Contract with gotry capabilities/anything.ts: 同构(L4 证据链 + 降级不阻塞 + 三值)。'),
|
|
1499
|
+
// D-30 第四刀(issue #112):平铺 typed;url required → 宿主权即拒(字符串 blob 形态退役)
|
|
1649
1500
|
parameters: {
|
|
1650
|
-
url: {
|
|
1651
|
-
|
|
1652
|
-
required: true,
|
|
1653
|
-
description: '完整 URL,https://…'
|
|
1654
|
-
},
|
|
1655
|
-
timeoutMs: {
|
|
1656
|
-
type: 'integer',
|
|
1657
|
-
description: '超时毫秒,默认 20000'
|
|
1658
|
-
}
|
|
1501
|
+
url: { type: 'string', required: true, description: '完整 URL,https://…' },
|
|
1502
|
+
timeoutMs: { type: 'integer', description: '超时毫秒,默认 20000' },
|
|
1659
1503
|
},
|
|
1660
1504
|
output: {
|
|
1661
|
-
schema: {
|
|
1662
|
-
|
|
1663
|
-
},
|
|
1664
|
-
render: (_args, value)=>[
|
|
1665
|
-
{
|
|
1666
|
-
type: 'text',
|
|
1667
|
-
text: String(value.content?.slice(0, 800) ?? JSON.stringify(value).slice(0, 800))
|
|
1668
|
-
}
|
|
1669
|
-
]
|
|
1505
|
+
schema: { type: 'json' },
|
|
1506
|
+
render: (_args, value) => [{ type: 'text', text: String(value.content?.slice(0, 800) ?? JSON.stringify(value).slice(0, 800)) }],
|
|
1670
1507
|
},
|
|
1671
|
-
async execute
|
|
1508
|
+
async execute(args, _exec) {
|
|
1672
1509
|
const q = args;
|
|
1673
1510
|
const started = Date.now();
|
|
1674
1511
|
if (!q.url) {
|
|
1675
|
-
return JSON.parse(JSON.stringify({
|
|
1676
|
-
ok: false,
|
|
1677
|
-
summary: 'url 必填',
|
|
1678
|
-
evidence: '[agent-reach:error] empty url'
|
|
1679
|
-
}));
|
|
1512
|
+
return JSON.parse(JSON.stringify({ ok: false, summary: 'url 必填', evidence: '[agent-reach:error] empty url' }));
|
|
1680
1513
|
}
|
|
1681
|
-
const itpW = await interpretEffect({
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
url: q.url,
|
|
1685
|
-
timeoutMs: q.timeoutMs
|
|
1686
|
-
}
|
|
1687
|
-
});
|
|
1688
|
-
if (!itpW.result) return declinedObservation('WEB_READ', itpW.trace);
|
|
1514
|
+
const itpW = await interpretEffect({ effect: 'WEB_READ', params: { url: q.url, timeoutMs: q.timeoutMs } });
|
|
1515
|
+
if (!itpW.result)
|
|
1516
|
+
return declinedObservation('WEB_READ', itpW.trace);
|
|
1689
1517
|
const r = itpW.result;
|
|
1690
1518
|
const dir = await ensureStateDir(config.stateRoot);
|
|
1691
|
-
await recordLatency(join(dir, 'bridge-latency.jsonl'), Date.now() - started, `agent-reach:${r.via}`).catch(()=>{});
|
|
1692
|
-
const summary = r.ok
|
|
1519
|
+
await recordLatency(join(dir, 'bridge-latency.jsonl'), Date.now() - started, `agent-reach:${r.via}`).catch(() => { });
|
|
1520
|
+
const summary = r.ok
|
|
1521
|
+
? `${q.url} → ${r.title ?? '(no title)'} (${r.latencyMs}ms)\n${r.evidence}\n---\n${r.content?.slice(0, 600) ?? ''}`
|
|
1522
|
+
: `${q.url} → unavailable (${r.error})\n${r.evidence}`;
|
|
1693
1523
|
return JSON.parse(JSON.stringify({
|
|
1694
|
-
ok: r.ok,
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
title: r.title,
|
|
1698
|
-
content: r.content,
|
|
1699
|
-
evidence: r.evidence,
|
|
1700
|
-
summary,
|
|
1701
|
-
latency_ms: Date.now() - started
|
|
1524
|
+
ok: r.ok, url: q.url, via: r.via, title: r.title,
|
|
1525
|
+
content: r.content, evidence: r.evidence, summary,
|
|
1526
|
+
latency_ms: Date.now() - started,
|
|
1702
1527
|
}));
|
|
1703
1528
|
},
|
|
1704
|
-
presentCall:
|
|
1705
|
-
card: 'generic',
|
|
1706
|
-
title: `读网页:${args.url ?? ''}`,
|
|
1707
|
-
kind: 'fetch',
|
|
1708
|
-
rawInput: args
|
|
1709
|
-
})
|
|
1529
|
+
presentCall: args => ({ card: 'generic', title: `读网页:${args.url ?? ''}`, kind: 'fetch', rawInput: args }),
|
|
1710
1530
|
}));
|
|
1711
1531
|
registerGuarded(defineTool({
|
|
1712
1532
|
name: 'gotry_video_subtitle',
|
|
1713
|
-
description: 'Extract subtitles from a YouTube/Bilibili video (yt-dlp, optional tool). ' +
|
|
1533
|
+
description: 'Extract subtitles from a YouTube/Bilibili video (yt-dlp, optional tool). ' +
|
|
1534
|
+
'If yt-dlp is installed on this machine, returns the subtitle text (vtt, zh-Hans/zh/en preference). ' +
|
|
1535
|
+
'If NOT installed, degrades gracefully with install instructions — never blocks. ' +
|
|
1536
|
+
'Evidence chain: [agent-reach:yt-dlp@ts] / [@not-installed@ts].',
|
|
1537
|
+
// D-30 第四刀(issue #112):平铺 typed;url required → 宿主权即拒
|
|
1714
1538
|
parameters: {
|
|
1715
|
-
url: {
|
|
1716
|
-
|
|
1717
|
-
required: true,
|
|
1718
|
-
description: '视频页 URL(youtube 等)'
|
|
1719
|
-
},
|
|
1720
|
-
lang: {
|
|
1721
|
-
type: 'string',
|
|
1722
|
-
description: '字幕语言优先级,如 zh-Hans,zh,en'
|
|
1723
|
-
}
|
|
1539
|
+
url: { type: 'string', required: true, description: '视频页 URL(youtube 等)' },
|
|
1540
|
+
lang: { type: 'string', description: '字幕语言优先级,如 zh-Hans,zh,en' },
|
|
1724
1541
|
},
|
|
1725
1542
|
output: {
|
|
1726
|
-
schema: {
|
|
1727
|
-
|
|
1728
|
-
},
|
|
1729
|
-
render: (_args, value)=>[
|
|
1730
|
-
{
|
|
1731
|
-
type: 'text',
|
|
1732
|
-
text: String(value.summary ?? JSON.stringify(value).slice(0, 600))
|
|
1733
|
-
}
|
|
1734
|
-
]
|
|
1543
|
+
schema: { type: 'json' },
|
|
1544
|
+
render: (_args, value) => [{ type: 'text', text: String(value.summary ?? JSON.stringify(value).slice(0, 600)) }],
|
|
1735
1545
|
},
|
|
1736
|
-
async execute
|
|
1546
|
+
async execute(args, _exec) {
|
|
1737
1547
|
const q = args;
|
|
1738
1548
|
if (!q.url) {
|
|
1739
|
-
return JSON.parse(JSON.stringify({
|
|
1740
|
-
ok: false,
|
|
1741
|
-
summary: 'url 必填'
|
|
1742
|
-
}));
|
|
1549
|
+
return JSON.parse(JSON.stringify({ ok: false, summary: 'url 必填' }));
|
|
1743
1550
|
}
|
|
1744
|
-
const itpV = await interpretEffect({
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
url: q.url,
|
|
1748
|
-
lang: q.lang
|
|
1749
|
-
}
|
|
1750
|
-
});
|
|
1751
|
-
if (!itpV.result) return declinedObservation('VIDEO_SUBTITLE', itpV.trace);
|
|
1551
|
+
const itpV = await interpretEffect({ effect: 'VIDEO_SUBTITLE', params: { url: q.url, lang: q.lang } });
|
|
1552
|
+
if (!itpV.result)
|
|
1553
|
+
return declinedObservation('VIDEO_SUBTITLE', itpV.trace);
|
|
1752
1554
|
const r = itpV.result;
|
|
1753
|
-
const summary = r.verdict === 'found'
|
|
1555
|
+
const summary = r.verdict === 'found'
|
|
1556
|
+
? `${q.url} 字幕提取成功 (${r.latencyMs}ms)\n${r.evidence}\n---\n${(r.subtitles ?? '').slice(0, 800)}`
|
|
1557
|
+
: r.verdict === 'not-installed'
|
|
1558
|
+
? `yt-dlp 未安装。${r.stderr}\n${r.evidence}`
|
|
1559
|
+
: `${q.url} 字幕提取失败(${r.verdict})\n${r.evidence}`;
|
|
1754
1560
|
return JSON.parse(JSON.stringify({
|
|
1755
|
-
ok: r.ok,
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
subtitles: r.subtitles?.slice(0, 4000),
|
|
1759
|
-
evidence: r.evidence,
|
|
1760
|
-
summary,
|
|
1761
|
-
latency_ms: r.latencyMs
|
|
1561
|
+
ok: r.ok, verdict: r.verdict, url: q.url,
|
|
1562
|
+
subtitles: r.subtitles?.slice(0, 4000), evidence: r.evidence, summary,
|
|
1563
|
+
latency_ms: r.latencyMs,
|
|
1762
1564
|
}));
|
|
1763
1565
|
},
|
|
1764
|
-
presentCall:
|
|
1765
|
-
card: 'generic',
|
|
1766
|
-
title: `视频字幕:${args.url ?? ''}`,
|
|
1767
|
-
kind: 'fetch',
|
|
1768
|
-
rawInput: args
|
|
1769
|
-
})
|
|
1566
|
+
presentCall: args => ({ card: 'generic', title: `视频字幕:${args.url ?? ''}`, kind: 'fetch', rawInput: args }),
|
|
1770
1567
|
}));
|
|
1771
1568
|
registerGuarded(defineTool({
|
|
1772
1569
|
name: 'gotry_github_search',
|
|
1773
|
-
description: 'Search GitHub repositories (gh CLI, optional tool). ' +
|
|
1570
|
+
description: 'Search GitHub repositories (gh CLI, optional tool). ' +
|
|
1571
|
+
'If gh is installed and authenticated, returns repos with name/description/stars/url. ' +
|
|
1572
|
+
'If NOT installed, degrades with install instructions — never blocks. ' +
|
|
1573
|
+
'Evidence chain: [agent-reach:gh@ts] / [@not-installed@ts].',
|
|
1574
|
+
// D-30 第四刀(issue #112):平铺 typed;query required → 宿主权即拒(顶层字段与旧内层同名,形态从对象变字符串)
|
|
1774
1575
|
parameters: {
|
|
1775
|
-
query: {
|
|
1776
|
-
|
|
1777
|
-
required: true,
|
|
1778
|
-
description: 'GitHub 搜索关键词,如 agent-reach'
|
|
1779
|
-
},
|
|
1780
|
-
limit: {
|
|
1781
|
-
type: 'integer',
|
|
1782
|
-
description: '结果数上限,默认 5'
|
|
1783
|
-
}
|
|
1576
|
+
query: { type: 'string', required: true, description: 'GitHub 搜索关键词,如 agent-reach' },
|
|
1577
|
+
limit: { type: 'integer', description: '结果数上限,默认 5' },
|
|
1784
1578
|
},
|
|
1785
1579
|
output: {
|
|
1786
|
-
schema: {
|
|
1787
|
-
|
|
1788
|
-
},
|
|
1789
|
-
render: (_args, value)=>[
|
|
1790
|
-
{
|
|
1791
|
-
type: 'text',
|
|
1792
|
-
text: String(value.summary ?? JSON.stringify(value).slice(0, 600))
|
|
1793
|
-
}
|
|
1794
|
-
]
|
|
1580
|
+
schema: { type: 'json' },
|
|
1581
|
+
render: (_args, value) => [{ type: 'text', text: String(value.summary ?? JSON.stringify(value).slice(0, 600)) }],
|
|
1795
1582
|
},
|
|
1796
|
-
async execute
|
|
1583
|
+
async execute(args, _exec) {
|
|
1797
1584
|
const q = args;
|
|
1798
1585
|
if (!q.query) {
|
|
1799
|
-
return JSON.parse(JSON.stringify({
|
|
1800
|
-
ok: false,
|
|
1801
|
-
summary: 'query 必填'
|
|
1802
|
-
}));
|
|
1586
|
+
return JSON.parse(JSON.stringify({ ok: false, summary: 'query 必填' }));
|
|
1803
1587
|
}
|
|
1804
|
-
const itpG = await interpretEffect({
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
query: q.query,
|
|
1808
|
-
limit: q.limit
|
|
1809
|
-
}
|
|
1810
|
-
});
|
|
1811
|
-
if (!itpG.result) return declinedObservation('GITHUB_SEARCH', itpG.trace);
|
|
1588
|
+
const itpG = await interpretEffect({ effect: 'GITHUB_SEARCH', params: { query: q.query, limit: q.limit } });
|
|
1589
|
+
if (!itpG.result)
|
|
1590
|
+
return declinedObservation('GITHUB_SEARCH', itpG.trace);
|
|
1812
1591
|
const r = itpG.result;
|
|
1813
|
-
const summary = r.verdict === 'found'
|
|
1592
|
+
const summary = r.verdict === 'found'
|
|
1593
|
+
? `${q.query} → ${r.repos?.length ?? 0} repos\n${(r.repos ?? []).map((x, i) => ` ${i + 1}. ${x.name} ★${x.stars ?? '?'} — ${(x.description ?? '').slice(0, 60)}`).join('\n')}\n${r.evidence}`
|
|
1594
|
+
: r.verdict === 'not-installed'
|
|
1595
|
+
? `gh 未安装。${r.stderr}\n${r.evidence}`
|
|
1596
|
+
: `${q.query} 搜索失败(${r.verdict})\n${r.evidence}`;
|
|
1814
1597
|
return JSON.parse(JSON.stringify({
|
|
1815
|
-
ok: r.ok,
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
repos: r.repos,
|
|
1819
|
-
evidence: r.evidence,
|
|
1820
|
-
summary,
|
|
1821
|
-
latency_ms: r.latencyMs
|
|
1598
|
+
ok: r.ok, verdict: r.verdict, query: q.query,
|
|
1599
|
+
repos: r.repos, evidence: r.evidence, summary,
|
|
1600
|
+
latency_ms: r.latencyMs,
|
|
1822
1601
|
}));
|
|
1823
1602
|
},
|
|
1824
|
-
presentCall:
|
|
1825
|
-
card: 'generic',
|
|
1826
|
-
title: `GitHub 搜索:${args.query ?? ''}`,
|
|
1827
|
-
kind: 'search',
|
|
1828
|
-
rawInput: args
|
|
1829
|
-
})
|
|
1603
|
+
presentCall: args => ({ card: 'generic', title: `GitHub 搜索:${args.query ?? ''}`, kind: 'search', rawInput: args }),
|
|
1830
1604
|
}));
|
|
1831
1605
|
registerGuarded(defineTool({
|
|
1832
1606
|
name: 'gotry_agent_reach',
|
|
1833
|
-
description: 'Agent Reach — the PRIMARY external-data gateway (thin wrapper over Panniantong/Agent-Reach upstream registry, zero channel knowledge here). ' +
|
|
1607
|
+
description: 'Agent Reach — the PRIMARY external-data gateway (thin wrapper over Panniantong/Agent-Reach upstream registry, zero channel knowledge here). ' +
|
|
1608
|
+
'Prefer this for ANY external/internet fact beyond weather/flights/hotels. ' +
|
|
1609
|
+
'Call ANY upstream channel method by reflection: web.read(url) / v2ex.get_hot_topics() / v2ex.search(query) / ' +
|
|
1610
|
+
'xueqiu.get_stock_quote(symbol) / xueqiu.search_stock(query) / youtube.transcribe(url) / <channel>.check() ... ' +
|
|
1611
|
+
'Unknown channel or method? Just call it — the error returns the upstream inventory (channel list or method signatures) so you can self-correct. ' +
|
|
1612
|
+
'Action "status" runs the real `agent-reach doctor` (.venv/bin/agent-reach). ' +
|
|
1613
|
+
'Channels needing cookies/setup return the upstream check() guidance verbatim — do NOT give up there: hand the user the exact configure command, then offer to re-check. ' +
|
|
1614
|
+
'Evidence chain: [agent-reach:<channel>.<method>@ts].',
|
|
1615
|
+
// D-30 第三刀(issue #112):query blob → 平铺 typed;全字段可选 → interpretArgs 容忍层。
|
|
1616
|
+
// action 不设 enum:反射桥按 D-4a' 透传上游新增渠道/方法,gotry 零改动(生态开放面)
|
|
1834
1617
|
parameters: {
|
|
1835
|
-
action: {
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
},
|
|
1839
|
-
|
|
1840
|
-
type: 'string',
|
|
1841
|
-
description: '上游渠道名,如 web/v2ex/xueqiu(action=reach 时)'
|
|
1842
|
-
},
|
|
1843
|
-
method: {
|
|
1844
|
-
type: 'string',
|
|
1845
|
-
description: '上游方法名,如 read/get_hot_topics/get_stock_quote'
|
|
1846
|
-
},
|
|
1847
|
-
args: {
|
|
1848
|
-
type: 'string',
|
|
1849
|
-
description: '空格分隔参数'
|
|
1850
|
-
},
|
|
1851
|
-
timeoutMs: {
|
|
1852
|
-
type: 'integer',
|
|
1853
|
-
description: '超时毫秒'
|
|
1854
|
-
}
|
|
1618
|
+
action: { type: 'string', description: 'status(默认,无 channel 时)或 reach' },
|
|
1619
|
+
channel: { type: 'string', description: '上游渠道名,如 web/v2ex/xueqiu(action=reach 时)' },
|
|
1620
|
+
method: { type: 'string', description: '上游方法名,如 read/get_hot_topics/get_stock_quote' },
|
|
1621
|
+
args: { type: 'string', description: '空格分隔参数' },
|
|
1622
|
+
timeoutMs: { type: 'integer', description: '超时毫秒' },
|
|
1855
1623
|
},
|
|
1856
1624
|
output: {
|
|
1857
|
-
schema: {
|
|
1858
|
-
|
|
1859
|
-
},
|
|
1860
|
-
render: (_args, value)=>[
|
|
1861
|
-
{
|
|
1862
|
-
type: 'text',
|
|
1863
|
-
text: String(value.summary ?? JSON.stringify(value).slice(0, 800))
|
|
1864
|
-
}
|
|
1865
|
-
]
|
|
1625
|
+
schema: { type: 'json' },
|
|
1626
|
+
render: (_args, value) => [{ type: 'text', text: String(value.summary ?? JSON.stringify(value).slice(0, 800)) }],
|
|
1866
1627
|
},
|
|
1867
|
-
async execute
|
|
1628
|
+
async execute(args, _exec) {
|
|
1868
1629
|
const q = unwrapQuery(args);
|
|
1869
1630
|
const started = Date.now();
|
|
1870
1631
|
const dir = await ensureStateDir(config.stateRoot);
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
args: q.args,
|
|
1878
|
-
timeoutMs: q.timeoutMs
|
|
1879
|
-
}
|
|
1880
|
-
});
|
|
1881
|
-
if (!itpAR.result) return declinedObservation('AGENT_REACH', itpAR.trace);
|
|
1882
|
-
if (q.action === 'status' || !q.action && !q.channel) {
|
|
1632
|
+
// D-23 收编(issue #115):status/reach 两分支收敛为单一 AGENT_REACH 效应——
|
|
1633
|
+
// 解译器横切(trace/declined 面)统一,渲染逻辑留在工具层不变
|
|
1634
|
+
const itpAR = await interpretEffect({ effect: 'AGENT_REACH', params: { action: q.action, channel: q.channel, method: q.method, args: q.args, timeoutMs: q.timeoutMs } });
|
|
1635
|
+
if (!itpAR.result)
|
|
1636
|
+
return declinedObservation('AGENT_REACH', itpAR.trace);
|
|
1637
|
+
if (q.action === 'status' || (!q.action && !q.channel)) {
|
|
1883
1638
|
const st = itpAR.result;
|
|
1884
|
-
await recordLatency(join(dir, 'bridge-latency.jsonl'), Date.now() - started, 'agent-reach:doctor').catch(()=>{});
|
|
1885
|
-
const summary = st.via === 'agent-reach-cli'
|
|
1639
|
+
await recordLatency(join(dir, 'bridge-latency.jsonl'), Date.now() - started, 'agent-reach:doctor').catch(() => { });
|
|
1640
|
+
const summary = st.via === 'agent-reach-cli'
|
|
1641
|
+
? `Agent Reach doctor(上游 CLI,原样透传):\n${st.output}\n${st.evidence}`
|
|
1642
|
+
: `Agent Reach 未装配(.venv 缺失)——可选依赖,机/酒/网页社媒读取之外的工具不受影响。\n补装:终端跑 npx @danceiny/gotry doctor --fix;整体依赖状态可调 gotry_doctor 工具查看。\n${st.evidence}`;
|
|
1886
1643
|
return JSON.parse(JSON.stringify({
|
|
1887
|
-
ok: st.ok,
|
|
1888
|
-
|
|
1889
|
-
output: st.output,
|
|
1890
|
-
evidence: st.evidence,
|
|
1891
|
-
summary,
|
|
1892
|
-
latency_ms: Date.now() - started
|
|
1644
|
+
ok: st.ok, via: st.via, output: st.output, evidence: st.evidence, summary,
|
|
1645
|
+
latency_ms: Date.now() - started,
|
|
1893
1646
|
}));
|
|
1894
1647
|
}
|
|
1895
1648
|
if (!q.channel || !q.method) {
|
|
1896
|
-
return JSON.parse(JSON.stringify({
|
|
1897
|
-
ok: false,
|
|
1898
|
-
summary: 'channel 与 method 必填(或 action=status);清单可先随便调一次,inventory 会带回上游渠道/方法表'
|
|
1899
|
-
}));
|
|
1649
|
+
return JSON.parse(JSON.stringify({ ok: false, summary: 'channel 与 method 必填(或 action=status);清单可先随便调一次,inventory 会带回上游渠道/方法表' }));
|
|
1900
1650
|
}
|
|
1901
1651
|
const r = itpAR.result;
|
|
1902
|
-
|
|
1652
|
+
// 长结果干净截断:数组按条目边界保留(dsh 工具上限会拦腰断 JSON,模型只能看到半条)
|
|
1653
|
+
const dataStr = (v) => {
|
|
1903
1654
|
if (Array.isArray(v)) {
|
|
1904
1655
|
const kept = [];
|
|
1905
1656
|
let budget = 3500;
|
|
1906
|
-
for (const item of v){
|
|
1657
|
+
for (const item of v) {
|
|
1907
1658
|
const s = JSON.stringify(item);
|
|
1908
|
-
if (budget - s.length < 0)
|
|
1659
|
+
if (budget - s.length < 0)
|
|
1660
|
+
break;
|
|
1909
1661
|
budget -= s.length + 1;
|
|
1910
1662
|
kept.push(item);
|
|
1911
1663
|
}
|
|
1912
|
-
if (kept.length < v.length)
|
|
1664
|
+
if (kept.length < v.length)
|
|
1665
|
+
kept.push(`…(截断:保留 ${kept.length}/${v.length} 条;可用上游方法带 limit 参数取更少)`);
|
|
1913
1666
|
return kept;
|
|
1914
1667
|
}
|
|
1915
|
-
if (typeof v === 'string')
|
|
1668
|
+
if (typeof v === 'string')
|
|
1669
|
+
return v.length > 4000 ? v.slice(0, 4000) + '…(截断)' : v;
|
|
1916
1670
|
return v;
|
|
1917
1671
|
};
|
|
1918
|
-
if (r.ok)
|
|
1919
|
-
|
|
1920
|
-
|
|
1672
|
+
if (r.ok)
|
|
1673
|
+
r.data = dataStr(r.data);
|
|
1674
|
+
await recordLatency(join(dir, 'bridge-latency.jsonl'), Date.now() - started, `agent-reach:${q.channel}.${q.method}:${r.verdict}`).catch(() => { });
|
|
1675
|
+
const summary = r.verdict === 'found'
|
|
1676
|
+
? `${q.channel}.${q.method} → found (${r.latencyMs}ms)\n${r.evidence}\n${typeof r.data === 'string' ? r.data.slice(0, 600) : JSON.stringify(r.data ?? null).slice(0, 600)}`
|
|
1677
|
+
: r.verdict === 'needs-setup'
|
|
1678
|
+
? `${q.channel}.${q.method} → 需配置(上游 check() 原话): ${r.setup ?? ''}\n${r.evidence}`
|
|
1679
|
+
: r.verdict === 'not-installed'
|
|
1680
|
+
? `${q.channel}.${q.method} → 上游未装配(可选依赖): ${r.setup ?? ''}\n整体依赖状态可调 gotry_doctor 查看;补装: npx @danceiny/gotry doctor --fix\n${r.evidence}`
|
|
1681
|
+
: `${q.channel}.${q.method} → ${r.error ?? 'error'}${r.inventory ? `\n上游清单: ${JSON.stringify(r.inventory).slice(0, 1200)}` : ''}\n${r.evidence}`;
|
|
1921
1682
|
return JSON.parse(JSON.stringify({
|
|
1922
|
-
ok: r.ok,
|
|
1923
|
-
channel: r.channel,
|
|
1924
|
-
method: q.method,
|
|
1925
|
-
verdict: r.verdict,
|
|
1683
|
+
ok: r.ok, channel: r.channel, method: q.method, verdict: r.verdict,
|
|
1926
1684
|
data: typeof r.data === 'string' ? r.data.slice(0, 4000) : r.data,
|
|
1927
|
-
inventory: r.inventory,
|
|
1928
|
-
|
|
1929
|
-
evidence: r.evidence,
|
|
1930
|
-
summary,
|
|
1931
|
-
latency_ms: Date.now() - started
|
|
1685
|
+
inventory: r.inventory, setup: r.setup, evidence: r.evidence, summary,
|
|
1686
|
+
latency_ms: Date.now() - started,
|
|
1932
1687
|
}));
|
|
1933
1688
|
},
|
|
1934
|
-
presentCall: (args)
|
|
1935
|
-
|
|
1936
|
-
title: `Agent Reach:${String(args.channel ?? '')}.${String(args.method ?? 'status')}`,
|
|
1937
|
-
kind: 'fetch',
|
|
1938
|
-
rawInput: args
|
|
1939
|
-
}),
|
|
1940
|
-
presentResult: (args, value)=>{
|
|
1689
|
+
presentCall: args => ({ card: 'generic', title: `Agent Reach:${String(args.channel ?? '')}.${String(args.method ?? 'status')}`, kind: 'fetch', rawInput: args }),
|
|
1690
|
+
presentResult: (args, value) => {
|
|
1941
1691
|
const r = value;
|
|
1942
1692
|
const q = args ?? {};
|
|
1943
1693
|
const icon = r.verdict === 'found' ? '✅' : r.verdict === 'needs-setup' ? '🔧' : r.verdict === 'not-installed' ? '📦' : '❌';
|
|
1944
1694
|
return {
|
|
1945
1695
|
card: 'generic',
|
|
1946
1696
|
title: `AgentReach ${icon} ${q.channel ?? ''}.${q.method ?? 'status'} ${r.verdict ?? ''}`,
|
|
1947
|
-
content: [
|
|
1948
|
-
{
|
|
1949
|
-
type: 'text',
|
|
1950
|
-
text: String(r.summary ?? '')
|
|
1951
|
-
}
|
|
1952
|
-
]
|
|
1697
|
+
content: [{ type: 'text', text: String(r.summary ?? '') }],
|
|
1953
1698
|
};
|
|
1954
|
-
}
|
|
1699
|
+
},
|
|
1955
1700
|
}));
|
|
1701
|
+
// ---- 依赖体检面(2026-09-02 迪拜 session 复盘:可选依赖未装配时,LLM 需要一个
|
|
1702
|
+
// 统一的状态入口来「显示状态 + 引导补装」,而不是在 not-installed 里各自摸索)----
|
|
1956
1703
|
registerGuarded(defineTool({
|
|
1957
1704
|
name: 'gotry_doctor',
|
|
1958
|
-
description: 'Check optional-dependency health for ALL gotry tools (read-only
|
|
1705
|
+
description: '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. '
|
|
1706
|
+
+ 'Call this when ANY gotry tool returns not-installed / needs-setup, when the user asks 「体检/依赖状态/工具为什么不可用」, or BEFORE leaning on a channel for a plan. '
|
|
1707
|
+
+ 'Returns per-item status (ok/degraded/missing) with exact fix commands. '
|
|
1708
|
+
+ 'Action=diagnose (default): pure read-only — never installs anything, only renders the report. '
|
|
1709
|
+
+ '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. '
|
|
1710
|
+
+ '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. '
|
|
1711
|
+
+ '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. '
|
|
1712
|
+
+ 'A markdown report is rendered for the workspace (gotry-state/doctor-report.md) so the sidebar workbench can preview it.',
|
|
1713
|
+
// D-30 第三刀(issue #112):query blob → 平铺 typed;全字段可选 → interpretArgs 容忍层
|
|
1714
|
+
// issue #284 增量:action + items 两参数;默认 diagnose 保持读-only 不变(向后兼容)
|
|
1959
1715
|
parameters: {
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
}
|
|
1716
|
+
action: { type: 'string', enum: ['diagnose', 'repair'], description: '默认 diagnose;repair 时按 items 范围补装可选依赖,user-action / unavailable 项永不冒充 auto' },
|
|
1717
|
+
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' },
|
|
1718
|
+
writeReport: { type: 'boolean', description: '默认 true,把 markdown 报告写进 gotry-state/doctor-report.md(侧栏工作台可预览)' },
|
|
1964
1719
|
},
|
|
1965
1720
|
output: {
|
|
1966
|
-
schema: {
|
|
1967
|
-
|
|
1968
|
-
},
|
|
1969
|
-
render: (_args, value)=>[
|
|
1970
|
-
{
|
|
1971
|
-
type: 'text',
|
|
1972
|
-
text: String(value.summary ?? JSON.stringify(value).slice(0, 900))
|
|
1973
|
-
}
|
|
1974
|
-
]
|
|
1721
|
+
schema: { type: 'json' },
|
|
1722
|
+
render: (_args, value) => [{ type: 'text', text: String(value.summary ?? JSON.stringify(value).slice(0, 900)) }],
|
|
1975
1723
|
},
|
|
1976
|
-
async execute
|
|
1977
|
-
const q = unwrapQuery(args, '
|
|
1978
|
-
const
|
|
1979
|
-
|
|
1980
|
-
});
|
|
1724
|
+
async execute(args, _exec) {
|
|
1725
|
+
const q = unwrapQuery(args, 'action');
|
|
1726
|
+
const action = q?.action === 'repair' ? 'repair' : 'diagnose';
|
|
1727
|
+
const check = seams.doctor?.check ?? runDoctorChecks;
|
|
1728
|
+
const report = await check({ stateRoot: config.stateRoot ?? '.' });
|
|
1729
|
+
// repair 时必须把复检后的报告写盘,避免侧栏仍显示修复前状态。
|
|
1981
1730
|
let reportPath;
|
|
1982
|
-
|
|
1731
|
+
const writeReport = async (current) => {
|
|
1732
|
+
if (q?.writeReport === false)
|
|
1733
|
+
return;
|
|
1983
1734
|
try {
|
|
1984
1735
|
const { writeFile } = await import('node:fs/promises');
|
|
1985
1736
|
const dir = await ensureStateDir(config.stateRoot);
|
|
1986
1737
|
reportPath = join(dir, 'doctor-report.md');
|
|
1987
|
-
await writeFile(reportPath, renderDoctorReportMd(
|
|
1988
|
-
}
|
|
1738
|
+
await writeFile(reportPath, renderDoctorReportMd(current), 'utf-8');
|
|
1739
|
+
}
|
|
1740
|
+
catch {
|
|
1989
1741
|
reportPath = undefined;
|
|
1990
1742
|
}
|
|
1743
|
+
};
|
|
1744
|
+
const icon = (s) => (s === 'ok' ? '✅' : s === 'degraded' ? '⚠️' : '❌');
|
|
1745
|
+
const reportLines = (current) => current.items.map(i => `${icon(i.status)} ${i.label}:${i.detail}${i.fix ? `\n ↳ 修复: ${i.fix}` : ''}`);
|
|
1746
|
+
if (action === 'diagnose') {
|
|
1747
|
+
await writeReport(report);
|
|
1748
|
+
const diagSummary = `${report.summary}\n${reportLines(report).join('\n')}${reportPath ? `\n报告已写: ${reportPath}(侧栏工作台可直接预览)` : '\n(报告写盘失败,仅本对话展示)'}`;
|
|
1749
|
+
// 读-only 路径:保持向后兼容,模型拿到的 verdict/items 与 issue #284 之前一致
|
|
1750
|
+
return JSON.parse(JSON.stringify({
|
|
1751
|
+
ok: true,
|
|
1752
|
+
action: 'diagnose',
|
|
1753
|
+
verdict: report.ok ? 'all-clear' : 'needs-attention',
|
|
1754
|
+
items: report.items,
|
|
1755
|
+
report_path: reportPath,
|
|
1756
|
+
evidence: `[doctor@${new Date().toISOString()}]`,
|
|
1757
|
+
summary: diagSummary,
|
|
1758
|
+
}));
|
|
1759
|
+
}
|
|
1760
|
+
// action='repair':从诊断 → 选定 auto-repairable → scope-keyed 审批 → 真执行(走 bootstrap.runOnboardingFix)→ 复检。
|
|
1761
|
+
// 范围过滤:items 非空时只考虑这些 id(未知 id 静默剔除,落 skipped 不冒充 auto)。
|
|
1762
|
+
// runDoctorRepair 内部懒加载 bin/gotry-bootstrap.js,自适应 status→level 适配 + 真实安装器。
|
|
1763
|
+
const requestedItems = Array.isArray(q?.items) && q.items.length > 0 ? new Set(q.items) : null;
|
|
1764
|
+
const scopedReport = requestedItems
|
|
1765
|
+
? { ...report, items: report.items.filter((i) => requestedItems.has(i.id)) }
|
|
1766
|
+
: report;
|
|
1767
|
+
const exec = _exec;
|
|
1768
|
+
// 生产 repair 让 bootstrap 安装编排复用本工具的同一诊断注册表做终态复检;
|
|
1769
|
+
// fixture 若显式提供 recheck,则保留其隔离实现,再单独取一次工具报告。
|
|
1770
|
+
let recheck;
|
|
1771
|
+
const injectedRepair = seams.doctor?.repair;
|
|
1772
|
+
const repairOptions = {
|
|
1773
|
+
...injectedRepair,
|
|
1774
|
+
recheck: injectedRepair?.recheck ?? (async () => {
|
|
1775
|
+
recheck = await check({ stateRoot: config.stateRoot ?? '.' });
|
|
1776
|
+
return recheck.items.map((item) => ({
|
|
1777
|
+
label: item.label,
|
|
1778
|
+
level: item.status,
|
|
1779
|
+
detail: item.detail,
|
|
1780
|
+
fix: item.fix,
|
|
1781
|
+
}));
|
|
1782
|
+
}),
|
|
1783
|
+
};
|
|
1784
|
+
const repair = await runDoctorRepair(scopedReport, (scopeKey, reason) => repairApprovalGate.request(exec?.agent, scopeKey, reason, { callId: exec?.callId, signal: exec?.signal }), repairOptions);
|
|
1785
|
+
if (repair.approval === 'granted' && !recheck) {
|
|
1786
|
+
recheck = await check({ stateRoot: config.stateRoot ?? '.' });
|
|
1991
1787
|
}
|
|
1992
|
-
const
|
|
1993
|
-
|
|
1994
|
-
const
|
|
1788
|
+
const finalReport = recheck ?? report;
|
|
1789
|
+
await writeReport(finalReport);
|
|
1790
|
+
const diagSummary = `${report.summary}\n${reportLines(report).join('\n')}`;
|
|
1791
|
+
// 整合诊断 + 修复双视角 summary(模型可分清哪段是体检、哪段是修复)
|
|
1792
|
+
const repairLines = [];
|
|
1793
|
+
for (const r of repair.repairs) {
|
|
1794
|
+
repairLines.push(`${r.status === 'installed' ? '✅' : '❌'} ${r.label}: ${r.reason}${r.nextAction ? ` → ${r.nextAction}` : ''}`);
|
|
1795
|
+
}
|
|
1796
|
+
for (const s of repair.skipped) {
|
|
1797
|
+
repairLines.push(`⏭ ${s.label}: ${s.reason}(bucket=${s.bucket})`);
|
|
1798
|
+
}
|
|
1799
|
+
const recheckSummary = recheck
|
|
1800
|
+
? `\n\n[recheck] ${recheck.summary}\n${reportLines(recheck).join('\n')}`
|
|
1801
|
+
: '';
|
|
1802
|
+
const reportSummary = reportPath ? `\n报告已写: ${reportPath}(侧栏工作台可直接预览)` : '\n(报告写盘失败,仅本对话展示)';
|
|
1803
|
+
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}`;
|
|
1995
1804
|
return JSON.parse(JSON.stringify({
|
|
1996
|
-
ok:
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
1805
|
+
ok: repair.ok,
|
|
1806
|
+
action: 'repair',
|
|
1807
|
+
verdict: repair.verdict,
|
|
1808
|
+
diagnosis: report,
|
|
1809
|
+
recheck,
|
|
1810
|
+
plan: {
|
|
1811
|
+
selected: repair.selected,
|
|
1812
|
+
needs_user_action: repair.skipped.filter((s) => s.bucket === 'user-action').map((s) => ({ id: s.id, label: s.label, reason: s.reason })),
|
|
1813
|
+
unavailable: repair.skipped.filter((s) => s.bucket === 'unavailable').map((s) => ({ id: s.id, label: s.label, reason: s.reason })),
|
|
1814
|
+
scope_key: repair.scopeKey,
|
|
1815
|
+
},
|
|
1816
|
+
approval: repair.approval,
|
|
1817
|
+
repairs: repair.repairs,
|
|
1818
|
+
skipped: repair.skipped,
|
|
2000
1819
|
evidence: `[doctor@${new Date().toISOString()}]`,
|
|
2001
|
-
summary
|
|
1820
|
+
summary: combinedSummary,
|
|
1821
|
+
report_path: reportPath,
|
|
2002
1822
|
}));
|
|
2003
1823
|
},
|
|
2004
|
-
presentCall:
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
presentResult: (_args, value)=>{
|
|
1824
|
+
presentCall: args => {
|
|
1825
|
+
const q = unwrapQuery(args, 'action');
|
|
1826
|
+
const a = q?.action === 'repair';
|
|
1827
|
+
return { card: 'generic', title: a ? '🩺 依赖体检+修复(待批准)' : '🩺 依赖体检', kind: 'execute', rawInput: args };
|
|
1828
|
+
},
|
|
1829
|
+
presentResult: (_args, value) => {
|
|
2011
1830
|
const r = value;
|
|
1831
|
+
const deniedLabel = r.approval === 'cancelled'
|
|
1832
|
+
? '↩️ 已取消'
|
|
1833
|
+
: r.approval === 'unavailable'
|
|
1834
|
+
? '⚠️ 无审批通道'
|
|
1835
|
+
: '🚫 已拒绝';
|
|
1836
|
+
const title = r.action === 'repair'
|
|
1837
|
+
? `🩺 修复:${r.verdict === 'repaired' ? '✅ 已就位' : r.verdict === 'partial-repair' ? '⚠️ 部分成功' : r.verdict === 'repair-blocked' ? '❌ 复检未通过' : r.verdict === 'approval-denied' ? deniedLabel : '🔧 无可补装项'}`
|
|
1838
|
+
: `🩺 依赖体检:${r.verdict === 'all-clear' ? '✅ 全部就绪' : '🔧 有项待处理'}`;
|
|
2012
1839
|
return {
|
|
2013
1840
|
card: 'generic',
|
|
2014
|
-
title
|
|
2015
|
-
content: [
|
|
2016
|
-
{
|
|
2017
|
-
type: 'text',
|
|
2018
|
-
text: String(r.summary ?? '')
|
|
2019
|
-
}
|
|
2020
|
-
]
|
|
1841
|
+
title,
|
|
1842
|
+
content: [{ type: 'text', text: String(r.summary ?? '') }],
|
|
2021
1843
|
};
|
|
2022
|
-
}
|
|
1844
|
+
},
|
|
2023
1845
|
}));
|
|
1846
|
+
// ---- 产物面(issue #25):agent 生成的文件可在 dsh 内发现与阅读,不再只是本地文件名 ----
|
|
2024
1847
|
registerGuarded(defineTool({
|
|
2025
1848
|
name: 'gotry_artifacts_list',
|
|
2026
|
-
description: 'List GoTry artifacts — deep-planning deliverables (async runs from the state ledger) plus agent-written markdown files ' +
|
|
1849
|
+
description: 'List GoTry artifacts — deep-planning deliverables (async runs from the state ledger) plus agent-written markdown files ' +
|
|
1850
|
+
'in the working directory (trip plans etc.). READ-ONLY discovery. ' +
|
|
1851
|
+
'Use when the user asks to see/open/revisit a previously generated artifact ' +
|
|
1852
|
+
'(「看看刚才生成的行程」「上次的规划在哪」「打开那个 md」) — list first, then read with gotry_artifacts_read.',
|
|
1853
|
+
// D-30 第三刀(issue #112):query blob → 平铺 typed;全字段可选 → interpretArgs 容忍层
|
|
2027
1854
|
parameters: {
|
|
2028
|
-
limit: {
|
|
2029
|
-
type: 'integer',
|
|
2030
|
-
description: '最多返回条数,默认 20'
|
|
2031
|
-
}
|
|
1855
|
+
limit: { type: 'integer', description: '最多返回条数,默认 20' },
|
|
2032
1856
|
},
|
|
2033
1857
|
output: {
|
|
2034
|
-
schema: {
|
|
2035
|
-
|
|
1858
|
+
schema: { type: 'json' },
|
|
1859
|
+
render: (_args, value) => [{ type: 'text', text: String(value.summary ?? JSON.stringify(value).slice(0, 600)) }],
|
|
1860
|
+
presentationMeta: (_args, value) => {
|
|
1861
|
+
const r = value;
|
|
1862
|
+
const paths = Array.isArray(r.artifacts)
|
|
1863
|
+
? r.artifacts.map(a => String(a.path ?? '')).filter(Boolean)
|
|
1864
|
+
: [];
|
|
1865
|
+
return {
|
|
1866
|
+
shape: 'paths',
|
|
1867
|
+
paths,
|
|
1868
|
+
truncated: Boolean(r.truncated),
|
|
1869
|
+
total: r.total ?? paths.length,
|
|
1870
|
+
};
|
|
2036
1871
|
},
|
|
2037
|
-
render: (_args, value)=>[
|
|
2038
|
-
{
|
|
2039
|
-
type: 'text',
|
|
2040
|
-
text: String(value.summary ?? JSON.stringify(value).slice(0, 600))
|
|
2041
|
-
}
|
|
2042
|
-
]
|
|
2043
1872
|
},
|
|
2044
|
-
async execute
|
|
1873
|
+
async execute(args, exec) {
|
|
2045
1874
|
const q = unwrapQuery(args, 'limit');
|
|
2046
|
-
const r = await listArtifacts({
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
}),
|
|
2065
|
-
presentResult: (_args, value)=>{
|
|
2066
|
-
const r = value;
|
|
1875
|
+
const r = await listArtifacts({ stateRoot: config.stateRoot ?? '.', cwd: sessionCwd(exec), limit: q.limit });
|
|
1876
|
+
const lines = r.artifacts.map(a => `- [${a.source}] ${a.title}${a.status ? `(${a.status})` : ''} — ${a.path}${a.updated ? ` @ ${a.updated.slice(0, 16).replace('T', ' ')}` : ''}`);
|
|
1877
|
+
const summary = r.artifacts.length
|
|
1878
|
+
? `在册产物 ${r.artifacts.length}/${r.total} 项${r.truncated ? '(截断,可加 limit)' : ''}:\n${lines.join('\n')}`
|
|
1879
|
+
: '无在册产物(异步深度规划交付与工作目录 md 文件都会出现在这里)';
|
|
1880
|
+
return JSON.parse(JSON.stringify({ ok: true, artifacts: r.artifacts, total: r.total, truncated: r.truncated, summary }));
|
|
1881
|
+
},
|
|
1882
|
+
// Host presentationMeta remains persisted with the ToolResult for Host-side
|
|
1883
|
+
// consumers; the packaged public Client separately renders the runtime block
|
|
1884
|
+
// for these custom wire names in DSH Web.
|
|
1885
|
+
presentResult: (_args, result) => {
|
|
1886
|
+
if (result.isError || typeof result.meta !== 'object' || result.meta === null || Array.isArray(result.meta))
|
|
1887
|
+
return undefined;
|
|
1888
|
+
const meta = result.meta;
|
|
1889
|
+
if (meta.shape !== 'paths' || !Array.isArray(meta.paths) || !meta.paths.every(p => typeof p === 'string') ||
|
|
1890
|
+
typeof meta.truncated !== 'boolean' || typeof meta.total !== 'number' || !Number.isInteger(meta.total) || meta.total < 0) {
|
|
1891
|
+
return undefined;
|
|
1892
|
+
}
|
|
2067
1893
|
return {
|
|
2068
|
-
card: '
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
}
|
|
2075
|
-
]
|
|
1894
|
+
card: 'search',
|
|
1895
|
+
shape: 'paths',
|
|
1896
|
+
title: `产物:${meta.total} 项在册${meta.truncated ? '(已截断)' : ''}`,
|
|
1897
|
+
paths: meta.paths,
|
|
1898
|
+
truncated: meta.truncated,
|
|
1899
|
+
total: meta.total,
|
|
2076
1900
|
};
|
|
2077
|
-
}
|
|
1901
|
+
},
|
|
2078
1902
|
}));
|
|
2079
1903
|
registerGuarded(defineTool({
|
|
2080
1904
|
name: 'gotry_artifacts_read',
|
|
2081
|
-
description: 'Read one GoTry artifact as a line-numbered file view rendered directly in the chat UI. ' +
|
|
1905
|
+
description: 'Read one GoTry artifact as a line-numbered file view rendered directly in the chat UI. ' +
|
|
1906
|
+
'Input: the path from gotry_artifacts_list, or a bare async ticket id (e.g. dp-xxxx). ' +
|
|
1907
|
+
'Optional offset (1-based) / limit window for paging large files. ' +
|
|
1908
|
+
'READ-ONLY; text artifacts only (md/txt/json/jsonl/csv/log/yaml).',
|
|
1909
|
+
// D-30 第三刀(issue #112):query blob → 平铺 typed;path required → 宿主权入口拒畸形参数(同 flyai/hotel 刀法)
|
|
2082
1910
|
parameters: {
|
|
2083
|
-
path: {
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
description: 'list 返回的路径或工单 id'
|
|
2087
|
-
},
|
|
2088
|
-
offset: {
|
|
2089
|
-
type: 'integer',
|
|
2090
|
-
description: '起始行号(1 起)'
|
|
2091
|
-
},
|
|
2092
|
-
limit: {
|
|
2093
|
-
type: 'integer',
|
|
2094
|
-
description: '窗口行数,默认 400'
|
|
2095
|
-
}
|
|
1911
|
+
path: { type: 'string', required: true, description: 'list 返回的路径或工单 id' },
|
|
1912
|
+
offset: { type: 'integer', description: '起始行号(1 起)' },
|
|
1913
|
+
limit: { type: 'integer', description: '窗口行数,默认 400' },
|
|
2096
1914
|
},
|
|
2097
1915
|
output: {
|
|
2098
|
-
schema: {
|
|
2099
|
-
|
|
1916
|
+
schema: { type: 'json' },
|
|
1917
|
+
render: (_args, value) => [{ type: 'text', text: String(value.content ?? JSON.stringify(value).slice(0, 600)) }],
|
|
1918
|
+
presentationMeta: (_args, value) => {
|
|
1919
|
+
const r = value;
|
|
1920
|
+
return {
|
|
1921
|
+
path: String(r.path ?? ''),
|
|
1922
|
+
offset: r.offset ?? 1,
|
|
1923
|
+
lines: Array.isArray(r.lines) ? r.lines.map(line => ({ number: line.number, text: line.text })) : [],
|
|
1924
|
+
totalLines: r.totalLines ?? 0,
|
|
1925
|
+
...(r.lang ? { lang: r.lang } : {}),
|
|
1926
|
+
...(r.source ? { source: r.source } : {}),
|
|
1927
|
+
...(r.version ? { version: r.version } : {}),
|
|
1928
|
+
};
|
|
2100
1929
|
},
|
|
2101
|
-
render: (_args, value)=>[
|
|
2102
|
-
{
|
|
2103
|
-
type: 'text',
|
|
2104
|
-
text: String(value.content ?? JSON.stringify(value).slice(0, 600))
|
|
2105
|
-
}
|
|
2106
|
-
]
|
|
2107
1930
|
},
|
|
2108
|
-
async execute
|
|
1931
|
+
async execute(args, exec) {
|
|
2109
1932
|
const q = args;
|
|
2110
|
-
if (!q.path)
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
error: 'path 必填(来自 gotry_artifacts_list)'
|
|
2114
|
-
})
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
});
|
|
2121
|
-
if (!r.ok) return JSON.parse(JSON.stringify(r));
|
|
1933
|
+
if (!q.path)
|
|
1934
|
+
throw new Error('gotry_artifacts_read requires path');
|
|
1935
|
+
if (!q.path)
|
|
1936
|
+
return JSON.parse(JSON.stringify({ ok: false, error: 'path 必填(来自 gotry_artifacts_list)' }));
|
|
1937
|
+
const r = await readArtifact({ stateRoot: config.stateRoot ?? '.', cwd: sessionCwd(exec), path: q.path, offset: q.offset, limit: q.limit });
|
|
1938
|
+
if (!r.ok)
|
|
1939
|
+
return JSON.parse(JSON.stringify(r));
|
|
1940
|
+
// 身份/来源展示(issue #285 第 1 条「显示身份/来源」):行号视图保留 read 卡的
|
|
1941
|
+
// path/offset/lines/totalLines/lang;fallback content 用 source label 前缀,
|
|
1942
|
+
// 让无法渲染 read 卡的 UI 也能区分「本会话选了哪个产物」,避免把旧摘要当新内容。
|
|
2122
1943
|
return JSON.parse(JSON.stringify({
|
|
2123
1944
|
...r,
|
|
2124
|
-
|
|
1945
|
+
source: r.source,
|
|
1946
|
+
summary: `${r.path}(${r.totalLines} 行)第 ${r.offset}-${r.offset + r.lines.length - 1} 行${r.windowed ? `(共 ${r.totalLines} 行,可翻页)` : ''}`,
|
|
2125
1947
|
}));
|
|
2126
1948
|
},
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
1949
|
+
// Keep the standard location payload for Host consumers. The Web client only
|
|
1950
|
+
// renders its built-in keyed tools, so this is not claimed as native custom-card E2E.
|
|
1951
|
+
presentCall: args => ({
|
|
1952
|
+
card: 'generic',
|
|
1953
|
+
title: `读产物:${args.path ?? ''}`,
|
|
1954
|
+
kind: 'read',
|
|
1955
|
+
rawInput: args,
|
|
1956
|
+
...(args.path ? { locations: [{ path: String(args.path), line: 1 }] } : {}),
|
|
1957
|
+
}),
|
|
1958
|
+
presentResult: (_args, result) => {
|
|
1959
|
+
if (result.isError || typeof result.meta !== 'object' || result.meta === null || Array.isArray(result.meta))
|
|
1960
|
+
return undefined;
|
|
1961
|
+
const meta = result.meta;
|
|
1962
|
+
if (typeof meta.path !== 'string' || meta.path === '' || typeof meta.offset !== 'number' || !Number.isInteger(meta.offset) || meta.offset < 1 ||
|
|
1963
|
+
!Array.isArray(meta.lines) || !meta.lines.every(line => typeof line === 'object' && line !== null && !Array.isArray(line) &&
|
|
1964
|
+
typeof line.number === 'number' && Number.isInteger(line.number) &&
|
|
1965
|
+
typeof line.text === 'string') ||
|
|
1966
|
+
typeof meta.totalLines !== 'number' || !Number.isInteger(meta.totalLines) || meta.totalLines < 0) {
|
|
1967
|
+
return undefined;
|
|
2146
1968
|
}
|
|
1969
|
+
const source = typeof meta.source === 'string' ? meta.source : 'unknown';
|
|
1970
|
+
const identityLine = `source: ${source} · path: ${meta.path} · ${meta.totalLines} lines`;
|
|
1971
|
+
const rawContent = result.content
|
|
1972
|
+
.filter(block => block.type === 'text')
|
|
1973
|
+
.map(block => block.text)
|
|
1974
|
+
.join('\n');
|
|
2147
1975
|
return {
|
|
2148
1976
|
card: 'read',
|
|
2149
|
-
title:
|
|
2150
|
-
path:
|
|
2151
|
-
offset:
|
|
2152
|
-
lines:
|
|
2153
|
-
totalLines:
|
|
2154
|
-
lang:
|
|
1977
|
+
title: meta.path.split('/').pop() ?? meta.path,
|
|
1978
|
+
path: meta.path,
|
|
1979
|
+
offset: meta.offset,
|
|
1980
|
+
lines: meta.lines,
|
|
1981
|
+
totalLines: meta.totalLines,
|
|
1982
|
+
...(typeof meta.lang === 'string' ? { lang: meta.lang } : {}),
|
|
2155
1983
|
content: [
|
|
2156
|
-
{
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
}
|
|
2160
|
-
]
|
|
1984
|
+
{ type: 'text', text: identityLine },
|
|
1985
|
+
{ type: 'text', text: rawContent },
|
|
1986
|
+
],
|
|
2161
1987
|
};
|
|
2162
|
-
}
|
|
1988
|
+
},
|
|
2163
1989
|
}));
|
|
1990
|
+
// ---- 产物事实闸(issue #46,ADR-19):含可下单事实的产物交付前必过 ----
|
|
1991
|
+
// 每个航班号/时刻/机场/价格/政策断言必须回溯到本会话 exact-date 工具结果
|
|
1992
|
+
// (bookable-facts 侧车,query_id 可重放);无法回溯即 blocked——不得宣称「已验证方案」。
|
|
2164
1993
|
registerGuarded(defineTool({
|
|
2165
1994
|
name: 'gotry_fact_gate',
|
|
2166
|
-
description: 'Factuality gate for itinerary artifacts (issue #46): call BEFORE delivering any artifact that contains bookable facts '
|
|
1995
|
+
description: 'Factuality gate for itinerary artifacts (issue #46): call BEFORE delivering any artifact that contains bookable facts '
|
|
1996
|
+
+ '(flight numbers, times, airports, prices, visa/entry policies). Every bookable claim in the markdown is checked against '
|
|
1997
|
+
+ 'the session fact registry (exact-date tool results recorded by gotry_flyai_search / gotry_session_search — hit AND miss). '
|
|
1998
|
+
+ 'Blocked when: a claimed flight is absent from the exact-date source (never backfill from route pages/history/adjacent dates); '
|
|
1999
|
+
+ 'route+date never queried; times contradict the snapshot; carrier→airport mapping conflicts (FD=DMK vs VZ=BKK are NOT interchangeable); '
|
|
2000
|
+
+ '「联程」without protected_connection=true; policy claims without 「截至 YYYY-MM-DD」; unconditional ✓ on unverified claims. '
|
|
2001
|
+
+ 'Optional itinerary object enables machine invariants (hotel_nights + onboard_nights = total nights, O&D segments vs flight legs, '
|
|
2002
|
+
+ 'budget floor ≥ sum of item minimums). verdict=blocked ⇒ fix or downgrade wording — never present as a verified plan.',
|
|
2003
|
+
// D-30 第三刀(issue #112):query blob → 平铺 typed;markdown/path 二选一无公共 required
|
|
2004
|
+
// → interpretArgs 容忍层;itinerary 保 object+additionalProperties:true(嵌套形状归一在闸内,
|
|
2005
|
+
// #118 D-26 渲染原语切片再收紧),给模型「这是对象」的可见信号
|
|
2167
2006
|
parameters: {
|
|
2168
|
-
markdown: {
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
},
|
|
2172
|
-
path: {
|
|
2173
|
-
type: 'string',
|
|
2174
|
-
description: '产物路径(artifacts_list 返回的);与 markdown 二选一'
|
|
2175
|
-
},
|
|
2176
|
-
tripYear: {
|
|
2177
|
-
type: 'integer',
|
|
2178
|
-
description: '行程年份,如 2027(日期归一用)'
|
|
2179
|
-
},
|
|
2180
|
-
itinerary: {
|
|
2181
|
-
type: 'object',
|
|
2182
|
-
additionalProperties: true,
|
|
2183
|
-
description: '结构化行程 { trip_start, trip_end, stays:[{place,check_in,check_out}], onboard_nights, od_segments:[{from,to,date,mode,legs}], budget_items:[{label,min_cny,max_cny}], claimed_floor_cny? }'
|
|
2184
|
-
}
|
|
2007
|
+
markdown: { type: 'string', description: '产物全文;与 path 二选一' },
|
|
2008
|
+
path: { type: 'string', description: '产物路径(artifacts_list 返回的);与 markdown 二选一' },
|
|
2009
|
+
tripYear: { type: 'integer', description: '行程年份,如 2027(日期归一用)' },
|
|
2010
|
+
itinerary: { type: 'object', additionalProperties: true, description: '结构化行程 { trip_start, trip_end, stays:[{place,check_in,check_out}], onboard_nights, od_segments:[{from,to,date,mode,legs}], budget_items:[{label,min_cny,max_cny}], claimed_floor_cny? }' },
|
|
2185
2011
|
},
|
|
2186
2012
|
output: {
|
|
2187
|
-
schema: {
|
|
2188
|
-
|
|
2189
|
-
},
|
|
2190
|
-
render: (_args, value)=>[
|
|
2191
|
-
{
|
|
2192
|
-
type: 'text',
|
|
2193
|
-
text: String(value.summary ?? JSON.stringify(value).slice(0, 800))
|
|
2194
|
-
}
|
|
2195
|
-
]
|
|
2013
|
+
schema: { type: 'json' },
|
|
2014
|
+
render: (_args, value) => [{ type: 'text', text: String(value.summary ?? JSON.stringify(value).slice(0, 800)) }],
|
|
2196
2015
|
},
|
|
2197
|
-
async execute
|
|
2016
|
+
async execute(args, exec) {
|
|
2198
2017
|
const q = unwrapQuery(args, 'markdown');
|
|
2199
2018
|
const avMap = await loadAirlineAirportMap();
|
|
2200
2019
|
if (!avMap) {
|
|
2201
|
-
return JSON.parse(JSON.stringify({
|
|
2202
|
-
ok: false,
|
|
2203
|
-
summary: '航司机场映射缺失(data/airline-airports.json)——事实闸不可用,fail closed:产物不得宣称已验证'
|
|
2204
|
-
}));
|
|
2020
|
+
return JSON.parse(JSON.stringify({ ok: false, summary: '航司机场映射缺失(data/airline-airports.json)——事实闸不可用,fail closed:产物不得宣称已验证' }));
|
|
2205
2021
|
}
|
|
2206
2022
|
let markdown = q.markdown;
|
|
2207
2023
|
if (!markdown && q.path) {
|
|
2208
|
-
const r = await readArtifact({
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
});
|
|
2212
|
-
if (!r.ok) return JSON.parse(JSON.stringify({
|
|
2213
|
-
ok: false,
|
|
2214
|
-
summary: `产物读取失败:${String(r.error ?? '')}`
|
|
2215
|
-
}));
|
|
2024
|
+
const r = await readArtifact({ stateRoot: config.stateRoot ?? '.', cwd: sessionCwd(exec), path: q.path });
|
|
2025
|
+
if (!r.ok)
|
|
2026
|
+
return JSON.parse(JSON.stringify({ ok: false, summary: `产物读取失败:${String(r.error ?? '')}` }));
|
|
2216
2027
|
markdown = r.content;
|
|
2217
2028
|
}
|
|
2218
|
-
if (!markdown)
|
|
2219
|
-
ok: false,
|
|
2220
|
-
summary: '需要 markdown(产物全文)或 path(产物路径)'
|
|
2221
|
-
}));
|
|
2029
|
+
if (!markdown)
|
|
2030
|
+
return JSON.parse(JSON.stringify({ ok: false, summary: '需要 markdown(产物全文)或 path(产物路径)' }));
|
|
2222
2031
|
const itinerary = q.itinerary;
|
|
2223
2032
|
const registry = await loadFactRegistry(config.stateRoot ?? '.');
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
const
|
|
2229
|
-
const
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
}));
|
|
2235
|
-
},
|
|
2236
|
-
presentCall:
|
|
2237
|
-
|
|
2238
|
-
title: '产物事实闸',
|
|
2239
|
-
kind: 'execute',
|
|
2240
|
-
rawInput: args
|
|
2241
|
-
}),
|
|
2242
|
-
presentResult: (_args, value)=>{
|
|
2033
|
+
// 政策复核提醒桥接(issue #359):闸侧 opts.tripStart 与 renderer 同一传入
|
|
2034
|
+
// 才能让合法 `renderPolicyFact(f, tripStart)` 行通过公开闸。已加载的
|
|
2035
|
+
// itinerary.trip_start 是权威源(同次闸同时驱动 itineraryInvariants),不再
|
|
2036
|
+
// 额外要求调用方传 opts.tripStart;空 itinerary/无 trip_start 即无提醒。
|
|
2037
|
+
const tripStart = typeof itinerary?.trip_start === 'string' ? itinerary.trip_start : undefined;
|
|
2038
|
+
const report = gateArtifact(markdown, registry, avMap, { trip_year: q.tripYear, itinerary, tripStart });
|
|
2039
|
+
const lines = report.violations.slice(0, 20).map(v => ` L${v.line} [${v.kind}] ${v.detail}`);
|
|
2040
|
+
const summary = report.verdict === 'pass'
|
|
2041
|
+
? `事实闸 PASS:${report.traceable}/${report.claims_checked} 可下单 claim 全部回溯到 exact-date 工具结果(query_id 可重放)——可宣称「已验证方案」。`
|
|
2042
|
+
: `事实闸 BLOCKED(${report.violations.length} 违例,${report.traceable}/${report.claims_checked} claim 可回溯)——不得宣称「已验证方案」:\n${lines.join('\n')}\n修正路径:逐条改成 exact-date 源返回的事实,或降级为「未确认/当前不可售,到 D-xx 复核」;exact-date miss 的 route+date 不得用历史班期/相邻日期/航线页填充。`;
|
|
2043
|
+
return JSON.parse(JSON.stringify({ ok: true, ...report, summary }));
|
|
2044
|
+
},
|
|
2045
|
+
presentCall: args => ({ card: 'generic', title: '产物事实闸', kind: 'execute', rawInput: args }),
|
|
2046
|
+
presentResult: (_args, value) => {
|
|
2243
2047
|
const r = value;
|
|
2244
2048
|
return {
|
|
2245
2049
|
card: 'generic',
|
|
2246
2050
|
title: `事实闸:${r.verdict === 'pass' ? '✅ pass' : `⛔ blocked(${r.violations?.length ?? '?'})`}`,
|
|
2247
|
-
content: [
|
|
2248
|
-
{
|
|
2249
|
-
type: 'text',
|
|
2250
|
-
text: String(r.summary ?? '')
|
|
2251
|
-
}
|
|
2252
|
-
]
|
|
2051
|
+
content: [{ type: 'text', text: String(r.summary ?? '') }],
|
|
2253
2052
|
};
|
|
2254
|
-
}
|
|
2053
|
+
},
|
|
2255
2054
|
}));
|
|
2256
2055
|
}
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
//# sourceURL=ts/src/index.ts
|