@danceiny/gotry 0.0.1-rc.16 → 0.0.1-rc.17

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.
Files changed (229) hide show
  1. package/README.md +47 -13
  2. package/README.zh-CN.md +20 -10
  3. package/bin/gotry-booking-copilot.js +53 -0
  4. package/bin/gotry-bootstrap.js +111 -51
  5. package/bin/gotry-inner.js +442 -60
  6. package/bin/gotry-runtime-resolution.d.ts +27 -0
  7. package/bin/gotry-runtime-resolution.js +50 -0
  8. package/bin/gotry.js +1 -1
  9. package/cordis.gotry-patch.yml +5 -1
  10. package/dist/capabilities/agent-reach-deep.js +1 -1
  11. package/dist/capabilities/agent-reach.js +1 -1
  12. package/dist/capabilities/anything.js +1 -1
  13. package/dist/capabilities/artifacts.js +1 -1
  14. package/dist/capabilities/effect.js +1 -1
  15. package/dist/capabilities/fact-log.js +1 -1
  16. package/dist/capabilities/flyai.js +20 -6
  17. package/dist/capabilities/hbcli.js +1 -1
  18. package/dist/capabilities/incident-log.js +1 -1
  19. package/dist/capabilities/model-override.js +18 -0
  20. package/dist/capabilities/opensky.js +1 -1
  21. package/dist/capabilities/resilience.js +1 -1
  22. package/dist/capabilities/session/action-cache.js +1 -1
  23. package/dist/capabilities/session/adapters/ctrip-flight.js +1 -1
  24. package/dist/capabilities/session/adapters/meituan-local.js +1 -1
  25. package/dist/capabilities/session/benchmark.js +1 -1
  26. package/dist/capabilities/session/extension-bridge.js +19 -6
  27. package/dist/capabilities/session/extension-channel.js +3 -3
  28. package/dist/capabilities/session/extension-distribution.js +234 -0
  29. package/dist/capabilities/session/extract.js +1 -1
  30. package/dist/capabilities/session/golden-score.js +92 -0
  31. package/dist/capabilities/session/health-watch.js +1 -1
  32. package/dist/capabilities/session/read-guard.js +1 -1
  33. package/dist/capabilities/session/static-flight-golden.js +137 -0
  34. package/dist/capabilities/session/transport.js +1 -1
  35. package/dist/capabilities/session/wizard.js +21 -251
  36. package/dist/capabilities/session-consent.js +1 -1
  37. package/dist/capabilities/session-login.js +11 -5
  38. package/dist/capabilities/session-search.js +46 -4
  39. package/dist/capabilities/weather.js +168 -46
  40. package/dist/data/session-golden-20.json +25 -0
  41. package/dist/data/sf-golden-manifest.json +102 -0
  42. package/dist/data/sf-static-routes.json +91 -0
  43. package/dist/scripts/action-cache-tests.js +1 -1
  44. package/dist/scripts/agent-planning-budget-e2e.js +227 -0
  45. package/dist/scripts/agent-planning-budget-tests.js +173 -0
  46. package/dist/scripts/agent-reach-deep-tests.js +1 -1
  47. package/dist/scripts/agent-reach-tests.js +1 -1
  48. package/dist/scripts/agent-reach-wrapper-tests.js +1 -1
  49. package/dist/scripts/anything-tests.js +1 -1
  50. package/dist/scripts/async-collect.js +1 -1
  51. package/dist/scripts/benchmark-environment-bridge-e2e.js +981 -0
  52. package/dist/scripts/benchmark-environment-bridge-tests.js +2544 -0
  53. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +335 -0
  54. package/dist/scripts/booking-copilot-availability-policy-v2-tests.js +1355 -0
  55. package/dist/scripts/booking-copilot-bin-proof-tests.js +89 -0
  56. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +113 -0
  57. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +356 -0
  58. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +557 -0
  59. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +140 -0
  60. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +202 -0
  61. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +102 -0
  62. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +374 -0
  63. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +447 -0
  64. package/dist/scripts/booking-copilot-runtime-proof-tests.js +205 -0
  65. package/dist/scripts/booking-copilot-server-proof-tests.js +317 -0
  66. package/dist/scripts/booking-copilot-startup-proof-tests.js +283 -0
  67. package/dist/scripts/booking-copilot-v2-runtime-proof-tests.js +3935 -0
  68. package/dist/scripts/booking-saga-tests.js +1 -1
  69. package/dist/scripts/booking-surface-contract-proof-tests.js +393 -0
  70. package/dist/scripts/booking-surface-v2-contract-proof-tests.js +1174 -0
  71. package/dist/scripts/bootstrap-tests.js +30 -10
  72. package/dist/scripts/build-changelog.js +1 -1
  73. package/dist/scripts/changelog-tests.js +1 -1
  74. package/dist/scripts/companion-tests.js +1 -1
  75. package/dist/scripts/diff-test.js +1 -1
  76. package/dist/scripts/dsh-runtime-closure-tests.js +225 -0
  77. package/dist/scripts/dsh-runtime-closure.js +150 -0
  78. package/dist/scripts/effect-tests.js +1 -1
  79. package/dist/scripts/engine-run.js +1 -1
  80. package/dist/scripts/engine-tests.js +1 -1
  81. package/dist/scripts/evaluation-cadence-tests.js +347 -0
  82. package/dist/scripts/evaluation-contract-tests.js +574 -0
  83. package/dist/scripts/extension-distribution-cli.js +35 -0
  84. package/dist/scripts/extension-distribution-tests.js +341 -0
  85. package/dist/scripts/extension-tests.js +128 -23
  86. package/dist/scripts/fact-gate-tests.js +1 -1
  87. package/dist/scripts/flyai-tests.js +1 -1
  88. package/dist/scripts/hbcli-e2e-tests.js +1 -1
  89. package/dist/scripts/hbcli-tests.js +1 -1
  90. package/dist/scripts/health-watch-cli.js +1 -1
  91. package/dist/scripts/i18n-tests.js +1 -1
  92. package/dist/scripts/incident-tests.js +1 -1
  93. package/dist/scripts/journey-tests.js +1 -1
  94. package/dist/scripts/ledger-tests.js +1 -1
  95. package/dist/scripts/ledger-workflow-crash.js +1 -1
  96. package/dist/scripts/memory-capture-tests.js +1 -1
  97. package/dist/scripts/memory-decay-tests.js +1 -1
  98. package/dist/scripts/memory-metrics.js +1 -1
  99. package/dist/scripts/memory-value-report.js +1 -1
  100. package/dist/scripts/model-override-e2e.js +176 -0
  101. package/dist/scripts/nightly-evidence-tests.js +1 -1
  102. package/dist/scripts/nightly-evidence.js +1 -1
  103. package/dist/scripts/nudge-digest.js +1 -1
  104. package/dist/scripts/onboarding-tests.js +21 -53
  105. package/dist/scripts/opensky-check.js +1 -1
  106. package/dist/scripts/opensky-tests.js +1 -1
  107. package/dist/scripts/pnpm-dsh-closure-proof.js +20 -0
  108. package/dist/scripts/price-drift-tests.js +1 -1
  109. package/dist/scripts/price-drift-watch.js +1 -1
  110. package/dist/scripts/probe-poi-tests.js +1 -1
  111. package/dist/scripts/product-metrics.js +1 -1
  112. package/dist/scripts/publish-preverify.js +40 -4
  113. package/dist/scripts/realtime-pricing-tests.js +1 -1
  114. package/dist/scripts/replay-async.js +1 -1
  115. package/dist/scripts/replay-real.js +1 -1
  116. package/dist/scripts/replay.js +1 -1
  117. package/dist/scripts/session-attach-diagnose.js +1 -1
  118. package/dist/scripts/session-attach-poc.js +1 -1
  119. package/dist/scripts/session-benchmark.js +1 -1
  120. package/dist/scripts/session-extract-tests.js +1 -1
  121. package/dist/scripts/session-login.js +1 -1
  122. package/dist/scripts/session-tests.js +70 -20
  123. package/dist/scripts/sf-live-benchmark.js +338 -0
  124. package/dist/scripts/sf-live-cli-tests.js +21 -0
  125. package/dist/scripts/sf-soft-score-tests.js +108 -0
  126. package/dist/scripts/sf-summary.js +93 -0
  127. package/dist/scripts/skeleton-check.js +1 -1
  128. package/dist/scripts/skeleton-integration-test.js +1 -1
  129. package/dist/scripts/skills-contract-tests.js +1 -1
  130. package/dist/scripts/smoke-session-gate-tests.js +29 -0
  131. package/dist/scripts/smoke.js +79 -33
  132. package/dist/scripts/state-cli-tests.js +1 -1
  133. package/dist/scripts/state-cli.js +1 -1
  134. package/dist/scripts/static-golden-tests.js +299 -0
  135. package/dist/scripts/time-eval-tests.js +1 -1
  136. package/dist/scripts/travel-timeline-tests.js +1 -1
  137. package/dist/scripts/unified-tests.js +1 -1
  138. package/dist/scripts/weather-tests.js +694 -44
  139. package/dist/scripts/z3-race-tests.js +1 -1
  140. package/dist/src/artifact-gate.js +1 -1
  141. package/dist/src/benchmark-agent-conformance.js +370 -0
  142. package/dist/src/benchmark-environment-bridge.js +384 -0
  143. package/dist/src/benchmark-headless-child-diagnostics.js +173 -0
  144. package/dist/src/benchmark-tool-isolation.js +124 -0
  145. package/dist/src/bookable-facts.js +1 -1
  146. package/dist/src/booking-saga.js +1 -1
  147. package/dist/src/booking-surface/availability-policy-v2.js +830 -0
  148. package/dist/src/booking-surface/canonical-schema.js +113 -0
  149. package/dist/src/booking-surface/contracts-v2.js +89 -0
  150. package/dist/src/booking-surface/contracts.js +46 -0
  151. package/dist/src/booking-surface/dsh-planner.js +453 -0
  152. package/dist/src/booking-surface/dsh-plugin.js +93 -0
  153. package/dist/src/booking-surface/error-codes.js +94 -0
  154. package/dist/src/booking-surface/index.js +15 -0
  155. package/dist/src/booking-surface/profile.js +68 -0
  156. package/dist/src/booking-surface/runtime-v2.js +1771 -0
  157. package/dist/src/booking-surface/runtime.js +351 -0
  158. package/dist/src/booking-surface/server-v2.js +334 -0
  159. package/dist/src/booking-surface/server.js +302 -0
  160. package/dist/src/booking-surface/startup.js +159 -0
  161. package/dist/src/booking-surface/validation-v2.js +319 -0
  162. package/dist/src/booking-surface/validation.js +809 -0
  163. package/dist/src/bridge.js +1 -1
  164. package/dist/src/companions.js +1 -1
  165. package/dist/src/contracts.js +1 -1
  166. package/dist/src/dsh-llm.js +1 -1
  167. package/dist/src/engine.js +1 -1
  168. package/dist/src/evaluation-cadence.js +234 -0
  169. package/dist/src/evaluation-contracts.js +906 -0
  170. package/dist/src/i18n.js +1 -1
  171. package/dist/src/index.js +50 -19
  172. package/dist/src/journey.js +1 -1
  173. package/dist/src/loop.js +1 -1
  174. package/dist/src/memory-capture.js +1 -1
  175. package/dist/src/memory-decay.js +1 -1
  176. package/dist/src/memory-utility.js +1 -1
  177. package/dist/src/mock-llm.js +1 -1
  178. package/dist/src/model.js +1 -1
  179. package/dist/src/realtime-pricing.js +1 -1
  180. package/dist/src/slot-spec.js +1 -1
  181. package/dist/src/state-ledger.js +2 -1
  182. package/dist/src/time-anchor.js +1 -1
  183. package/dist/src/tool-budget.js +136 -0
  184. package/dist/src/tool-packet.js +1 -1
  185. package/dist/src/travel-slots.js +1 -1
  186. package/dist/src/travel-timeline.js +1 -1
  187. package/dist/src/unified.js +1 -1
  188. package/dist/src/wish-pool.js +1 -1
  189. package/dist/src/z3-shared.js +1 -1
  190. package/extension/README.md +31 -7
  191. package/package.json +286 -11
  192. package/schemas/booking.surface.v1.schema.json +927 -0
  193. package/schemas/booking.surface.v2.schema.json +61 -0
  194. package/ts/capabilities/flyai.ts +16 -3
  195. package/ts/capabilities/session/extension-bridge.ts +37 -14
  196. package/ts/capabilities/session/extension-channel.ts +6 -3
  197. package/ts/capabilities/session/extension-distribution.ts +264 -0
  198. package/ts/capabilities/session/golden-score.ts +139 -0
  199. package/ts/capabilities/session/health-watch.ts +1 -1
  200. package/ts/capabilities/session/static-flight-golden.ts +209 -0
  201. package/ts/capabilities/session/wizard.ts +34 -176
  202. package/ts/capabilities/session-login.ts +15 -5
  203. package/ts/capabilities/session-search.ts +40 -3
  204. package/ts/capabilities/weather.ts +141 -52
  205. package/ts/package.json +3 -3
  206. package/ts/src/benchmark-agent-conformance.ts +448 -0
  207. package/ts/src/benchmark-environment-bridge.ts +348 -0
  208. package/ts/src/benchmark-headless-child-diagnostics.ts +184 -0
  209. package/ts/src/benchmark-tool-isolation.ts +166 -0
  210. package/ts/src/booking-surface/availability-policy-v2.ts +523 -0
  211. package/ts/src/booking-surface/canonical-schema.js +113 -0
  212. package/ts/src/booking-surface/contracts-v2.ts +118 -0
  213. package/ts/src/booking-surface/contracts.ts +380 -0
  214. package/ts/src/booking-surface/dsh-planner.ts +452 -0
  215. package/ts/src/booking-surface/dsh-plugin.js +93 -0
  216. package/ts/src/booking-surface/error-codes.ts +101 -0
  217. package/ts/src/booking-surface/index.ts +12 -0
  218. package/ts/src/booking-surface/profile.ts +42 -0
  219. package/ts/src/booking-surface/runtime-v2.ts +1466 -0
  220. package/ts/src/booking-surface/runtime.ts +483 -0
  221. package/ts/src/booking-surface/server-v2.ts +247 -0
  222. package/ts/src/booking-surface/server.ts +324 -0
  223. package/ts/src/booking-surface/startup.ts +196 -0
  224. package/ts/src/booking-surface/validation-v2.ts +205 -0
  225. package/ts/src/booking-surface/validation.ts +453 -0
  226. package/ts/src/index.ts +64 -11
  227. package/ts/src/state-ledger.ts +1 -0
  228. package/ts/src/tool-budget.ts +165 -0
  229. 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 断言覆盖);
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(issue #21 onboarding UX,P3.6 gotry-session-onboarding-goal,
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
- * 单一职责:**编排** 4 步闭环(扩展落位→打开 chrome://extensions→剪贴板复制→后台 health-watch) +
6
- * 跨平台降级(macOS osascript / Linux zenity / Windows msg / headless 终端)。
7
- * 不实现任何 cookie 值读取、不发起任何写入操作、不引 GUI 包。
4
+ * 单一职责(2026-09-02 重设):**只做 Node 端两件事**——
5
+ * ① 扩展文件从源落位到 `~/.gotry/extension`(幂等)
6
+ * ② source/dest 静态预检 + manifest.key 校验(给 health-watch 前的快照)
8
7
  *
9
- * 形态:
10
- * - `runOnboardingWizard({ dryRun, openBrowser, clipboard, panel })` 返回步骤结果数组;
11
- * - 每步 `{step, status: 'ok'|'skip'|'fail', summary}`;终端 stdout 顺序打印;
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
- * - macOS:`osascript -e 'tell application "Google Chrome" to open location "chrome://extensions"'`
16
- * + `pbcopy <path>`;有 Cocoa 引导面板(`osascript -e 'display dialog ...'`)。
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
- * 不引入 npm 包:osascript/zenity/pbcopy/xclip/clip/start/xdg-open 走 child_process.spawn,
23
- * 失败降级 stdout,不挡主流程(run-all §40 onboarding-tests 4 断言覆盖跨平台降级)。
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, platform } from 'node:os'
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' | 'open-chrome-extensions' | 'clipboard-extension-path' | 'watch-extension-ready' | 'panel-guide'
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
- /** 打开 chrome://extensions;失败降级提示用户手开 */
110
- function openChromeExtensions(sp: typeof spawn, plat: 'darwin' | 'linux' | 'win32' | 'headless', dryRun: boolean): OnboardingStep {
111
- if (dryRun) return { step: 'open-chrome-extensions', status: 'skip', summary: '[dry-run] skip 打开 chrome://extensions' }
112
- if (plat === 'headless') return { step: 'open-chrome-extensions', status: 'skip', summary: 'headless 环境跳过 GUI;请在你桌面 Chrome 手动打开 chrome://extensions' }
113
- const cmd = (() => {
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
- if (plat === 'darwin') {
197
- const script = `display dialog "${message.replace(/"/g, '\\"')}" with title "GoTry 扩展安装" buttons {"我装好了"} default button 1`
198
- sp('osascript', ['-e', script], { stdio: 'ignore', detached: true }).unref?.()
199
- return { step: 'panel-guide', status: 'ok', summary: '已弹 macOS Cocoa 引导面板' }
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
- if (plat === 'linux') {
202
- const child = sp('zenity', ['--info', '--title=GoTry 扩展安装', `--text=${message}`], { stdio: 'ignore', detached: true })
203
- child.unref?.()
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 panelGuideTerminal(plat, extensionDir, 120_000)
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
- * 主入口:wizard 编排 5 步闭环(ensure→open→clipboard→panel→watch-precheck)。
239
- * 真实 health-watch 由 cli runner 在面板按下「我装好了」后启,本函数只做静态预检。
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, platform: plat, extensionDir }
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(`platform=${result.platform}; extensionDir=${result.extensionDir}; overall=${result.ok ? 'ok' : 'has-failures'}\n`)
132
+ destination.write(`extensionDir=${result.extensionDir}; overall=${result.ok ? 'ok' : 'has-failures'}\n`)
275
133
  }
276
134
 
277
- /** 留作 onboard final summary 的只读探针(便于 cli 输出「已就绪 / 待装」) */
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)