@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
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
* 文本类(md/txt/json/jsonl/csv/log/yaml/yml)——本工具是「产物查看」,不是通用文件浏览器。
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import {
|
|
16
|
+
import { createHash } from 'node:crypto'
|
|
17
|
+
import { readdir, readFile, realpath, stat } from 'node:fs/promises'
|
|
17
18
|
import { existsSync } from 'node:fs'
|
|
18
19
|
import { isAbsolute, join, resolve, sep } from 'node:path'
|
|
19
20
|
|
|
@@ -31,11 +32,14 @@ export interface ArtifactEntry {
|
|
|
31
32
|
|
|
32
33
|
export interface ArtifactReadView {
|
|
33
34
|
ok: true
|
|
35
|
+
source: 'state-root' | 'cwd'
|
|
34
36
|
path: string
|
|
35
37
|
offset: number
|
|
36
38
|
lines: Array<{ number: number; text: string }>
|
|
37
39
|
totalLines: number
|
|
38
40
|
lang?: string
|
|
41
|
+
/** Short content fingerprint shown by the Web Client to distinguish refreshes. */
|
|
42
|
+
version: string
|
|
39
43
|
content: string
|
|
40
44
|
windowed: boolean
|
|
41
45
|
}
|
|
@@ -100,6 +104,8 @@ async function listDeliverableFiles(root: string, limit: number): Promise<Artifa
|
|
|
100
104
|
const p = join(dir, n)
|
|
101
105
|
const st = await stat(p).catch(() => null)
|
|
102
106
|
if (!st?.isFile()) continue
|
|
107
|
+
const canonical = await realpath(p).catch(() => null)
|
|
108
|
+
if (!canonical || !underRoot(canonical, root) || hasDeniedSegment(canonical)) continue
|
|
103
109
|
entries.push({
|
|
104
110
|
source: 'async-run',
|
|
105
111
|
id: n.replace(/\.deliverable\.md$/, ''),
|
|
@@ -127,6 +133,8 @@ async function listCwdMarkdown(cwd: string, limit: number): Promise<ArtifactEntr
|
|
|
127
133
|
const p = join(cwd, d.name)
|
|
128
134
|
const st = await stat(p).catch(() => null)
|
|
129
135
|
if (!st) continue
|
|
136
|
+
const canonical = await realpath(p).catch(() => null)
|
|
137
|
+
if (!canonical || !underRoot(canonical, cwd) || hasDeniedSegment(canonical)) continue
|
|
130
138
|
entries.push({
|
|
131
139
|
source: 'cwd-file',
|
|
132
140
|
id: d.name,
|
|
@@ -147,10 +155,12 @@ export async function listArtifacts(opts: {
|
|
|
147
155
|
const limit = Math.max(1, Math.min(opts.limit ?? 20, MAX_LIST))
|
|
148
156
|
const root = rootOf(opts.stateRoot)
|
|
149
157
|
const cwd = opts.cwd ? resolve(opts.cwd) : process.cwd()
|
|
158
|
+
const canonicalRoot = await realpath(root).catch(() => root)
|
|
159
|
+
const canonicalCwd = await realpath(cwd).catch(() => cwd)
|
|
150
160
|
|
|
151
161
|
const seenPath = new Set<string>()
|
|
152
162
|
const merged: ArtifactEntry[] = []
|
|
153
|
-
for (const e of [...listRunsFromLedger(
|
|
163
|
+
for (const e of [...listRunsFromLedger(canonicalRoot, 'local', limit), ...(await listDeliverableFiles(canonicalRoot, limit)), ...(await listCwdMarkdown(canonicalCwd, limit))]) {
|
|
154
164
|
if (seenPath.has(e.path)) continue
|
|
155
165
|
seenPath.add(e.path)
|
|
156
166
|
merged.push(e)
|
|
@@ -158,7 +168,7 @@ export async function listArtifacts(opts: {
|
|
|
158
168
|
merged.sort((a, b) => String(b.updated ?? '').localeCompare(String(a.updated ?? '')))
|
|
159
169
|
|
|
160
170
|
const total = merged.length
|
|
161
|
-
return { artifacts: merged.slice(0, limit), total, truncated: total > limit, roots: [
|
|
171
|
+
return { artifacts: merged.slice(0, limit), total, truncated: total > limit, roots: [canonicalRoot, canonicalCwd] }
|
|
162
172
|
}
|
|
163
173
|
|
|
164
174
|
/**
|
|
@@ -177,6 +187,8 @@ export async function readArtifact(opts: {
|
|
|
177
187
|
}): Promise<ArtifactReadView | { ok: false; error: string; hint?: string }> {
|
|
178
188
|
const root = rootOf(opts.stateRoot)
|
|
179
189
|
const cwd = opts.cwd ? resolve(opts.cwd) : process.cwd()
|
|
190
|
+
const canonicalRoot = await realpath(root).catch(() => root)
|
|
191
|
+
const canonicalCwd = await realpath(cwd).catch(() => cwd)
|
|
180
192
|
const raw = String(opts.path ?? '').trim()
|
|
181
193
|
if (!raw) return { ok: false, error: 'path 必填(来自 gotry_artifacts_list 的 path,或异步工单 id)' }
|
|
182
194
|
|
|
@@ -194,7 +206,21 @@ export async function readArtifact(opts: {
|
|
|
194
206
|
filePath = asyncDeliverablePath(root, raw)
|
|
195
207
|
} else {
|
|
196
208
|
const p = asyncDeliverablePath(root, raw)
|
|
197
|
-
|
|
209
|
+
const canonical = await realpath(p).catch(() => null)
|
|
210
|
+
if (canonical) {
|
|
211
|
+
if (!underRoot(canonical, canonicalRoot) || hasDeniedSegment(canonical)) {
|
|
212
|
+
return { ok: false, error: `路径越界:${raw}`, hint: `只读 ${root} 与 dsh 工作目录内的文本产物` }
|
|
213
|
+
}
|
|
214
|
+
const ext = canonical.slice(canonical.lastIndexOf('.') + 1).toLowerCase()
|
|
215
|
+
if (!TEXT_EXT_LANG[ext]) {
|
|
216
|
+
return { ok: false, error: `不支持的文件类型 .${ext}`, hint: `白名单:${Object.keys(TEXT_EXT_LANG).join('/')}` }
|
|
217
|
+
}
|
|
218
|
+
const st = await stat(canonical).catch(() => null)
|
|
219
|
+
if (!st?.isFile()) return { ok: false, error: `文件不存在:${raw}`, hint: '先 gotry_artifacts_list 看在册产物' }
|
|
220
|
+
if (st.size > MAX_BYTES) return { ok: false, error: `文件过大(${st.size} bytes > ${MAX_BYTES})` }
|
|
221
|
+
filePath = canonical
|
|
222
|
+
text = await readFile(canonical, 'utf-8')
|
|
223
|
+
}
|
|
198
224
|
}
|
|
199
225
|
if (text === null) return { ok: false, error: `工单 ${raw} 无 deliverable(未交付或不存在)`, hint: '先 gotry_artifacts_list 看在册产物' }
|
|
200
226
|
}
|
|
@@ -202,19 +228,28 @@ export async function readArtifact(opts: {
|
|
|
202
228
|
// 2) 路径形态:目录白名单 + 扩展名白名单
|
|
203
229
|
if (text === null) {
|
|
204
230
|
const candidates = isAbsolute(raw) ? [resolve(raw)] : [resolve(cwd, raw), resolve(root, raw), resolve(root, 'gotry-state', 'async', raw)]
|
|
205
|
-
const
|
|
231
|
+
const inScope = (p: string) => (underRoot(p, cwd) || underRoot(p, root)) && !hasDeniedSegment(p)
|
|
232
|
+
// Prefer an existing candidate so a cwd miss does not mask a valid state-root file.
|
|
233
|
+
const allowed = candidates.find(p => inScope(p) && existsSync(p)) ?? candidates.find(inScope)
|
|
206
234
|
if (!allowed) {
|
|
207
235
|
return { ok: false, error: `路径越界:${raw}`, hint: `只读 ${root} 与 dsh 工作目录内的文本产物` }
|
|
208
236
|
}
|
|
209
|
-
const
|
|
237
|
+
const canonical = await realpath(allowed).catch(() => null)
|
|
238
|
+
if (!canonical) {
|
|
239
|
+
return { ok: false, error: `文件不存在:${raw}`, hint: '先 gotry_artifacts_list 看在册产物' }
|
|
240
|
+
}
|
|
241
|
+
if (!(underRoot(canonical, canonicalCwd) || underRoot(canonical, canonicalRoot)) || hasDeniedSegment(canonical)) {
|
|
242
|
+
return { ok: false, error: `路径越界:${raw}`, hint: `只读 ${root} 与 dsh 工作目录内的文本产物` }
|
|
243
|
+
}
|
|
244
|
+
const ext = canonical.slice(canonical.lastIndexOf('.') + 1).toLowerCase()
|
|
210
245
|
if (!TEXT_EXT_LANG[ext]) {
|
|
211
246
|
return { ok: false, error: `不支持的文件类型 .${ext}`, hint: `白名单:${Object.keys(TEXT_EXT_LANG).join('/')}` }
|
|
212
247
|
}
|
|
213
|
-
const st = await stat(
|
|
248
|
+
const st = await stat(canonical).catch(() => null)
|
|
214
249
|
if (!st?.isFile()) return { ok: false, error: `文件不存在:${raw}`, hint: '先 gotry_artifacts_list 看在册产物' }
|
|
215
250
|
if (st.size > MAX_BYTES) return { ok: false, error: `文件过大(${st.size} bytes > ${MAX_BYTES})` }
|
|
216
|
-
filePath =
|
|
217
|
-
text = await readFile(
|
|
251
|
+
filePath = canonical
|
|
252
|
+
text = await readFile(canonical, 'utf-8')
|
|
218
253
|
}
|
|
219
254
|
|
|
220
255
|
const allLines = text.split('\n')
|
|
@@ -224,11 +259,13 @@ export async function readArtifact(opts: {
|
|
|
224
259
|
const ext = filePath.slice(filePath.lastIndexOf('.') + 1).toLowerCase()
|
|
225
260
|
return {
|
|
226
261
|
ok: true,
|
|
262
|
+
source: underRoot(filePath, canonicalRoot) ? 'state-root' : 'cwd',
|
|
227
263
|
path: filePath,
|
|
228
264
|
offset,
|
|
229
265
|
lines: slice.map((t, i) => ({ number: offset + i, text: t })),
|
|
230
266
|
totalLines: allLines.length,
|
|
231
267
|
lang: TEXT_EXT_LANG[ext] ?? 'text',
|
|
268
|
+
version: createHash('sha256').update(text).digest('hex').slice(0, 12),
|
|
232
269
|
content: slice.join('\n'),
|
|
233
270
|
windowed: allLines.length > offset - 1 + slice.length,
|
|
234
271
|
}
|
package/ts/capabilities/flyai.ts
CHANGED
|
@@ -113,6 +113,7 @@ interface RawItem {
|
|
|
113
113
|
seatClassName?: string
|
|
114
114
|
}>
|
|
115
115
|
}>
|
|
116
|
+
adultPrice?: string
|
|
116
117
|
ticketPrice?: string
|
|
117
118
|
/** 机/火与酒店条目的顶层 price(未鉴权态为打码串,机/火如 "1xxx",酒店如 "¥7xx") */
|
|
118
119
|
price?: string
|
|
@@ -127,6 +128,51 @@ interface RawItem {
|
|
|
127
128
|
detailUrl?: string
|
|
128
129
|
}
|
|
129
130
|
|
|
131
|
+
interface ParsedItems<T> {
|
|
132
|
+
options: T[]
|
|
133
|
+
malformedCount: number
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function nonEmptyText(value: unknown): string | undefined {
|
|
137
|
+
if (typeof value !== 'string') return undefined
|
|
138
|
+
const text = value.trim()
|
|
139
|
+
return text || undefined
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function optionalText(value: unknown, allowNull = false): string | undefined | null {
|
|
143
|
+
if (value === undefined) return undefined
|
|
144
|
+
if (allowNull && value === null) return null
|
|
145
|
+
return nonEmptyText(value)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function positiveFiniteNumber(value: unknown): number | undefined {
|
|
149
|
+
const number = typeof value === 'number'
|
|
150
|
+
? value
|
|
151
|
+
: typeof value === 'string' && value.trim() ? Number(value.trim()) : Number.NaN
|
|
152
|
+
return Number.isFinite(number) && number > 0 ? number : undefined
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function positiveFiniteDuration(value: unknown): number | undefined {
|
|
156
|
+
if (typeof value === 'number') return positiveFiniteNumber(value)
|
|
157
|
+
const text = nonEmptyText(value)
|
|
158
|
+
if (!text) return undefined
|
|
159
|
+
const minutes = /^(\d+(?:\.\d+)?)\s*分钟$/.exec(text)?.[1]
|
|
160
|
+
return positiveFiniteNumber(minutes ?? text)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function parseTransportPrice(value: unknown): { price: number; priceRaw?: string } | undefined {
|
|
164
|
+
const raw = nonEmptyText(value)
|
|
165
|
+
if (!raw) return undefined
|
|
166
|
+
const bare = raw.replace(/^¥\s*/, '')
|
|
167
|
+
if (/^\d+(?:\.\d+)?$/.test(bare)) {
|
|
168
|
+
const price = Number(bare)
|
|
169
|
+
return Number.isFinite(price) && price > 0 ? { price } : undefined
|
|
170
|
+
}
|
|
171
|
+
// 未鉴权态可能返回 1xxx/¥7xx;保留原值,但绝不把模糊串猜成数字。
|
|
172
|
+
if (/^\d+x+$/.test(bare)) return { price: 0, priceRaw: raw }
|
|
173
|
+
return undefined
|
|
174
|
+
}
|
|
175
|
+
|
|
130
176
|
/**
|
|
131
177
|
* 从 CLI stdout 中提取首个完整且含 itemList 的 JSON 对象。
|
|
132
178
|
*
|
|
@@ -265,7 +311,7 @@ export async function flyaiSearch(q: FlyaiQuery): Promise<FlyaiResult> {
|
|
|
265
311
|
}
|
|
266
312
|
return { ...base, latencyMs, ok: false, via: 'flyai-error', verdict: 'error', evidence: `[实时API:flyai@error@${ts}] ${r.error ?? `exit ${r.code}`}`, error: r.error ?? r.stderr.replace(/\s+/g, ' ').slice(0, 200) }
|
|
267
313
|
}
|
|
268
|
-
let items:
|
|
314
|
+
let items: unknown[]
|
|
269
315
|
try {
|
|
270
316
|
// main-lane 加固的扫描器:容忍 npx 前缀日志/不完整对象——data:null 语义失败
|
|
271
317
|
// (issue #24)的「出发日期非法」原话经 raw stdout 片段进 error 终态,语义不丢
|
|
@@ -278,60 +324,133 @@ export async function flyaiSearch(q: FlyaiQuery): Promise<FlyaiResult> {
|
|
|
278
324
|
}
|
|
279
325
|
|
|
280
326
|
if (q.kind === 'hotel') {
|
|
281
|
-
const
|
|
327
|
+
const parsed = parseHotelItems(items)
|
|
328
|
+
if (parsed.malformedCount > 0) return malformedItemListResult(q.kind, latencyMs, ts, parsed.malformedCount, items.length)
|
|
329
|
+
const hotels = parsed.options
|
|
282
330
|
const verdict: FlyaiResult['verdict'] = hotels.length > 0 ? 'hit' : 'miss'
|
|
283
331
|
return { kind: q.kind, latencyMs, ok: true, via: 'flyai', verdict, evidence: `[实时API:flyai@${ts}] ${hotels.length}/${items.length} hotel options`, hotels }
|
|
284
332
|
}
|
|
285
|
-
const
|
|
333
|
+
const parsed = parseTransportItems(items)
|
|
334
|
+
if (parsed.malformedCount > 0) return malformedItemListResult(q.kind, latencyMs, ts, parsed.malformedCount, items.length)
|
|
335
|
+
const options = parsed.options
|
|
286
336
|
const verdict: FlyaiResult['verdict'] = options.length > 0 ? 'hit' : 'miss'
|
|
287
337
|
return { kind: q.kind, latencyMs, ok: true, via: 'flyai', verdict, evidence: `[实时API:flyai@${ts}] ${options.length}/${items.length} ${q.kind} options`, options }
|
|
288
338
|
}
|
|
289
339
|
|
|
290
|
-
|
|
291
|
-
|
|
340
|
+
function malformedItemListResult(kind: FlyaiKind, latencyMs: number, ts: string, malformedCount: number, total: number): FlyaiResult {
|
|
341
|
+
const label = kind === 'hotel' ? 'hotel' : 'transport'
|
|
342
|
+
const error = `FlyAI ${label} itemList malformed: ${malformedCount}/${total} item(s) failed typed validation`
|
|
343
|
+
return {
|
|
344
|
+
kind,
|
|
345
|
+
latencyMs,
|
|
346
|
+
ok: false,
|
|
347
|
+
via: 'flyai-error',
|
|
348
|
+
verdict: 'error',
|
|
349
|
+
evidence: `[实时API:flyai@error@${ts}] ${label} itemList malformed (${malformedCount}/${total} items failed typed validation; valid siblings discarded)`,
|
|
350
|
+
error,
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** 机/火条目 → FlyaiOption;非空列表含任一不完整条目即整体 malformed */
|
|
355
|
+
function parseTransportItems(items: unknown[]): ParsedItems<FlyaiOption> {
|
|
292
356
|
const options: FlyaiOption[] = []
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
if (!
|
|
296
|
-
|
|
297
|
-
|
|
357
|
+
let malformedCount = 0
|
|
358
|
+
for (const raw of items) {
|
|
359
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
360
|
+
malformedCount += 1
|
|
361
|
+
continue
|
|
362
|
+
}
|
|
363
|
+
const it = raw as RawItem
|
|
364
|
+
const journey = Array.isArray(it.journeys) ? it.journeys[0] : undefined
|
|
365
|
+
const seg = journey && typeof journey === 'object' && !Array.isArray(journey) && Array.isArray(journey.segments)
|
|
366
|
+
? journey.segments[0]
|
|
367
|
+
: undefined
|
|
368
|
+
if (!seg || typeof seg !== 'object' || Array.isArray(seg)) {
|
|
369
|
+
malformedCount += 1
|
|
370
|
+
continue
|
|
371
|
+
}
|
|
372
|
+
const no = nonEmptyText(seg.marketingTransportNo)
|
|
373
|
+
const name = optionalText(seg.marketingTransportName)
|
|
374
|
+
const depDateTime = nonEmptyText(seg.depDateTime)
|
|
375
|
+
const arrDateTime = nonEmptyText(seg.arrDateTime)
|
|
376
|
+
const depStation = nonEmptyText(seg.depStationName)
|
|
377
|
+
const arrStation = nonEmptyText(seg.arrStationName)
|
|
378
|
+
const durationMin = positiveFiniteDuration(seg.duration)
|
|
379
|
+
const rawPriceValue = it.adultPrice !== undefined
|
|
380
|
+
? it.adultPrice
|
|
381
|
+
: it.ticketPrice !== undefined
|
|
382
|
+
? it.ticketPrice
|
|
383
|
+
: it.price
|
|
384
|
+
const parsedPrice = parseTransportPrice(rawPriceValue)
|
|
385
|
+
const seatClass = optionalText(seg.seatClassName)
|
|
386
|
+
const jumpUrl = optionalText(it.jumpUrl)
|
|
387
|
+
if (!no || !depDateTime || !arrDateTime || !depStation || !arrStation || durationMin === undefined || !parsedPrice
|
|
388
|
+
|| (seg.marketingTransportName !== undefined && name === undefined)
|
|
389
|
+
|| (seg.seatClassName !== undefined && seatClass === undefined)
|
|
390
|
+
|| (it.jumpUrl !== undefined && jumpUrl === undefined)) {
|
|
391
|
+
malformedCount += 1
|
|
392
|
+
continue
|
|
393
|
+
}
|
|
298
394
|
options.push({
|
|
299
|
-
no
|
|
300
|
-
name:
|
|
301
|
-
depDateTime
|
|
302
|
-
arrDateTime
|
|
303
|
-
depStation
|
|
304
|
-
arrStation
|
|
305
|
-
durationMin
|
|
306
|
-
price:
|
|
307
|
-
priceRaw:
|
|
308
|
-
seatClass:
|
|
309
|
-
jumpUrl:
|
|
395
|
+
no,
|
|
396
|
+
name: name ?? '',
|
|
397
|
+
depDateTime,
|
|
398
|
+
arrDateTime,
|
|
399
|
+
depStation,
|
|
400
|
+
arrStation,
|
|
401
|
+
durationMin,
|
|
402
|
+
price: parsedPrice.price,
|
|
403
|
+
priceRaw: parsedPrice.priceRaw,
|
|
404
|
+
seatClass: seatClass ?? undefined,
|
|
405
|
+
jumpUrl: jumpUrl ?? undefined,
|
|
310
406
|
})
|
|
311
407
|
}
|
|
312
|
-
return options
|
|
408
|
+
return { options, malformedCount }
|
|
313
409
|
}
|
|
314
410
|
|
|
315
|
-
/** 酒店条目 → FlyaiHotelOption
|
|
316
|
-
function parseHotelItems(items:
|
|
411
|
+
/** 酒店条目 → FlyaiHotelOption;非空列表含任一不完整条目即整体 malformed */
|
|
412
|
+
function parseHotelItems(items: unknown[]): ParsedItems<FlyaiHotelOption> {
|
|
317
413
|
const hotels: FlyaiHotelOption[] = []
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
414
|
+
let malformedCount = 0
|
|
415
|
+
for (const raw of items) {
|
|
416
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
417
|
+
malformedCount += 1
|
|
418
|
+
continue
|
|
419
|
+
}
|
|
420
|
+
const it = raw as RawItem
|
|
421
|
+
const name = nonEmptyText(it.name)
|
|
422
|
+
const star = optionalText(it.star)
|
|
423
|
+
const rawPrice = optionalText(it.price)
|
|
424
|
+
const rate = optionalText(it.rate, true)
|
|
425
|
+
const address = optionalText(it.address)
|
|
426
|
+
const poi = optionalText(it.interestsPoi)
|
|
427
|
+
const hotelId = optionalText(it.shId)
|
|
428
|
+
const jumpUrl = optionalText(it.detailUrl)
|
|
429
|
+
if (!name
|
|
430
|
+
|| (it.star !== undefined && star === undefined)
|
|
431
|
+
|| (it.price !== undefined && rawPrice === undefined)
|
|
432
|
+
|| (it.rate !== undefined && rate === undefined)
|
|
433
|
+
|| (it.address !== undefined && address === undefined)
|
|
434
|
+
|| (it.interestsPoi !== undefined && poi === undefined)
|
|
435
|
+
|| (it.shId !== undefined && hotelId === undefined)
|
|
436
|
+
|| (it.detailUrl !== undefined && jumpUrl === undefined)) {
|
|
437
|
+
malformedCount += 1
|
|
438
|
+
continue
|
|
439
|
+
}
|
|
321
440
|
// 打码价如 "¥7xx" 绝不能截成数字 7(会伪装成真价)——仅全数字串才落 price
|
|
322
|
-
const bare = rawPrice.replace(/^[¥]/, '')
|
|
441
|
+
const bare = (rawPrice ?? '').replace(/^[¥]/, '')
|
|
323
442
|
const numericPrice = Number(bare)
|
|
324
443
|
hotels.push({
|
|
325
|
-
name
|
|
326
|
-
star:
|
|
444
|
+
name,
|
|
445
|
+
star: star ?? undefined,
|
|
327
446
|
price: /^\d+(\.\d+)?$/.test(bare) && numericPrice > 0 ? numericPrice : 0,
|
|
328
447
|
priceRaw: rawPrice || undefined,
|
|
329
|
-
rate:
|
|
330
|
-
address:
|
|
331
|
-
poi:
|
|
332
|
-
hotelId:
|
|
333
|
-
jumpUrl:
|
|
448
|
+
rate: rate ?? undefined,
|
|
449
|
+
address: address ?? undefined,
|
|
450
|
+
poi: poi ?? undefined,
|
|
451
|
+
hotelId: hotelId ?? undefined,
|
|
452
|
+
jumpUrl: jumpUrl ?? undefined,
|
|
334
453
|
})
|
|
335
454
|
}
|
|
336
|
-
return hotels
|
|
337
|
-
}
|
|
455
|
+
return { options: hotels, malformedCount }
|
|
456
|
+
}
|
package/ts/capabilities/hbcli.ts
CHANGED
|
@@ -207,7 +207,7 @@ export async function searchHotels(
|
|
|
207
207
|
// 自动跑过官方脚本;PATH 未含 ~/.local/bin 时上方候选路径也已兜住),
|
|
208
208
|
// 裸 "spawn hbcli ENOENT" 读起来像工具坏了——实际静态包降级是设计行为
|
|
209
209
|
const rawReason = live.error ?? live.via
|
|
210
|
-
const reason = /ENOENT/i.test(rawReason) ? '未安装 hbcli(可选实时源;npx gotry setup 可按官方脚本安装)' : rawReason
|
|
210
|
+
const reason = /ENOENT/i.test(rawReason) ? '未安装 hbcli(可选实时源;npx @danceiny/gotry setup 可按官方脚本安装)' : rawReason
|
|
211
211
|
// 降级:读静态包,按目的地过滤命中的住宿块(issue #24)——整包倾倒会把无关场景
|
|
212
212
|
// (深圳/普吉/曼谷/云南/大理混装)灌给模型且不指明哪块相关;包内无该目的地时明示
|
|
213
213
|
// 「无数据」而不是伪装成可用结果。
|
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 进程级事故日志(D-NEW
|
|
2
|
+
* 进程级事故日志(D-NEW):同步、durable 的 JSONL 诊断记录。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* 设计: 进程级 handler 同步写盘(fsync),尽量在进程被杀前留下事故证据。
|
|
9
|
-
* handler 自身不再抛——再次抛出会绕过 handler,变成无声死亡。
|
|
4
|
+
* `uncaughtExceptionMonitor` 只观察 fatal 事故并留下证据;宿主仍决定
|
|
5
|
+
* 是否退出。GoTry 不吞掉 fatal 事件、不调用 process.exit、也不隐式重启。
|
|
6
|
+
* 工具 execute 的业务失败仍由 guardToolExecute 转成结构化失败返回。
|
|
10
7
|
*
|
|
11
8
|
* §11 状态面同步:事故日志是 gotry-state/incidents.jsonl,
|
|
12
|
-
*
|
|
9
|
+
* 用户可见、可清理。
|
|
13
10
|
*/
|
|
14
11
|
|
|
15
|
-
import {
|
|
12
|
+
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, writeFileSync } from 'node:fs'
|
|
16
13
|
import { dirname, isAbsolute, join } from 'node:path'
|
|
17
|
-
import {
|
|
14
|
+
import { pathToFileURL } from 'node:url'
|
|
18
15
|
import type { ToolFailure } from '../src/tool-packet.ts'
|
|
19
16
|
|
|
20
17
|
export type IncidentKind = 'uncaughtException' | 'unhandledRejection' | 'plugin_error' | 'tool_execute_error'
|
|
@@ -43,68 +40,104 @@ function ensureDir(path: string): void {
|
|
|
43
40
|
|
|
44
41
|
let installed = false
|
|
45
42
|
let logPath: string | null = null
|
|
43
|
+
let activeMonitor: ((err: Error, origin: NodeJS.UncaughtExceptionOrigin) => void) | null = null
|
|
44
|
+
|
|
45
|
+
function safeErrorMessage(value: unknown): string {
|
|
46
|
+
try {
|
|
47
|
+
if (value instanceof Error) return typeof value.message === 'string' ? value.message : '[error-without-message]'
|
|
48
|
+
if (typeof value === 'string') return value
|
|
49
|
+
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint' || typeof value === 'symbol') return String(value)
|
|
50
|
+
const json = JSON.stringify(value)
|
|
51
|
+
return typeof json === 'string' ? json : '[unprintable-error]'
|
|
52
|
+
} catch {
|
|
53
|
+
return '[unprintable-error]'
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function safeErrorStack(value: unknown): string {
|
|
58
|
+
try {
|
|
59
|
+
const stack = (value as { stack?: unknown } | null | undefined)?.stack
|
|
60
|
+
return typeof stack === 'string' ? stack : ''
|
|
61
|
+
} catch {
|
|
62
|
+
return ''
|
|
63
|
+
}
|
|
64
|
+
}
|
|
46
65
|
|
|
47
66
|
/**
|
|
48
|
-
* 写入一条事故并 fsync
|
|
49
|
-
*
|
|
67
|
+
* 写入一条事故并 fsync。整个 append 使用同一个 fd,并保证所有失败路径
|
|
68
|
+
* 都尝试 close;返回 true 只表示写入、fsync、close 全部成功。
|
|
50
69
|
*/
|
|
51
70
|
export function recordIncident(inc: Incident, stateRoot?: string): boolean {
|
|
52
71
|
const target = stateRoot ? resolveIncidentsPath(stateRoot) : (logPath ?? resolveIncidentsPath('.'))
|
|
72
|
+
let fd: number | undefined
|
|
73
|
+
let closeAttempted = false
|
|
53
74
|
try {
|
|
54
75
|
ensureDir(target)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
76
|
+
fd = openSync(target, 'a')
|
|
77
|
+
writeFileSync(fd, JSON.stringify(inc) + '\n', { encoding: 'utf8' })
|
|
78
|
+
fsyncSync(fd)
|
|
79
|
+
closeAttempted = true
|
|
80
|
+
closeSync(fd)
|
|
81
|
+
fd = undefined
|
|
60
82
|
return true
|
|
61
83
|
} catch {
|
|
62
|
-
//
|
|
84
|
+
// 事故处理中的诊断失败绝不能再次抛出。
|
|
63
85
|
return false
|
|
86
|
+
} finally {
|
|
87
|
+
if (fd !== undefined && !closeAttempted) {
|
|
88
|
+
closeAttempted = true
|
|
89
|
+
try { closeSync(fd) } catch { /* best effort close on a failed write */ }
|
|
90
|
+
}
|
|
64
91
|
}
|
|
65
92
|
}
|
|
66
93
|
|
|
67
94
|
/**
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* @returns 一个 disposer,卸载所有监听(测试场景使用)
|
|
95
|
+
* 安装进程级事故观察器。只挂 monitor,不改变 Node/dsh 的 fatal 行为。
|
|
96
|
+
* 进程整个生命周期仅生效一次;disposer 供宿主卸载和测试使用。
|
|
72
97
|
*/
|
|
73
98
|
export function installProcessGuards(stateRoot: string, labels?: { uncaughtException?: string; unhandledRejection?: string }): () => void {
|
|
74
99
|
if (installed) return () => { /* 已装,no-op */ }
|
|
75
100
|
installed = true
|
|
76
101
|
logPath = resolveIncidentsPath(stateRoot)
|
|
77
102
|
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
kind
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
103
|
+
const monitor = (err: Error, origin: NodeJS.UncaughtExceptionOrigin) => {
|
|
104
|
+
try {
|
|
105
|
+
const kind: IncidentKind = origin === 'unhandledRejection' ? 'unhandledRejection' : 'uncaughtException'
|
|
106
|
+
const source = kind === 'unhandledRejection'
|
|
107
|
+
? (labels?.unhandledRejection ?? 'promise')
|
|
108
|
+
: (labels?.uncaughtException ?? origin)
|
|
109
|
+
recordIncident({
|
|
110
|
+
ts: new Date().toISOString(),
|
|
111
|
+
kind,
|
|
112
|
+
message: safeErrorMessage(err).slice(0, 2000),
|
|
113
|
+
stack: safeErrorStack(err).slice(0, 4000),
|
|
114
|
+
source,
|
|
115
|
+
}, stateRoot)
|
|
116
|
+
} catch {
|
|
117
|
+
// Formatting must never replace the original fatal event. Keep a final
|
|
118
|
+
// plain fallback attempt for diagnostics, while the host retains control.
|
|
119
|
+
try {
|
|
120
|
+
recordIncident({
|
|
121
|
+
ts: new Date().toISOString(),
|
|
122
|
+
kind: origin === 'unhandledRejection' ? 'unhandledRejection' : 'uncaughtException',
|
|
123
|
+
message: '[incident-observer-fallback]',
|
|
124
|
+
source: 'incident-observer',
|
|
125
|
+
}, stateRoot)
|
|
126
|
+
} catch { /* observer is strictly best effort */ }
|
|
127
|
+
}
|
|
100
128
|
}
|
|
101
129
|
|
|
102
|
-
process.on('
|
|
103
|
-
|
|
130
|
+
process.on('uncaughtExceptionMonitor', monitor)
|
|
131
|
+
activeMonitor = monitor
|
|
132
|
+
let active = true
|
|
104
133
|
return () => {
|
|
105
|
-
|
|
106
|
-
|
|
134
|
+
if (!active) return
|
|
135
|
+
active = false
|
|
136
|
+
if (activeMonitor !== monitor) return
|
|
137
|
+
process.off('uncaughtExceptionMonitor', monitor)
|
|
107
138
|
installed = false
|
|
139
|
+
logPath = null
|
|
140
|
+
activeMonitor = null
|
|
108
141
|
}
|
|
109
142
|
}
|
|
110
143
|
|
|
@@ -112,24 +145,23 @@ export function installProcessGuards(stateRoot: string, labels?: { uncaughtExcep
|
|
|
112
145
|
* 工具执行面异常隔离(D-NEW gotry 侧收尾):dsh 的一个工具 execute 抛错/拒绝
|
|
113
146
|
* 会沿 cordis 传到主循环,拖垮整个会话。包装后:降级为结构化错误返回给 LLM、
|
|
114
147
|
* 事故落盘,永不向上抛。落盘失败也不抛(双保险,仍返回结构化错误)。
|
|
115
|
-
* 失败形状 = ToolFailure(RFC S1 observation envelope 的失败分支,编译期对齐)。
|
|
116
148
|
*/
|
|
117
149
|
export function guardToolExecute<A, R>(name: string, stateRoot: string, execute: (args: A, exec: unknown) => R | Promise<R>): (args: A, exec: unknown) => Promise<R> {
|
|
118
150
|
return async (args: A, exec: unknown): Promise<R> => {
|
|
119
151
|
try {
|
|
120
152
|
return await execute(args, exec)
|
|
121
153
|
} catch (e) {
|
|
122
|
-
const
|
|
154
|
+
const message = safeErrorMessage(e)
|
|
123
155
|
recordIncident({
|
|
124
156
|
ts: new Date().toISOString(),
|
|
125
157
|
kind: 'tool_execute_error',
|
|
126
|
-
message: `${name}: ${
|
|
127
|
-
stack: (
|
|
158
|
+
message: `${name}: ${message}`.slice(0, 2000),
|
|
159
|
+
stack: safeErrorStack(e).slice(0, 4000),
|
|
128
160
|
source: name,
|
|
129
161
|
}, stateRoot)
|
|
130
162
|
const failure: ToolFailure = {
|
|
131
163
|
ok: false,
|
|
132
|
-
summary: `gotry_${name} 内部错误(已隔离,会话继续): ${
|
|
164
|
+
summary: `gotry_${name} 内部错误(已隔离,会话继续): ${message.slice(0, 300)}`,
|
|
133
165
|
evidence: `[incident:tool_execute_error@${new Date().toISOString()}]`,
|
|
134
166
|
}
|
|
135
167
|
return failure as R
|
|
@@ -137,12 +169,9 @@ export function guardToolExecute<A, R>(name: string, stateRoot: string, execute:
|
|
|
137
169
|
}
|
|
138
170
|
}
|
|
139
171
|
|
|
140
|
-
/** CLI
|
|
141
|
-
if (
|
|
172
|
+
/** CLI 调试:最小 fatal 子进程会留下事故证据并按 Node 原生语义非零退出。 */
|
|
173
|
+
if (process.argv[1] !== undefined && pathToFileURL(process.argv[1]).href === import.meta.url && process.argv[2] === '--smoke') {
|
|
142
174
|
const stateRoot = process.argv[3] ?? '.'
|
|
143
175
|
installProcessGuards(stateRoot, { uncaughtException: 'smoke', unhandledRejection: 'smoke' })
|
|
144
|
-
|
|
145
|
-
// 触发一次未捕获,应被写盘(不真正杀进程——进程继续跑完)
|
|
146
|
-
setTimeout(() => Promise.reject(new Error('intentional smoke-rejection')), 50)
|
|
147
|
-
setTimeout(() => { console.log('SMOKE OK'); process.exit(0) }, 500)
|
|
176
|
+
setTimeout(() => { throw new Error('intentional smoke-uncaught') }, 50)
|
|
148
177
|
}
|