@danceiny/gotry 0.0.1-rc.16 → 0.0.1-rc.18
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 +161 -116
- package/README.zh-CN.md +173 -144
- package/bin/gotry-booking-copilot.js +53 -0
- package/bin/gotry-bootstrap.js +113 -66
- package/bin/gotry-inner.js +436 -70
- package/bin/gotry-runtime-resolution.d.ts +27 -0
- package/bin/gotry-runtime-resolution.js +50 -0
- package/bin/gotry.js +1 -1
- package/cordis.gotry-patch.yml +5 -1
- package/dist/capabilities/agent-reach-deep.js +1 -1
- package/dist/capabilities/agent-reach.js +1 -1
- package/dist/capabilities/anything.js +1 -1
- package/dist/capabilities/artifacts.js +1 -1
- package/dist/capabilities/effect.js +1 -1
- package/dist/capabilities/fact-log.js +1 -1
- package/dist/capabilities/flyai.js +20 -6
- package/dist/capabilities/hbcli.js +1 -1
- package/dist/capabilities/incident-log.js +1 -1
- package/dist/capabilities/model-override.js +18 -0
- package/dist/capabilities/opensky.js +1 -1
- package/dist/capabilities/resilience.js +1 -1
- package/dist/capabilities/session/action-cache.js +1 -1
- package/dist/capabilities/session/adapters/ctrip-flight.js +1 -1
- package/dist/capabilities/session/adapters/meituan-local.js +1 -1
- package/dist/capabilities/session/benchmark.js +1 -1
- package/dist/capabilities/session/extension-bridge.js +19 -6
- package/dist/capabilities/session/extension-channel.js +3 -3
- package/dist/capabilities/session/extension-distribution.js +234 -0
- package/dist/capabilities/session/extract.js +1 -1
- package/dist/capabilities/session/golden-score.js +92 -0
- package/dist/capabilities/session/health-watch.js +1 -1
- package/dist/capabilities/session/read-guard.js +1 -1
- package/dist/capabilities/session/static-flight-golden.js +137 -0
- package/dist/capabilities/session/transport.js +1 -1
- package/dist/capabilities/session/wizard.js +21 -251
- package/dist/capabilities/session-consent.js +1 -1
- package/dist/capabilities/session-login.js +11 -5
- package/dist/capabilities/session-search.js +46 -4
- package/dist/capabilities/weather.js +168 -46
- package/dist/data/session-golden-20.json +25 -0
- package/dist/data/sf-golden-manifest.json +102 -0
- package/dist/data/sf-static-routes.json +91 -0
- package/dist/scripts/action-cache-tests.js +1 -1
- package/dist/scripts/agent-planning-budget-e2e.js +227 -0
- package/dist/scripts/agent-planning-budget-tests.js +173 -0
- package/dist/scripts/agent-reach-deep-tests.js +1 -1
- package/dist/scripts/agent-reach-tests.js +1 -1
- package/dist/scripts/agent-reach-wrapper-tests.js +1 -1
- package/dist/scripts/anything-tests.js +1 -1
- package/dist/scripts/async-collect.js +1 -1
- package/dist/scripts/benchmark-environment-bridge-e2e.js +981 -0
- package/dist/scripts/benchmark-environment-bridge-tests.js +2544 -0
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +335 -0
- package/dist/scripts/booking-copilot-availability-policy-v2-tests.js +1355 -0
- package/dist/scripts/booking-copilot-bin-proof-tests.js +89 -0
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +113 -0
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +356 -0
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +557 -0
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +140 -0
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +202 -0
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +102 -0
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +374 -0
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +447 -0
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +205 -0
- package/dist/scripts/booking-copilot-server-proof-tests.js +317 -0
- package/dist/scripts/booking-copilot-startup-proof-tests.js +283 -0
- package/dist/scripts/booking-copilot-v2-runtime-proof-tests.js +3935 -0
- package/dist/scripts/booking-saga-tests.js +1 -1
- package/dist/scripts/booking-surface-contract-proof-tests.js +393 -0
- package/dist/scripts/booking-surface-v2-contract-proof-tests.js +1174 -0
- package/dist/scripts/bootstrap-tests.js +45 -39
- package/dist/scripts/build-changelog.js +1 -1
- package/dist/scripts/changelog-tests.js +1 -1
- package/dist/scripts/companion-tests.js +1 -1
- package/dist/scripts/diff-test.js +1 -1
- package/dist/scripts/dsh-runtime-closure-tests.js +225 -0
- package/dist/scripts/dsh-runtime-closure.js +150 -0
- package/dist/scripts/effect-tests.js +1 -1
- package/dist/scripts/engine-run.js +1 -1
- package/dist/scripts/engine-tests.js +1 -1
- package/dist/scripts/evaluation-cadence-tests.js +347 -0
- package/dist/scripts/evaluation-contract-tests.js +574 -0
- package/dist/scripts/extension-distribution-cli.js +35 -0
- package/dist/scripts/extension-distribution-tests.js +341 -0
- package/dist/scripts/extension-tests.js +128 -23
- package/dist/scripts/fact-gate-tests.js +1 -1
- package/dist/scripts/flyai-tests.js +1 -1
- package/dist/scripts/hbcli-e2e-tests.js +1 -1
- package/dist/scripts/hbcli-tests.js +1 -1
- package/dist/scripts/health-watch-cli.js +1 -1
- package/dist/scripts/i18n-tests.js +1 -1
- package/dist/scripts/incident-tests.js +1 -1
- package/dist/scripts/journey-tests.js +1 -1
- package/dist/scripts/ledger-tests.js +1 -1
- package/dist/scripts/ledger-workflow-crash.js +1 -1
- package/dist/scripts/memory-capture-tests.js +1 -1
- package/dist/scripts/memory-decay-tests.js +1 -1
- package/dist/scripts/memory-metrics.js +1 -1
- package/dist/scripts/memory-value-report.js +1 -1
- package/dist/scripts/model-override-e2e.js +176 -0
- package/dist/scripts/nightly-evidence-tests.js +1 -1
- package/dist/scripts/nightly-evidence.js +1 -1
- package/dist/scripts/nudge-digest.js +1 -1
- package/dist/scripts/onboarding-tests.js +21 -53
- package/dist/scripts/opensky-check.js +1 -1
- package/dist/scripts/opensky-tests.js +1 -1
- package/dist/scripts/pnpm-dsh-closure-proof.js +20 -0
- package/dist/scripts/price-drift-tests.js +1 -1
- package/dist/scripts/price-drift-watch.js +1 -1
- package/dist/scripts/probe-poi-tests.js +1 -1
- package/dist/scripts/product-metrics.js +1 -1
- package/dist/scripts/publish-preverify.js +40 -4
- package/dist/scripts/realtime-pricing-tests.js +1 -1
- package/dist/scripts/replay-async.js +1 -1
- package/dist/scripts/replay-real.js +1 -1
- package/dist/scripts/replay.js +1 -1
- package/dist/scripts/session-attach-diagnose.js +1 -1
- package/dist/scripts/session-attach-poc.js +1 -1
- package/dist/scripts/session-benchmark.js +1 -1
- package/dist/scripts/session-extract-tests.js +1 -1
- package/dist/scripts/session-login.js +1 -1
- package/dist/scripts/session-tests.js +70 -20
- package/dist/scripts/sf-live-benchmark.js +338 -0
- package/dist/scripts/sf-live-cli-tests.js +21 -0
- package/dist/scripts/sf-soft-score-tests.js +108 -0
- package/dist/scripts/sf-summary.js +93 -0
- package/dist/scripts/skeleton-check.js +1 -1
- package/dist/scripts/skeleton-integration-test.js +1 -1
- package/dist/scripts/skills-contract-tests.js +1 -1
- package/dist/scripts/smoke-session-gate-tests.js +29 -0
- package/dist/scripts/smoke.js +79 -33
- package/dist/scripts/state-cli-tests.js +1 -1
- package/dist/scripts/state-cli.js +1 -1
- package/dist/scripts/static-golden-tests.js +299 -0
- package/dist/scripts/time-eval-tests.js +1 -1
- package/dist/scripts/travel-timeline-tests.js +1 -1
- package/dist/scripts/unified-tests.js +1 -1
- package/dist/scripts/weather-tests.js +694 -44
- package/dist/scripts/z3-race-tests.js +1 -1
- package/dist/src/artifact-gate.js +1 -1
- package/dist/src/benchmark-agent-conformance.js +370 -0
- package/dist/src/benchmark-environment-bridge.js +384 -0
- package/dist/src/benchmark-headless-child-diagnostics.js +173 -0
- package/dist/src/benchmark-tool-isolation.js +124 -0
- package/dist/src/bookable-facts.js +1 -1
- package/dist/src/booking-saga.js +1 -1
- package/dist/src/booking-surface/availability-policy-v2.js +830 -0
- package/dist/src/booking-surface/canonical-schema.js +113 -0
- package/dist/src/booking-surface/contracts-v2.js +89 -0
- package/dist/src/booking-surface/contracts.js +46 -0
- package/dist/src/booking-surface/dsh-planner.js +453 -0
- package/dist/src/booking-surface/dsh-plugin.js +93 -0
- package/dist/src/booking-surface/error-codes.js +94 -0
- package/dist/src/booking-surface/index.js +15 -0
- package/dist/src/booking-surface/profile.js +68 -0
- package/dist/src/booking-surface/runtime-v2.js +1771 -0
- package/dist/src/booking-surface/runtime.js +351 -0
- package/dist/src/booking-surface/server-v2.js +334 -0
- package/dist/src/booking-surface/server.js +302 -0
- package/dist/src/booking-surface/startup.js +159 -0
- package/dist/src/booking-surface/validation-v2.js +319 -0
- package/dist/src/booking-surface/validation.js +809 -0
- package/dist/src/bridge.js +1 -1
- package/dist/src/companions.js +1 -1
- package/dist/src/contracts.js +1 -1
- package/dist/src/dsh-llm.js +1 -1
- package/dist/src/engine.js +1 -1
- package/dist/src/evaluation-cadence.js +234 -0
- package/dist/src/evaluation-contracts.js +906 -0
- package/dist/src/i18n.js +1 -1
- package/dist/src/index.js +50 -19
- package/dist/src/journey.js +1 -1
- package/dist/src/loop.js +1 -1
- package/dist/src/memory-capture.js +1 -1
- package/dist/src/memory-decay.js +1 -1
- package/dist/src/memory-utility.js +1 -1
- package/dist/src/mock-llm.js +1 -1
- package/dist/src/model.js +1 -1
- package/dist/src/realtime-pricing.js +1 -1
- package/dist/src/slot-spec.js +1 -1
- package/dist/src/state-ledger.js +2 -1
- package/dist/src/time-anchor.js +1 -1
- package/dist/src/tool-budget.js +136 -0
- package/dist/src/tool-packet.js +1 -1
- package/dist/src/travel-slots.js +1 -1
- package/dist/src/travel-timeline.js +1 -1
- package/dist/src/unified.js +1 -1
- package/dist/src/wish-pool.js +1 -1
- package/dist/src/z3-shared.js +1 -1
- package/extension/README.md +31 -7
- package/package.json +286 -11
- package/schemas/booking.surface.v1.schema.json +927 -0
- package/schemas/booking.surface.v2.schema.json +61 -0
- package/ts/capabilities/flyai.ts +16 -3
- package/ts/capabilities/session/extension-bridge.ts +37 -14
- package/ts/capabilities/session/extension-channel.ts +6 -3
- package/ts/capabilities/session/extension-distribution.ts +264 -0
- package/ts/capabilities/session/golden-score.ts +139 -0
- package/ts/capabilities/session/health-watch.ts +1 -1
- package/ts/capabilities/session/static-flight-golden.ts +209 -0
- package/ts/capabilities/session/wizard.ts +34 -176
- package/ts/capabilities/session-login.ts +15 -5
- package/ts/capabilities/session-search.ts +40 -3
- package/ts/capabilities/weather.ts +141 -52
- package/ts/package.json +3 -3
- package/ts/src/benchmark-agent-conformance.ts +448 -0
- package/ts/src/benchmark-environment-bridge.ts +348 -0
- package/ts/src/benchmark-headless-child-diagnostics.ts +184 -0
- package/ts/src/benchmark-tool-isolation.ts +166 -0
- package/ts/src/booking-surface/availability-policy-v2.ts +523 -0
- package/ts/src/booking-surface/canonical-schema.js +113 -0
- package/ts/src/booking-surface/contracts-v2.ts +118 -0
- package/ts/src/booking-surface/contracts.ts +380 -0
- package/ts/src/booking-surface/dsh-planner.ts +452 -0
- package/ts/src/booking-surface/dsh-plugin.js +93 -0
- package/ts/src/booking-surface/error-codes.ts +101 -0
- package/ts/src/booking-surface/index.ts +12 -0
- package/ts/src/booking-surface/profile.ts +42 -0
- package/ts/src/booking-surface/runtime-v2.ts +1466 -0
- package/ts/src/booking-surface/runtime.ts +483 -0
- package/ts/src/booking-surface/server-v2.ts +247 -0
- package/ts/src/booking-surface/server.ts +324 -0
- package/ts/src/booking-surface/startup.ts +196 -0
- package/ts/src/booking-surface/validation-v2.ts +205 -0
- package/ts/src/booking-surface/validation.ts +453 -0
- package/ts/src/index.ts +64 -11
- package/ts/src/state-ledger.ts +1 -0
- package/ts/src/tool-budget.ts +165 -0
- package/dist/scripts/wizard-bootstrap.js +0 -32
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SESSION_FIELD_ACCURACY_THRESHOLD,
|
|
3
|
+
type SessionComparableRecord,
|
|
4
|
+
} from './benchmark.ts'
|
|
5
|
+
|
|
6
|
+
export interface GoldenBand {
|
|
7
|
+
query_id: string
|
|
8
|
+
from: string
|
|
9
|
+
to: string
|
|
10
|
+
date: string
|
|
11
|
+
window_dep_local: { earliest: string; latest: string }
|
|
12
|
+
window_arr_local: { earliest: string; latest: string }
|
|
13
|
+
duration_min: { min: number; max: number }
|
|
14
|
+
price_cny: { min: number; max: number }
|
|
15
|
+
transport_hint: string
|
|
16
|
+
known_flights: string[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface GoldenSoftScore {
|
|
20
|
+
pass: boolean
|
|
21
|
+
total: number
|
|
22
|
+
correct: number
|
|
23
|
+
accuracy: number
|
|
24
|
+
missing: string[]
|
|
25
|
+
incorrect: string[]
|
|
26
|
+
golden_source: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseHHmm(value: string): number {
|
|
30
|
+
const [hours, minutes] = value.split(':')
|
|
31
|
+
return parseInt(hours!, 10) * 60 + parseInt(minutes!, 10)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseLocalMinutes(value: string): number | null {
|
|
35
|
+
const match = value.match(/(\d{2}):(\d{2})/)
|
|
36
|
+
if (!match) return null
|
|
37
|
+
return parseInt(match[1]!, 10) * 60 + parseInt(match[2]!, 10)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseLocalDate(value: string): string | null {
|
|
41
|
+
return value.match(/^(\d{4}-\d{2}-\d{2})[T ]/)?.[1] ?? null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function scoreSessionAgainstGoldenBand(
|
|
45
|
+
session: SessionComparableRecord,
|
|
46
|
+
band: GoldenBand,
|
|
47
|
+
goldenSource: string,
|
|
48
|
+
): GoldenSoftScore {
|
|
49
|
+
const missing: string[] = []
|
|
50
|
+
const hardIncorrect: string[] = []
|
|
51
|
+
const segment = session.route_segments[0]
|
|
52
|
+
|
|
53
|
+
if (!session.query_id) missing.push('query_id')
|
|
54
|
+
else if (session.query_id !== band.query_id) hardIncorrect.push('query_id')
|
|
55
|
+
if (!session.currency) missing.push('currency')
|
|
56
|
+
else if (session.currency !== 'CNY') hardIncorrect.push('currency')
|
|
57
|
+
if (!session.source) missing.push('source')
|
|
58
|
+
else if (session.source !== 'ctrip-flight') hardIncorrect.push('source')
|
|
59
|
+
if (!session.verdict) missing.push('verdict')
|
|
60
|
+
else if (session.verdict !== 'hit') hardIncorrect.push('verdict')
|
|
61
|
+
|
|
62
|
+
if (!segment) {
|
|
63
|
+
missing.push(
|
|
64
|
+
'route_segments[0].from',
|
|
65
|
+
'route_segments[0].to',
|
|
66
|
+
'route_segments[0].departure_at',
|
|
67
|
+
'route_segments[0].arrival_at',
|
|
68
|
+
'route_segments[0].transport_number',
|
|
69
|
+
)
|
|
70
|
+
} else {
|
|
71
|
+
if (!segment.from) missing.push('route_segments[0].from')
|
|
72
|
+
else if (segment.from !== band.from) hardIncorrect.push('route_segments[0].from')
|
|
73
|
+
if (!segment.to) missing.push('route_segments[0].to')
|
|
74
|
+
else if (segment.to !== band.to) hardIncorrect.push('route_segments[0].to')
|
|
75
|
+
if (!segment.departure_at) missing.push('route_segments[0].departure_at')
|
|
76
|
+
else if (parseLocalDate(segment.departure_at) !== band.date) {
|
|
77
|
+
hardIncorrect.push('route_segments[0].departure_at.date')
|
|
78
|
+
}
|
|
79
|
+
if (!segment.arrival_at) missing.push('route_segments[0].arrival_at')
|
|
80
|
+
else if (parseLocalDate(segment.arrival_at) !== band.date) {
|
|
81
|
+
hardIncorrect.push('route_segments[0].arrival_at.date')
|
|
82
|
+
}
|
|
83
|
+
if (!segment.transport_number) missing.push('route_segments[0].transport_number')
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const softChecks: Array<{ path: string; ok: boolean }> = []
|
|
87
|
+
if (segment?.departure_at) {
|
|
88
|
+
const minutes = parseLocalMinutes(segment.departure_at)
|
|
89
|
+
softChecks.push({
|
|
90
|
+
path: 'route_segments[0].departure_at',
|
|
91
|
+
ok: minutes !== null
|
|
92
|
+
&& minutes >= parseHHmm(band.window_dep_local.earliest)
|
|
93
|
+
&& minutes <= parseHHmm(band.window_dep_local.latest),
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
if (segment?.arrival_at) {
|
|
97
|
+
const minutes = parseLocalMinutes(segment.arrival_at)
|
|
98
|
+
softChecks.push({
|
|
99
|
+
path: 'route_segments[0].arrival_at',
|
|
100
|
+
ok: minutes !== null
|
|
101
|
+
&& minutes >= parseHHmm(band.window_arr_local.earliest)
|
|
102
|
+
&& minutes <= parseHHmm(band.window_arr_local.latest),
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
if (segment?.transport_number) {
|
|
106
|
+
const actual = segment.transport_number.toUpperCase()
|
|
107
|
+
softChecks.push({
|
|
108
|
+
path: 'route_segments[0].transport_number',
|
|
109
|
+
ok: band.known_flights.some((flight) => actual.startsWith(flight.toUpperCase()))
|
|
110
|
+
|| band.known_flights.some((flight) => actual.includes(flight.toUpperCase().slice(0, 2))),
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
if (Number.isFinite(session.price) && session.price > 0) {
|
|
114
|
+
const margin = band.price_cny.max * 0.15
|
|
115
|
+
softChecks.push({
|
|
116
|
+
path: 'price',
|
|
117
|
+
ok: session.price >= band.price_cny.min - margin
|
|
118
|
+
&& session.price <= band.price_cny.max + margin,
|
|
119
|
+
})
|
|
120
|
+
} else missing.push('price')
|
|
121
|
+
|
|
122
|
+
const softIncorrect = softChecks.filter((check) => !check.ok).map((check) => check.path)
|
|
123
|
+
const incorrect = [...hardIncorrect, ...softIncorrect]
|
|
124
|
+
// 合同字段固定为 13,缺字段不能靠缩小分母抬高准确率。
|
|
125
|
+
const total = 13
|
|
126
|
+
const correct = Math.max(0, total - new Set(missing).size - incorrect.length)
|
|
127
|
+
const accuracy = total > 0 ? correct / total : 0
|
|
128
|
+
return {
|
|
129
|
+
pass: missing.length === 0
|
|
130
|
+
&& hardIncorrect.length === 0
|
|
131
|
+
&& accuracy >= SESSION_FIELD_ACCURACY_THRESHOLD,
|
|
132
|
+
total,
|
|
133
|
+
correct,
|
|
134
|
+
accuracy,
|
|
135
|
+
missing: Array.from(new Set(missing)),
|
|
136
|
+
incorrect,
|
|
137
|
+
golden_source: goldenSource,
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* - 重放上限(防止 watch 反复 ready/抖动导致无限重放;maxRetries 默认 1)。
|
|
21
21
|
*
|
|
22
22
|
* 纪律:
|
|
23
|
-
* - 不引 GUI 依赖;只 spawn `node:http` 探活(已经在扩展桥验证过,§38
|
|
23
|
+
* - 不引 GUI 依赖;只 spawn `node:http` 探活(已经在扩展桥验证过,§38 24 断言覆盖);
|
|
24
24
|
* - 不持锁、不轮询文件、不读 dsh-runtime 共享状态;
|
|
25
25
|
* - 不污染扩展桥 single-instance;probe 走独立短连接。
|
|
26
26
|
*/
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import type { SessionComparableRecord, SessionComparableSegment } from './benchmark.ts'
|
|
5
|
+
|
|
6
|
+
export type GoldenSource = 'manual' | 'flyai' | 'static'
|
|
7
|
+
|
|
8
|
+
export interface StaticFlightRouteLeg {
|
|
9
|
+
from_iata: string
|
|
10
|
+
to_iata: string
|
|
11
|
+
carrier_codes: string[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface StaticFlightRoute {
|
|
15
|
+
query_id: string
|
|
16
|
+
from_city: string
|
|
17
|
+
to_city: string
|
|
18
|
+
route_kind: 'direct' | 'transfer'
|
|
19
|
+
legs: StaticFlightRouteLeg[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface StaticFlightSnapshot {
|
|
23
|
+
schema_version: 'sf-static-routes.v1'
|
|
24
|
+
source: {
|
|
25
|
+
name: string
|
|
26
|
+
url: string
|
|
27
|
+
revision: string
|
|
28
|
+
license: string
|
|
29
|
+
retrieved_at: string
|
|
30
|
+
limitations: string[]
|
|
31
|
+
}
|
|
32
|
+
routes: StaticFlightRoute[]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface StaticGoldenResolution {
|
|
36
|
+
requested_source: 'static'
|
|
37
|
+
effective_source: 'static-openflights+manual-band' | 'manual-golden'
|
|
38
|
+
record: SessionComparableRecord
|
|
39
|
+
fallback_reason?: string
|
|
40
|
+
estimated_fields: string[]
|
|
41
|
+
provenance: {
|
|
42
|
+
route_source: string
|
|
43
|
+
route_revision: string
|
|
44
|
+
route_license: string
|
|
45
|
+
band_source: 'sf-golden-manifest.json'
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function parseGoldenSource(args: string[]): GoldenSource {
|
|
50
|
+
const raw = args.find((arg) => arg.startsWith('--golden='))?.slice('--golden='.length) ?? 'manual'
|
|
51
|
+
if (raw === 'manual' || raw === 'flyai' || raw === 'static') return raw
|
|
52
|
+
throw new Error(`不支持的 golden vendor: ${raw}`)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function loadStaticFlightSnapshot(
|
|
56
|
+
path = join(import.meta.dirname, '..', '..', 'data', 'sf-static-routes.json'),
|
|
57
|
+
): StaticFlightSnapshot {
|
|
58
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8')) as unknown
|
|
59
|
+
if (!isRecord(parsed) || parsed.schema_version !== 'sf-static-routes.v1') {
|
|
60
|
+
throw new Error(`invalid static snapshot schema_version: ${String(isRecord(parsed) ? parsed.schema_version : undefined)}`)
|
|
61
|
+
}
|
|
62
|
+
const source = parsed.source
|
|
63
|
+
if (!isRecord(source)
|
|
64
|
+
|| !['name', 'url', 'revision', 'license', 'retrieved_at'].every((key) => typeof source[key] === 'string')
|
|
65
|
+
|| !Array.isArray(source.limitations)
|
|
66
|
+
|| !source.limitations.every((item) => typeof item === 'string')) {
|
|
67
|
+
throw new Error('invalid static snapshot source')
|
|
68
|
+
}
|
|
69
|
+
if (!Array.isArray(parsed.routes) || !parsed.routes.every(isStaticFlightRoute)) {
|
|
70
|
+
throw new Error('invalid static snapshot routes')
|
|
71
|
+
}
|
|
72
|
+
return parsed as unknown as StaticFlightSnapshot
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
76
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isStaticFlightRoute(value: unknown): boolean {
|
|
80
|
+
if (!isRecord(value)
|
|
81
|
+
|| typeof value.query_id !== 'string'
|
|
82
|
+
|| typeof value.from_city !== 'string'
|
|
83
|
+
|| typeof value.to_city !== 'string'
|
|
84
|
+
|| (value.route_kind !== 'direct' && value.route_kind !== 'transfer')
|
|
85
|
+
|| !Array.isArray(value.legs)
|
|
86
|
+
|| value.legs.length === 0) return false
|
|
87
|
+
return value.legs.every((leg) => isRecord(leg)
|
|
88
|
+
&& typeof leg.from_iata === 'string'
|
|
89
|
+
&& typeof leg.to_iata === 'string'
|
|
90
|
+
&& Array.isArray(leg.carrier_codes)
|
|
91
|
+
&& leg.carrier_codes.every((carrier) => typeof carrier === 'string'))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function provenance(snapshot?: StaticFlightSnapshot): StaticGoldenResolution['provenance'] {
|
|
95
|
+
return {
|
|
96
|
+
route_source: snapshot?.source.url ?? '',
|
|
97
|
+
route_revision: snapshot?.source.revision ?? '',
|
|
98
|
+
route_license: snapshot?.source.license ?? '',
|
|
99
|
+
band_source: 'sf-golden-manifest.json',
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function formatWithOffset(timestampMs: number, offset: string): string {
|
|
104
|
+
const sign = offset.startsWith('-') ? -1 : 1
|
|
105
|
+
const [hours, minutes] = offset.slice(1).split(':').map(Number)
|
|
106
|
+
const offsetMinutes = sign * ((hours ?? 0) * 60 + (minutes ?? 0))
|
|
107
|
+
const local = new Date(timestampMs + offsetMinutes * 60_000).toISOString().slice(0, 19)
|
|
108
|
+
return `${local}${offset}`
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function buildSegments(
|
|
112
|
+
query: { from: string; to: string },
|
|
113
|
+
route: StaticFlightRoute,
|
|
114
|
+
manualSegment: SessionComparableSegment,
|
|
115
|
+
): SessionComparableSegment[] | null {
|
|
116
|
+
if (route.route_kind === 'direct' && route.legs.length === 1) {
|
|
117
|
+
return [{
|
|
118
|
+
from: query.from,
|
|
119
|
+
to: query.to,
|
|
120
|
+
departure_at: manualSegment.departure_at,
|
|
121
|
+
arrival_at: manualSegment.arrival_at,
|
|
122
|
+
transport_number: `${route.legs[0]!.carrier_codes[0]}-STATIC`,
|
|
123
|
+
}]
|
|
124
|
+
}
|
|
125
|
+
if (route.route_kind !== 'transfer' || route.legs.length !== 2) return null
|
|
126
|
+
const departureMs = Date.parse(manualSegment.departure_at)
|
|
127
|
+
const arrivalMs = Date.parse(manualSegment.arrival_at)
|
|
128
|
+
if (!Number.isFinite(departureMs) || !Number.isFinite(arrivalMs) || arrivalMs <= departureMs) return null
|
|
129
|
+
const offset = manualSegment.departure_at.match(/([+-]\d{2}:\d{2})$/)?.[1] ?? '+08:00'
|
|
130
|
+
const totalMinutes = Math.floor((arrivalMs - departureMs) / 60_000)
|
|
131
|
+
if (totalMinutes < 90) return null
|
|
132
|
+
const layoverMinutes = Math.min(60, Math.floor(totalMinutes / 3))
|
|
133
|
+
const travelMinutes = totalMinutes - layoverMinutes
|
|
134
|
+
const firstTravelMinutes = Math.floor(travelMinutes / 2)
|
|
135
|
+
const secondDepartureMs = arrivalMs - (travelMinutes - firstTravelMinutes) * 60_000
|
|
136
|
+
const middle = route.legs[0]!.to_iata
|
|
137
|
+
return [
|
|
138
|
+
{
|
|
139
|
+
from: query.from,
|
|
140
|
+
to: middle,
|
|
141
|
+
departure_at: manualSegment.departure_at,
|
|
142
|
+
arrival_at: formatWithOffset(departureMs + firstTravelMinutes * 60_000, offset),
|
|
143
|
+
transport_number: `${route.legs[0]!.carrier_codes[0]}-STATIC`,
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
from: middle,
|
|
147
|
+
to: query.to,
|
|
148
|
+
departure_at: formatWithOffset(secondDepartureMs, offset),
|
|
149
|
+
arrival_at: manualSegment.arrival_at,
|
|
150
|
+
transport_number: `${route.legs[1]!.carrier_codes[0]}-STATIC`,
|
|
151
|
+
},
|
|
152
|
+
]
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function resolveStaticGolden(input: {
|
|
156
|
+
query: { id: string; from: string; to: string; date: string }
|
|
157
|
+
manualRecord: SessionComparableRecord
|
|
158
|
+
snapshot?: StaticFlightSnapshot
|
|
159
|
+
snapshotError?: string
|
|
160
|
+
warn?: (message: string) => void
|
|
161
|
+
}): StaticGoldenResolution {
|
|
162
|
+
const route = input.snapshot?.routes.find((candidate) => candidate.query_id === input.query.id)
|
|
163
|
+
let fallbackReason: string | undefined
|
|
164
|
+
if (input.snapshotError) {
|
|
165
|
+
fallbackReason = `static snapshot unavailable: ${input.snapshotError}`
|
|
166
|
+
} else if (!route) {
|
|
167
|
+
fallbackReason = `static route missing for ${input.query.id}`
|
|
168
|
+
} else if (route.from_city !== input.query.from || route.to_city !== input.query.to) {
|
|
169
|
+
fallbackReason = `static route mismatch for ${input.query.id}`
|
|
170
|
+
} else if (route.legs.some((leg) => leg.carrier_codes.length === 0)) {
|
|
171
|
+
fallbackReason = `static carrier coverage missing for ${input.query.id}`
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const manualSegment = input.manualRecord.route_segments[0]
|
|
175
|
+
const segments = !fallbackReason && route && manualSegment
|
|
176
|
+
? buildSegments(input.query, route, manualSegment)
|
|
177
|
+
: null
|
|
178
|
+
if (!fallbackReason && !segments) fallbackReason = `static route cannot form comparable record for ${input.query.id}`
|
|
179
|
+
|
|
180
|
+
if (fallbackReason) {
|
|
181
|
+
input.warn?.(`[sf-live-benchmark] static vendor failed for ${input.query.id}: ${fallbackReason}; fallback=manual-golden`)
|
|
182
|
+
return {
|
|
183
|
+
requested_source: 'static',
|
|
184
|
+
effective_source: 'manual-golden',
|
|
185
|
+
record: input.manualRecord,
|
|
186
|
+
fallback_reason: fallbackReason,
|
|
187
|
+
estimated_fields: [],
|
|
188
|
+
provenance: provenance(input.snapshot),
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
requested_source: 'static',
|
|
194
|
+
effective_source: 'static-openflights+manual-band',
|
|
195
|
+
record: {
|
|
196
|
+
...input.manualRecord,
|
|
197
|
+
route_segments: segments!,
|
|
198
|
+
journey_type: route!.route_kind,
|
|
199
|
+
source: 'static-openflights+manual-band',
|
|
200
|
+
},
|
|
201
|
+
estimated_fields: [
|
|
202
|
+
'route_segments[].departure_at',
|
|
203
|
+
'route_segments[].arrival_at',
|
|
204
|
+
'route_segments[].transport_number',
|
|
205
|
+
'price',
|
|
206
|
+
],
|
|
207
|
+
provenance: provenance(input.snapshot),
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -1,39 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 会话扩展 onboarding wizard(
|
|
3
|
-
* RFC user-session-data-rfc.md §3.3)。
|
|
2
|
+
* 会话扩展 onboarding wizard(RFC user-session-data-rfc.md §3.3,ADR-21 上架后重设)。
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* 单一职责(2026-09-02 重设):**只做 Node 端两件事**——
|
|
5
|
+
* ① 扩展文件从源落位到 `~/.gotry/extension`(幂等)
|
|
6
|
+
* ② source/dest 静态预检 + manifest.key 校验(给 health-watch 前的快照)
|
|
8
7
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* - 失败步不挡退出(exit 0;失败信息人话化,不抛栈)。
|
|
8
|
+
* 浏览器商店安装是浏览器的事,dsh 渲染 verdict.installUrl 是 dsh UI 的事;
|
|
9
|
+
* gotry **不再**弹 GUI 面板(pbcopy/osascript/zenity/msg)、不再 open 浏览器、
|
|
10
|
+
* 不再写一墙文字 CLI 指引。任何"装到能用"的 UX 都由浏览器商店 + dsh UI 分担。
|
|
13
11
|
*
|
|
14
|
-
*
|
|
15
|
-
* -
|
|
16
|
-
*
|
|
17
|
-
* - Linux:`xdg-open chrome://extensions`(浏览器由系统默认) + `xclip -selection clipboard`;
|
|
18
|
-
* 有 `zenity --info --text=...` 面板(无 zenity 降 terminal)。
|
|
19
|
-
* - Windows:`start chrome chrome://extensions` + `clip`;无 panel,降 terminal。
|
|
20
|
-
* - Headless(SSH / CI):terminal 面板 + 跳过所有 GUI 调用。
|
|
12
|
+
* 历史形态回顾:
|
|
13
|
+
* - 2026-08-30 初版:5 步编排 + 剪贴板 + GUI 面板 + 健康探活(全栈闭环初衷好,但越界管了浏览器)
|
|
14
|
+
* - 2026-09-02 上架后:缩为 2 步(ensure + watch precheck),纯 Node 职责
|
|
21
15
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* 输出形态:`runOnboardingWizard` 是纯编排,**不直接抓 Chrome 状态**——
|
|
26
|
-
* 健康检查由 health-watch.ts 接管(单独测试覆盖)。Wizard 只负责「把人引到 Chrome 前」,
|
|
27
|
-
* watch 负责「等 Chrome 真的就绪」。
|
|
16
|
+
* 接口契约:
|
|
17
|
+
* - `runOnboardingWizard({ dryRun, extensionDir?, sourceDir? })` 返回 2 步 OnboardingResult
|
|
18
|
+
* - `defaultExtensionDir()` / `describeExtensionDir()` 留作公开工具
|
|
28
19
|
*/
|
|
29
20
|
|
|
30
|
-
import { spawn } from 'node:child_process'
|
|
31
21
|
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from 'node:fs'
|
|
32
|
-
import { homedir
|
|
22
|
+
import { homedir } from 'node:os'
|
|
33
23
|
import { join } from 'node:path'
|
|
34
24
|
|
|
35
25
|
export interface OnboardingStep {
|
|
36
|
-
step: 'ensure-extension-files' | '
|
|
26
|
+
step: 'ensure-extension-files' | 'watch-extension-ready'
|
|
37
27
|
status: 'ok' | 'skip' | 'fail'
|
|
38
28
|
summary: string
|
|
39
29
|
}
|
|
@@ -45,16 +35,11 @@ export interface OnboardingOptions {
|
|
|
45
35
|
extensionDir?: string
|
|
46
36
|
/** 源扩展目录(默认仓库 ./extension/,随 npm 装入 dist/extension) */
|
|
47
37
|
sourceDir?: string
|
|
48
|
-
/** 覆盖平台探测(headless 测试) */
|
|
49
|
-
forcePlatform?: 'darwin' | 'linux' | 'win32' | 'headless'
|
|
50
|
-
/** 注入 spawn(测试可拦截命令执行);签名同 child_process.spawn,默认 spawn */
|
|
51
|
-
spawn?: typeof spawn
|
|
52
38
|
}
|
|
53
39
|
|
|
54
40
|
export interface OnboardingResult {
|
|
55
41
|
ok: boolean
|
|
56
42
|
steps: OnboardingStep[]
|
|
57
|
-
platform: 'darwin' | 'linux' | 'win32' | 'headless'
|
|
58
43
|
extensionDir: string
|
|
59
44
|
}
|
|
60
45
|
|
|
@@ -75,17 +60,6 @@ export function defaultExtensionDir(): string {
|
|
|
75
60
|
return join(homedir(), '.gotry', 'extension')
|
|
76
61
|
}
|
|
77
62
|
|
|
78
|
-
function detectPlatform(force?: OnboardingOptions['forcePlatform']): 'darwin' | 'linux' | 'win32' | 'headless' {
|
|
79
|
-
if (force) return force
|
|
80
|
-
if (process.env.GOTRY_ONBOARDING_HEADLESS === '1') return 'headless'
|
|
81
|
-
const p = platform()
|
|
82
|
-
if (p === 'darwin' || p === 'linux' || p === 'win32') {
|
|
83
|
-
if (p !== 'darwin' && !process.env.DISPLAY) return 'headless'
|
|
84
|
-
return p
|
|
85
|
-
}
|
|
86
|
-
return 'headless'
|
|
87
|
-
}
|
|
88
|
-
|
|
89
63
|
/** 落位扩展文件(幂等);缺源/源不全会 fail-soft,提示用户重跑 `gotry setup` */
|
|
90
64
|
function ensureExtensionFiles(source: string, target: string, dryRun: boolean): OnboardingStep {
|
|
91
65
|
if (dryRun) return { step: 'ensure-extension-files', status: 'skip', summary: '[dry-run] skip 扩展落位' }
|
|
@@ -106,162 +80,46 @@ function ensureExtensionFiles(source: string, target: string, dryRun: boolean):
|
|
|
106
80
|
}
|
|
107
81
|
}
|
|
108
82
|
|
|
109
|
-
/**
|
|
110
|
-
function
|
|
111
|
-
if (dryRun) return { step: '
|
|
112
|
-
if (
|
|
113
|
-
|
|
114
|
-
if (plat === 'darwin') return { bin: 'open', args: ['-a', 'Google Chrome', 'chrome://extensions'] }
|
|
115
|
-
if (plat === 'linux') return { bin: 'xdg-open', args: ['chrome://extensions'] }
|
|
116
|
-
return { bin: 'cmd', args: ['/c', 'start', 'chrome', 'chrome://extensions'] }
|
|
117
|
-
})()
|
|
118
|
-
try {
|
|
119
|
-
const child = sp(cmd.bin, cmd.args, { detached: true, stdio: 'ignore' })
|
|
120
|
-
child.unref?.()
|
|
121
|
-
return { step: 'open-chrome-extensions', status: 'ok', summary: `已打开 chrome://extensions(${plat})` }
|
|
122
|
-
} catch (e) {
|
|
123
|
-
return { step: 'open-chrome-extensions', status: 'fail', summary: `自动打开失败: ${e instanceof Error ? e.message.slice(0, 80) : String(e)};请手开 chrome://extensions` }
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/** 复制扩展目录路径到剪贴板;失败降级 stdout */
|
|
128
|
-
async function clipboardExtensionPath(sp: typeof spawn, plat: 'darwin' | 'linux' | 'win32' | 'headless', path: string, dryRun: boolean): Promise<OnboardingStep> {
|
|
129
|
-
if (dryRun) return { step: 'clipboard-extension-path', status: 'skip', summary: '[dry-run] skip 剪贴板复制' }
|
|
130
|
-
if (plat === 'headless') return { step: 'clipboard-extension-path', status: 'skip', summary: `headless 无剪贴板;扩展目录路径:${path}` }
|
|
131
|
-
const cmd = (() => {
|
|
132
|
-
if (plat === 'darwin') return { bin: 'pbcopy', args: [] as string[] }
|
|
133
|
-
if (plat === 'linux') return { bin: 'xclip', args: ['-selection', 'clipboard'] }
|
|
134
|
-
return { bin: 'clip', args: [] as string[] }
|
|
135
|
-
})()
|
|
136
|
-
// 关键:pbcopy/xclip/clip 都在 stdin EOF 之后才提交内容,stdin 必须 pipe,
|
|
137
|
-
// 且必须 await exit(否则 end() 还在 buffer 里就被当作「完成」)。
|
|
138
|
-
// 之前用 stdio: 'inherit' → stdin 是 null → write 没生效 → 用户剪贴板没被覆盖。
|
|
139
|
-
return new Promise<OnboardingStep>((resolveStep) => {
|
|
140
|
-
let child: ReturnType<typeof spawn>
|
|
141
|
-
try {
|
|
142
|
-
child = sp(cmd.bin, cmd.args, { stdio: ['pipe', 'ignore', 'ignore'] }) as unknown as ReturnType<typeof spawn>
|
|
143
|
-
} catch {
|
|
144
|
-
resolveStep({ step: 'clipboard-extension-path', status: 'skip', summary: `剪贴板工具不可用(${cmd.bin});扩展目录路径手动复制: ${path}` })
|
|
145
|
-
return
|
|
146
|
-
}
|
|
147
|
-
let settled = false
|
|
148
|
-
const settle = (s: OnboardingStep): void => {
|
|
149
|
-
if (settled) return
|
|
150
|
-
settled = true
|
|
151
|
-
resolveStep(s)
|
|
152
|
-
}
|
|
153
|
-
child.on('error', () => settle({ step: 'clipboard-extension-path', status: 'skip', summary: `剪贴板工具启动失败(${cmd.bin});扩展目录路径手动复制: ${path}` }))
|
|
154
|
-
child.on('exit', (code) => {
|
|
155
|
-
if (code === 0) settle({ step: 'clipboard-extension-path', status: 'ok', summary: `已复制扩展目录到剪贴板: ${path}` })
|
|
156
|
-
else settle({ step: 'clipboard-extension-path', status: 'fail', summary: `剪贴板工具退出码 ${code}(${cmd.bin});扩展目录路径手动复制: ${path}` })
|
|
157
|
-
})
|
|
158
|
-
try {
|
|
159
|
-
child.stdin?.write(path)
|
|
160
|
-
child.stdin?.end()
|
|
161
|
-
} catch {
|
|
162
|
-
settle({ step: 'clipboard-extension-path', status: 'skip', summary: `剪贴板工具 stdin 不可写(${cmd.bin});扩展目录路径手动复制: ${path}` })
|
|
163
|
-
}
|
|
164
|
-
})
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/** 终端面板 stdout(无 GUI 时唯一形态);headless / GUI 不可用时使用 */
|
|
168
|
-
function panelGuideTerminal(plat: 'darwin' | 'linux' | 'win32' | 'headless', extensionDir: string, watchMs: number): OnboardingStep {
|
|
169
|
-
const lines = [
|
|
170
|
-
'┌─ GoTry 扩展安装 ─────────────────────────────────┐',
|
|
171
|
-
'│ 共 3 步,约 30 秒,装完零弹窗 │',
|
|
172
|
-
'│ │',
|
|
173
|
-
'│ ① 在 Chrome 右上角开启「开发者模式」 │',
|
|
174
|
-
`│ ② 点「加载已解压的扩展程序」选这个目录(已复制): │`,
|
|
175
|
-
`│ ${extensionDir}`,
|
|
176
|
-
'│ ③ 弹窗「添加扩展」点「添加」 │',
|
|
177
|
-
`│ │`,
|
|
178
|
-
`│ 后台探活中(最长 ${Math.round(watchMs / 1000)}s)… │`,
|
|
179
|
-
'└───────────────────────────────────────────────────┘',
|
|
180
|
-
]
|
|
181
|
-
// headless = 纯文本面板;darwin/linux/win32 在 GUI 面板不可用时也走这路
|
|
182
|
-
if (plat === 'headless') {
|
|
183
|
-
process.stdout.write(lines.join('\n') + '\n')
|
|
184
|
-
return { step: 'panel-guide', status: 'skip', summary: 'headless 终端面板已打印' }
|
|
83
|
+
/** 静态预检:落位目录可见 + manifest.key 存在(扩展 ID 稳定)+ 源未新于 dest */
|
|
84
|
+
async function watchExtensionReady(dryRun: boolean, sourceDir: string, extensionDir: string): Promise<OnboardingStep> {
|
|
85
|
+
if (dryRun) return { step: 'watch-extension-ready', status: 'skip', summary: '[dry-run] skip health-watch precheck' }
|
|
86
|
+
if (!existsSync(extensionDir) || !existsSync(join(extensionDir, 'manifest.json'))) {
|
|
87
|
+
return { step: 'watch-extension-ready', status: 'fail', summary: `落位缺失 manifest.json:${extensionDir}` }
|
|
185
88
|
}
|
|
186
|
-
process.stdout.write(lines.join('\n') + '\n')
|
|
187
|
-
return { step: 'panel-guide', status: 'skip', summary: '终端面板已打印(GUI 面板降级)' }
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/** GUI 面板(Cocoa / zenity / msg);失败降 terminal */
|
|
191
|
-
function panelGuideGui(sp: typeof spawn, plat: 'darwin' | 'linux' | 'win32' | 'headless', extensionDir: string, dryRun: boolean): OnboardingStep {
|
|
192
|
-
if (dryRun) return { step: 'panel-guide', status: 'skip', summary: '[dry-run] skip GUI 面板' }
|
|
193
|
-
if (plat === 'headless') return panelGuideTerminal(plat, extensionDir, 120_000)
|
|
194
|
-
const message = `在 Chrome 右上角开启「开发者模式」 → 点「加载已解压的扩展程序」 → 选择(已复制):${extensionDir} → 弹窗点「添加扩展」`
|
|
195
89
|
try {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
return { step: '
|
|
90
|
+
const srcStat = statSync(sourceDir)
|
|
91
|
+
const dstStat = statSync(extensionDir)
|
|
92
|
+
if (srcStat.mtimeMs > dstStat.mtimeMs) {
|
|
93
|
+
return { step: 'watch-extension-ready', status: 'fail', summary: `扩展文件可能过期(源比落位新);重跑 npx gotry setup` }
|
|
200
94
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
child.on('error', () => {
|
|
205
|
-
process.stdout.write(`(zenity 不可用,走终端面板)扩展目录: ${extensionDir}\n`)
|
|
206
|
-
})
|
|
207
|
-
return { step: 'panel-guide', status: 'ok', summary: '已弹 zenity 引导面板' }
|
|
95
|
+
const manifest = JSON.parse(await (await import('node:fs/promises')).readFile(join(extensionDir, 'manifest.json'), 'utf8'))
|
|
96
|
+
if (typeof manifest.key !== 'string' || manifest.key.length < 100) {
|
|
97
|
+
return { step: 'watch-extension-ready', status: 'fail', summary: 'manifest.key 缺失或过短,扩展 ID 不能稳定' }
|
|
208
98
|
}
|
|
209
|
-
// Windows:无原生 msg 面板,降终端
|
|
210
|
-
return panelGuideTerminal(plat, extensionDir, 120_000)
|
|
211
99
|
} catch (e) {
|
|
212
|
-
return
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/** 探活:留给 health-watch.ts,这里只打日志;集成在 main runner 时再调 */
|
|
217
|
-
async function watchExtensionReady(dryRun: boolean, sourceDir: string, extensionDir: string): Promise<OnboardingStep> {
|
|
218
|
-
if (dryRun) return { step: 'watch-extension-ready', status: 'skip', summary: '[dry-run] skip health-watch' }
|
|
219
|
-
// 真实集成由 cli/wizard runner 接管(避免 wizard 模块 import health-watch 形成环);
|
|
220
|
-
// 这里只校验源文件齐全 + 落位目录可见
|
|
221
|
-
if (!existsSync(extensionDir) || !existsSync(join(extensionDir, 'manifest.json'))) {
|
|
222
|
-
return { step: 'watch-extension-ready', status: 'fail', summary: `落位缺失 manifest.json:${extensionDir}` }
|
|
223
|
-
}
|
|
224
|
-
const srcStat = statSync(sourceDir)
|
|
225
|
-
const dstStat = statSync(extensionDir)
|
|
226
|
-
if (srcStat.mtimeMs > dstStat.mtimeMs) {
|
|
227
|
-
return { step: 'watch-extension-ready', status: 'fail', summary: `扩展文件可能过期(源比落位新);重跑 npx gotry setup` }
|
|
228
|
-
}
|
|
229
|
-
// 读 manifest 校验 key 存在(=扩展 ID 稳定)
|
|
230
|
-
const manifest = JSON.parse(await (await import('node:fs/promises')).readFile(join(extensionDir, 'manifest.json'), 'utf8'))
|
|
231
|
-
if (typeof manifest.key !== 'string' || manifest.key.length < 100) {
|
|
232
|
-
return { step: 'watch-extension-ready', status: 'fail', summary: 'manifest.key 缺失或过短,扩展 ID 不能稳定' }
|
|
100
|
+
return { step: 'watch-extension-ready', status: 'fail', summary: `静态预检失败: ${e instanceof Error ? e.message.slice(0, 120) : String(e)}` }
|
|
233
101
|
}
|
|
234
102
|
return { step: 'watch-extension-ready', status: 'ok', summary: '扩展文件就绪(manifest.key 校验通过,后台轮询就绪态由 health-watch 接管)' }
|
|
235
103
|
}
|
|
236
104
|
|
|
237
105
|
/**
|
|
238
|
-
*
|
|
239
|
-
*
|
|
106
|
+
* 主入口(2 步):①扩展落位 ②watch precheck 静态预检。
|
|
107
|
+
* 真 health-watch 由 health-watch.ts 单独负责,不并入 wizard。
|
|
240
108
|
*/
|
|
241
109
|
export async function runOnboardingWizard(opts: OnboardingOptions = {}): Promise<OnboardingResult> {
|
|
242
|
-
const sp = opts.spawn ?? spawn
|
|
243
110
|
const dryRun = opts.dryRun ?? false
|
|
244
|
-
const plat = detectPlatform(opts.forcePlatform)
|
|
245
111
|
const extensionDir = opts.extensionDir ?? defaultExtensionDir()
|
|
246
112
|
const sourceDir = opts.sourceDir ?? resolveExtensionSourceDir()
|
|
247
113
|
|
|
248
114
|
const steps: OnboardingStep[] = []
|
|
249
115
|
steps.push(ensureExtensionFiles(sourceDir, extensionDir, dryRun))
|
|
250
|
-
steps.push(openChromeExtensions(sp, plat, dryRun))
|
|
251
|
-
if (plat !== 'headless') {
|
|
252
|
-
steps.push(await clipboardExtensionPath(sp, plat, extensionDir, dryRun))
|
|
253
|
-
steps.push(panelGuideGui(sp, plat, extensionDir, dryRun))
|
|
254
|
-
} else {
|
|
255
|
-
steps.push({ step: 'clipboard-extension-path', status: 'skip', summary: 'headless 跳过剪贴板' })
|
|
256
|
-
steps.push(panelGuideTerminal(plat, extensionDir, 120_000))
|
|
257
|
-
}
|
|
258
116
|
steps.push(await watchExtensionReady(dryRun, sourceDir, extensionDir))
|
|
259
117
|
|
|
260
118
|
const ok = steps.every((s) => s.status !== 'fail')
|
|
261
|
-
return { ok, steps,
|
|
119
|
+
return { ok, steps, extensionDir }
|
|
262
120
|
}
|
|
263
121
|
|
|
264
|
-
/** stdout 人话化打印(
|
|
122
|
+
/** stdout 人话化打印(给 bootstrap 子进程单行 JSON 输出之外的兜底形态) */
|
|
265
123
|
export function printWizardResult(result: OnboardingResult, destination: NodeJS.WritableStream = process.stdout): void {
|
|
266
124
|
const tag = (s: OnboardingStep): string => {
|
|
267
125
|
if (s.status === 'ok') return '✅'
|
|
@@ -271,10 +129,10 @@ export function printWizardResult(result: OnboardingResult, destination: NodeJS.
|
|
|
271
129
|
for (const s of result.steps) {
|
|
272
130
|
destination.write(`${tag(s)} ${s.step}: ${s.summary}\n`)
|
|
273
131
|
}
|
|
274
|
-
destination.write(`
|
|
132
|
+
destination.write(`extensionDir=${result.extensionDir}; overall=${result.ok ? 'ok' : 'has-failures'}\n`)
|
|
275
133
|
}
|
|
276
134
|
|
|
277
|
-
/**
|
|
135
|
+
/** 公开诊断探针(bootstrap 报告用) */
|
|
278
136
|
export function describeExtensionDir(extensionDir: string): { exists: boolean; fileCount: number; hasManifest: boolean } {
|
|
279
137
|
if (!existsSync(extensionDir)) return { exists: false, fileCount: 0, hasManifest: false }
|
|
280
138
|
const files = readdirSync(extensionDir)
|