@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.
Files changed (229) hide show
  1. package/README.md +161 -116
  2. package/README.zh-CN.md +173 -144
  3. package/bin/gotry-booking-copilot.js +53 -0
  4. package/bin/gotry-bootstrap.js +113 -66
  5. package/bin/gotry-inner.js +436 -70
  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 +45 -39
  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
@@ -6,10 +6,10 @@
6
6
  * gotry help # help
7
7
  *
8
8
  * 工作流程:
9
- * 1. 解析 argv + .env(provider-neutral → DEEPSEEK_API_KEY)
9
+ * 1. 解析 argv + .env(provider-neutral → DEEPSEEK_API_KEY/DEEPSEEK_BASE_URL)
10
10
  * 2. 定位 dsh runtime:
11
- * a. repo checkout → vendored ts/dsh-runtime/node_modules/...
12
- * b. npm 安装 → createRequire 解析依赖 @deepseek-ai/dsh(不走 npx:
11
+ * a. repo checkout / npm 安装 → root package 解析锁定的 @deepseek-ai/dsh
12
+ * b. 非 benchmark 且 root 缺失时 → legacy vendored node_modules fallback(不走 npx:
13
13
  * dsh cordis-loader 在子 cwd 求值 plugin name,必须绝对路径 patch)
14
14
  * 3. 运行时生成 patch(os.tmpdir):把 gotry-tools 插件路径重写为按本包
15
15
  * 位置解析的绝对路径 —— 仓内 cordis.gotry-patch.yml 里的 name 行只是
@@ -17,20 +17,30 @@
17
17
  */
18
18
 
19
19
  import { spawn, spawnSync } from 'node:child_process'
20
- import { existsSync, readFileSync, writeFileSync } from 'node:fs'
21
- import { fileURLToPath } from 'node:url'
20
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
21
+ import { fileURLToPath, pathToFileURL } from 'node:url'
22
22
  import { dirname, join } from 'node:path'
23
23
  import { tmpdir } from 'node:os'
24
24
  import { createRequire } from 'node:module'
25
+ import {
26
+ benchmarkRuntimeSupported,
27
+ resolveDshPackage,
28
+ selectDshCwd,
29
+ selectDshRuntime,
30
+ supportsNodeVersion,
31
+ } from './gotry-runtime-resolution.js'
25
32
 
26
33
  const here = dirname(fileURLToPath(import.meta.url))
27
34
  const repoRoot = join(here, '..')
35
+ const sourceCheckoutMode = existsSync(join(repoRoot, '.git'))
28
36
  const require_ = createRequire(import.meta.url)
37
+ const rootRequire = createRequire(join(repoRoot, 'package.json'))
29
38
 
30
39
  // --- 环境 .env 加载(provider-neutral) ---
31
40
  // npm 安装模式优先读用户当前目录的 .env(包目录内不该有凭证);repo 检出读仓根。
32
- const vendoredDshEarly = existsSync(join(repoRoot, 'ts/dsh-runtime/node_modules/@deepseek-ai/dsh/lib/bin.js'))
33
- const envCandidates = vendoredDshEarly
41
+ const sourceDshEarly = resolveDshPackage(rootRequire)
42
+ const vendoredDshEarly = !sourceDshEarly && existsSync(join(repoRoot, 'ts/dsh-runtime/node_modules/@deepseek-ai/dsh/lib/bin.js'))
43
+ const envCandidates = sourceCheckoutMode || vendoredDshEarly
34
44
  ? [join(repoRoot, '.env')]
35
45
  : [join(process.cwd(), '.env'), join(repoRoot, '.env')]
36
46
  const envLines = []
@@ -46,6 +56,22 @@ for (const line of envLines) {
46
56
  if (process.env.LLM_API_KEY && !process.env.DEEPSEEK_API_KEY) {
47
57
  process.env.DEEPSEEK_API_KEY = process.env.LLM_API_KEY
48
58
  }
59
+ // base 同点映射(issue #48):dsh 侧 llm-deepseek 读 DEEPSEEK_BASE_URL 并拼
60
+ // `${base}/chat/completions`(与 ts/src/dsh-llm.ts 同语义,自定义端点一般含 /v1);
61
+ // 只映射 key 不映射 base 时,OpenAI 兼容 key 会被发往 DeepSeek 官方端点必然 401。
62
+ if (process.env.LLM_BASE_URL && !process.env.DEEPSEEK_BASE_URL) {
63
+ process.env.DEEPSEEK_BASE_URL = process.env.LLM_BASE_URL
64
+ }
65
+ // model 映射(issue #77):LLM_MODEL 显式存在时让它真正驱动 dsh 会话面——
66
+ // ① 这里映射为 GOTRY_LLM_MODEL 传给 gotry-tools 插件,在 agent/request 瀑布做
67
+ // 内存覆盖(dsh settings 用户层 ~/.dsh 压过 composition 层,单靠 patch 不够;
68
+ // 覆盖零持久化,进程退即散,不改写用户设置);
69
+ // ② 下方运行时 patch 追加两条 by-id 覆盖:agent-default-model 默认模型 +
70
+ // llm-deepseek 目录条目(web UI 模型页可见、元数据可记账)。
71
+ // 不设 LLM_MODEL 则两条都不动——默认路径(组合配置或用户 web 设置)面不变。
72
+ if (process.env.LLM_MODEL && !process.env.GOTRY_LLM_MODEL) {
73
+ process.env.GOTRY_LLM_MODEL = process.env.LLM_MODEL
74
+ }
49
75
 
50
76
  // --- argv 解析 ---
51
77
  const args = process.argv.slice(2)
@@ -60,10 +86,6 @@ Usage:
60
86
  gotry "一段完整任务..." # headless 一问一答
61
87
  gotry help # this help
62
88
 
63
- Prerequisites:
64
- • Node 22+
65
- • \`.env\` 里 LLM_API_KEY (DeepSeek / OpenAI 兼容均可)
66
-
67
89
  Detail: https://github.com/Danceiny/gotry — README
68
90
  `)
69
91
  process.exit(0)
@@ -81,35 +103,55 @@ if (mode === 'setup') {
81
103
  process.exit(r.status ?? (r.error ? 1 : 0))
82
104
  }
83
105
 
84
- // --- dsh runtime 定位:repo checkout(vendored)优先,npm 安装走依赖解析 ---
85
- const vendoredDsh = join(repoRoot, 'ts/dsh-runtime/node_modules/@deepseek-ai/dsh/lib/bin.js')
86
- let dshBin = ''
87
- let dshCwd = ''
88
- if (existsSync(vendoredDsh)) {
89
- dshBin = vendoredDsh
90
- dshCwd = join(repoRoot, 'ts/dsh-runtime')
91
- } else {
92
- try {
93
- dshBin = require_.resolve('@deepseek-ai/dsh/lib/bin.js')
94
- dshCwd = process.cwd() // npm 安装:gotry-state 落在用户调用目录
95
- } catch {
96
- dshBin = ''
97
- }
106
+ const benchmarkEnvironmentConfig = process.env.GOTRY_BENCHMARK_ENV_CONFIG
107
+ if (!supportsNodeVersion(process.versions.node)) {
108
+ console.error('[gotry] Node.js 22.15 or newer is required')
109
+ process.exit(1)
98
110
  }
99
111
 
112
+ // --- dsh runtime 定位:root manifest/require.resolve 优先;旧 vendored 仅作 legacy fallback ---
113
+ const vendoredDsh = join(repoRoot, 'ts/dsh-runtime/node_modules/@deepseek-ai/dsh/lib/bin.js')
114
+ const installedPackageMode = !sourceCheckoutMode
115
+ const selectedDsh = selectDshRuntime({
116
+ repoRoot,
117
+ rootResolver: rootRequire,
118
+ benchmark: Boolean(benchmarkEnvironmentConfig),
119
+ })
120
+ let dshBin = selectedDsh?.bin ?? ''
121
+ const dshSource = selectedDsh?.source ?? ''
122
+ const dshCwd = selectedDsh
123
+ ? selectDshCwd({
124
+ repoRoot,
125
+ invocationCwd: process.cwd(),
126
+ sourceCheckoutMode,
127
+ benchmark: Boolean(benchmarkEnvironmentConfig),
128
+ })
129
+ : ''
130
+
100
131
  if (!dshBin) {
132
+ if (benchmarkEnvironmentConfig) {
133
+ console.error('[gotry] benchmark dsh runtime unavailable')
134
+ process.exit(1)
135
+ }
101
136
  console.error(`[gotry] 找不到 dsh runtime(既无 vendored ${vendoredDsh},依赖里也没有 @deepseek-ai/dsh)。`)
102
- console.error('repo checkout: cd ts/dsh-runtime && pnpm install;npm 安装: npm install(检查 node_modules)。')
137
+ console.error('repo checkout: npm ci && npm --prefix ts ci && node scripts/build-dist.mjs;npm 安装: npm install(检查 node_modules)。')
138
+ process.exit(1)
139
+ }
140
+ if (benchmarkEnvironmentConfig && !benchmarkRuntimeSupported(selectedDsh)) {
141
+ console.error('[gotry] benchmark dsh runtime unavailable')
103
142
  process.exit(1)
104
143
  }
105
144
 
106
145
  // --- 运行时 patch:插件路径按本包位置重写为绝对路径 ---
107
- // npm 模式指向 dist/ 纯 JS(Node 拒绝 strip node_modules 下的 .ts);
108
- // repo 检出指向 .ts 源码(vendored runtime 下天然可行)。
146
+ // 安装包始终指向 dist/ 纯 JS(Node 拒绝 strip node_modules 下的 .ts)。
147
+ // repo 检出仅在 vendored runtime 或显式 tsx loader 可用时走 .ts;否则
148
+ // 使用当前 worktree 已构建的 dist,避免 Node strip-only 拒绝参数属性语法。
109
149
  const distEntry = join(repoRoot, 'dist/src/index.js')
110
150
  const tsEntry = join(repoRoot, 'ts/src/index.ts')
111
- const npmMode = !existsSync(vendoredDsh)
112
- const pluginEntry = npmMode && existsSync(distEntry) ? distEntry : tsEntry
151
+ const tsxLoaderActive = /(?:^|\s)--(?:import|loader)(?:=|\s)(?:"[^"]*tsx[^"]*"|'[^']*tsx[^']*'|\S*tsx\S*)/.test(process.env.NODE_OPTIONS ?? '')
152
+ const sourceTypeScriptMode = !installedPackageMode && (dshSource === 'legacy-vendored' || tsxLoaderActive)
153
+ const pluginEntry = !sourceTypeScriptMode && existsSync(distEntry) ? distEntry : tsEntry
154
+ const distModuleMode = pluginEntry === distEntry
113
155
  const staticPatch = join(repoRoot, 'cordis.gotry-patch.yml')
114
156
  if (!existsSync(staticPatch)) {
115
157
  console.error(`[gotry] 找不到 patch 配置: ${staticPatch}`)
@@ -119,16 +161,214 @@ if (!existsSync(pluginEntry)) {
119
161
  console.error(`[gotry] 找不到插件入口: ${pluginEntry}(包不完整?)`)
120
162
  process.exit(1)
121
163
  }
164
+
165
+ // Benchmark mode projects the patch before any optional host plugin is
166
+ // resolved/imported. Non-GoTry (including unknown/future) insert entries are
167
+ // safely discarded; a missing/duplicate gotry-tools entry fails closed.
168
+ function projectBenchmarkPatch(raw, entryPath, configPath) {
169
+ const lines = raw.split('\n')
170
+ const rootInsertKey = /(?:^|[,{}]\s*)(?:insert|'insert'|"insert")\s*:/
171
+ const rootInsertLines = lines
172
+ .map((line, index) => {
173
+ const rootItem = line.match(/^-\s+(.*)$/)?.[1]?.trim()
174
+ const directProperty = line.match(/^ {2}(.*)$/)?.[1]?.trim()
175
+ return rootInsertKey.test(rootItem ?? '') || /^(?:insert|'insert'|"insert")\s*:/.test(directProperty ?? '') ? index : -1
176
+ })
177
+ .filter(index => index >= 0)
178
+ const canonicalInsertStarts = lines
179
+ .map((line, index) => /^- insert:\s*(?:#.*)?$/.test(line) ? index : -1)
180
+ .filter(index => index >= 0)
181
+ if (rootInsertLines.length !== 1 || canonicalInsertStarts.length !== 1 || rootInsertLines[0] !== canonicalInsertStarts[0]) {
182
+ throw new Error('benchmark insert block violation')
183
+ }
184
+ const start = canonicalInsertStarts[0]
185
+ let end = start + 1
186
+ while (end < lines.length) {
187
+ const line = lines[end]
188
+ if (line.trim() !== '' && !/^\s/.test(line) && !line.trim().startsWith('#')) break
189
+ end += 1
190
+ }
191
+ const body = lines.slice(start + 1, end)
192
+ const firstItem = body.find(line => line.trim() !== '' && !line.trim().startsWith('#'))
193
+ const firstItemMatch = firstItem?.match(/^(\s*)-\s+/)
194
+ if (!firstItemMatch) throw new Error('invalid insert sequence')
195
+ const itemIndent = firstItemMatch[1].length
196
+ const itemStarts = []
197
+ for (let index = 0; index < body.length; index += 1) {
198
+ const line = body[index]
199
+ if (line.trim() === '' || line.trim().startsWith('#')) continue
200
+ const indent = line.match(/^\s*/)?.[0].length ?? 0
201
+ if (indent < itemIndent || (indent === itemIndent && !/^\s*-\s+/.test(line))) {
202
+ throw new Error('invalid insert sequence')
203
+ }
204
+ if (indent === itemIndent) itemStarts.push(index)
205
+ }
206
+ if (itemStarts.length === 0) throw new Error('empty insert sequence')
207
+
208
+ const parseId = value => {
209
+ const quoted = value.trim().match(/^(['"])([A-Za-z0-9._-]+)\1(?:\s+#.*)?$/)
210
+ if (quoted) return quoted[2]
211
+ return value.trim().match(/^([A-Za-z0-9._-]+)(?:\s+#.*)?$/)?.[1] ?? null
212
+ }
213
+ const itemId = item => {
214
+ const header = item[0].slice(itemIndent + 2).trim()
215
+ const canonical = header.match(/^id:\s*(.+)$/)
216
+ if (canonical) return parseId(canonical[1])
217
+ if (header.startsWith('{')) {
218
+ const inline = header.match(/(?:^\{\s*|,\s*)id\s*:\s*(['"]?)([A-Za-z0-9._-]+)\1(?=\s*[,}])/)
219
+ if (inline) return inline[2]
220
+ }
221
+ const propertyIndent = ' '.repeat(itemIndent + 2)
222
+ const directIds = item.slice(1)
223
+ .map(line => line.startsWith(`${propertyIndent}id:`) ? parseId(line.slice(propertyIndent.length + 3)) : null)
224
+ .filter(Boolean)
225
+ if (directIds.length > 1) throw new Error('duplicate insert id')
226
+ return directIds[0] ?? null
227
+ }
228
+ const items = itemStarts.map((itemStart, index) => body.slice(itemStart, itemStarts[index + 1] ?? body.length))
229
+ const gotryItems = items.filter(item => itemId(item) === 'gotry-tools')
230
+ if (gotryItems.length !== 1) throw new Error('benchmark insert allowlist violation')
231
+ let gotry = gotryItems[0].join('\n')
232
+ gotry = rewriteBenchmarkPluginEntry(gotry, entryPath)
233
+ gotry = injectBenchmarkEnvironmentConfig(gotry, configPath)
234
+ const projected = [...lines.slice(0, start + 1), ...gotry.split('\n'), ...lines.slice(end)]
235
+ return projectBenchmarkSystemPrompt(projected).join('\n')
236
+ }
237
+
238
+ // The normal persona contains dynamic variables and product-only tool
239
+ // instructions. They are not meaningful in the minimal benchmark kernel and
240
+ // can make dsh fail while assembling the first request. Require the exact
241
+ // source shape before replacing it; malformed or ambiguous prompt config must
242
+ // fail closed rather than being partially interpreted.
243
+ function projectBenchmarkSystemPrompt(lines) {
244
+ const rootStarts = lines.map((line, index) => /^-\s+/.test(line) ? index : -1).filter(index => index >= 0)
245
+ const rootItems = rootStarts.map((itemStart, index) => lines.slice(itemStart, rootStarts[index + 1] ?? lines.length))
246
+ if (rootItems.length !== 2) throw new Error('benchmark system-prompt root shape violation')
247
+ const insertItems = rootItems.filter(item => /^- insert:\s*(?:#.*)?$/.test(item[0]))
248
+ const systemItems = rootItems.filter(item => /^- id:\s*system-prompt\s*(?:#.*)?$/.test(item[0]))
249
+ if (insertItems.length !== 1 || systemItems.length !== 1) throw new Error('benchmark system-prompt canonical shape violation')
250
+ const start = rootStarts[rootItems.indexOf(systemItems[0])]
251
+ let end = start + 1
252
+ while (end < lines.length && (lines[end].trim() === '' || /^\s/.test(lines[end]) || lines[end].trim().startsWith('#'))) end += 1
253
+ const item = lines.slice(start, end)
254
+ const configIndexes = item.map((line, index) => /^ {2}config:\s*$/.test(line) ? index : -1).filter(index => index >= 0)
255
+ const personaIndexes = item.map((line, index) => /^ {4}persona:\s*>-\s*$/.test(line) ? index : -1).filter(index => index >= 0)
256
+ if (configIndexes.length !== 1 || personaIndexes.length !== 1 || personaIndexes[0] <= configIndexes[0]) {
257
+ throw new Error('benchmark system-prompt config violation')
258
+ }
259
+ const persona = personaIndexes[0]
260
+ const blockEnd = item.slice(persona + 1).findIndex(line => line.trim() !== '' && !/^\s{6,}/.test(line))
261
+ const contentEnd = blockEnd < 0 ? item.length : persona + 1 + blockEnd
262
+ if (contentEnd === persona + 1 || item.slice(persona + 1, contentEnd).some(line => line.trim() !== '' && !/^\s{6,}/.test(line))) {
263
+ throw new Error('benchmark system-prompt persona violation')
264
+ }
265
+ const before = item.slice(1, configIndexes[0]).filter(line => line.trim() !== '' && !line.trim().startsWith('#'))
266
+ const between = item.slice(configIndexes[0] + 1, persona).filter(line => line.trim() !== '' && !line.trim().startsWith('#'))
267
+ const after = item.slice(contentEnd).filter(line => line.trim() !== '' && !line.trim().startsWith('#'))
268
+ if (before.length !== 0 || between.length !== 0 || after.length !== 0) throw new Error('benchmark system-prompt shape violation')
269
+ const replacement = [
270
+ '- id: system-prompt',
271
+ ' config:',
272
+ ' persona: >-',
273
+ ' You are GoTry, a task-agnostic travel planning assistant.',
274
+ ' Use only the current conversation and tools available in this benchmark session.',
275
+ ]
276
+ return [...lines.slice(0, start), ...replacement, ...lines.slice(end)]
277
+ }
278
+
279
+ function rewriteBenchmarkPluginEntry(raw, entryPath) {
280
+ const itemIndent = raw.match(/^(\s*)-\s+/)?.[1].length
281
+ if (itemIndent === undefined) throw new Error('benchmark plugin item unavailable')
282
+ const propertyIndent = ' '.repeat(itemIndent + 2)
283
+ const anchorPattern = new RegExp(`^(${propertyIndent}name:\\s*)'[^'\\r\\n]*ts\\/src\\/index\\.ts'(\\s*(?:#.*)?)$`, 'gm')
284
+ const anchors = [...raw.matchAll(anchorPattern)]
285
+ if (anchors.length !== 1 || anchors[0].index === undefined) throw new Error('benchmark plugin anchor unavailable')
286
+ const anchor = anchors[0]
287
+ const escapedEntry = entryPath.replace(/'/g, "''")
288
+ const replacement = `${anchor[1]}'${escapedEntry}'${anchor[2]}`
289
+ const rewritten = `${raw.slice(0, anchor.index)}${replacement}${raw.slice(anchor.index + anchor[0].length)}`
290
+ if (!rewritten.includes(replacement)) throw new Error('benchmark plugin rewrite unavailable')
291
+ return rewritten
292
+ }
293
+
294
+ function injectBenchmarkEnvironmentConfig(raw, configPath) {
295
+ const itemIndent = raw.match(/^(\s*)-\s+/)?.[1].length
296
+ if (itemIndent === undefined) throw new Error('benchmark config item unavailable')
297
+ const propertyIndent = ' '.repeat(itemIndent + 2)
298
+ const configIndent = ' '.repeat(itemIndent + 4)
299
+ const configAnchors = raw.match(new RegExp(`^${propertyIndent}config:\\s*(?:#.*)?$`, 'gm')) ?? []
300
+ if (configAnchors.length !== 1) throw new Error('benchmark config block unavailable')
301
+ if ((raw.match(new RegExp(`^${configIndent}benchmarkEnvironmentConfigPath\\s*:`, 'gm')) ?? []).length !== 0) {
302
+ throw new Error('duplicate benchmark config path')
303
+ }
304
+ const anchorPattern = new RegExp(`^(${configIndent})hbcliBin:\\s*'[^'\\r\\n]*'\\s*(?:#.*)?$`, 'gm')
305
+ const anchors = [...raw.matchAll(anchorPattern)]
306
+ if (anchors.length !== 1 || anchors[0].index === undefined) throw new Error('benchmark config anchor unavailable')
307
+ const anchor = anchors[0]
308
+ const escapedPath = configPath.replace(/'/g, "''")
309
+ const insertAt = anchor.index + anchor[0].length
310
+ const injected = `${raw.slice(0, insertAt)}\n${anchor[1]}benchmarkEnvironmentConfigPath: '${escapedPath}'${raw.slice(insertAt)}`
311
+ if ((injected.match(new RegExp(`^${configIndent}benchmarkEnvironmentConfigPath\\s*:`, 'gm')) ?? []).length !== 1) {
312
+ throw new Error('benchmark config injection unavailable')
313
+ }
314
+ return injected
315
+ }
316
+
317
+ let benchmarkTerminalConfig = null
318
+ let benchmarkPatchProjection = null
319
+ let benchmarkChildDiagnostics = null
320
+ if (benchmarkEnvironmentConfig !== undefined && benchmarkEnvironmentConfig !== '') {
321
+ let validBenchmarkEnvironmentConfig = false
322
+ try {
323
+ const bridgeModule = distModuleMode && existsSync(join(repoRoot, 'dist/src/benchmark-environment-bridge.js'))
324
+ ? join(repoRoot, 'dist/src/benchmark-environment-bridge.js')
325
+ : join(repoRoot, 'ts/src/benchmark-environment-bridge.ts')
326
+ const { loadBenchmarkEnvironmentConfig } = await import(pathToFileURL(bridgeModule).href)
327
+ const loadedConfig = benchmarkEnvironmentConfig.length > 0
328
+ && benchmarkEnvironmentConfig.length <= 4096
329
+ && !benchmarkEnvironmentConfig.includes('\0')
330
+ && !benchmarkEnvironmentConfig.includes('\r')
331
+ && !benchmarkEnvironmentConfig.includes('\n')
332
+ ? loadBenchmarkEnvironmentConfig(benchmarkEnvironmentConfig)
333
+ : null
334
+ validBenchmarkEnvironmentConfig = loadedConfig !== null
335
+ benchmarkTerminalConfig = loadedConfig?.terminal_output ?? null
336
+ if (validBenchmarkEnvironmentConfig) {
337
+ benchmarkPatchProjection = projectBenchmarkPatch(readFileSync(staticPatch, 'utf-8'), pluginEntry, benchmarkEnvironmentConfig)
338
+ }
339
+ } catch { validBenchmarkEnvironmentConfig = false }
340
+ if (!validBenchmarkEnvironmentConfig) {
341
+ console.error('[gotry] benchmark environment configuration unavailable')
342
+ process.exit(1)
343
+ }
344
+ }
345
+ if (benchmarkEnvironmentConfig && mode !== 'headless') {
346
+ console.error('[gotry] benchmark environment requires headless mode')
347
+ process.exit(1)
348
+ }
349
+ if (benchmarkEnvironmentConfig) {
350
+ try {
351
+ const diagnosticModule = distModuleMode && existsSync(join(repoRoot, 'dist/src/benchmark-headless-child-diagnostics.js'))
352
+ ? join(repoRoot, 'dist/src/benchmark-headless-child-diagnostics.js')
353
+ : join(repoRoot, 'ts/src/benchmark-headless-child-diagnostics.ts')
354
+ benchmarkChildDiagnostics = await import(pathToFileURL(diagnosticModule).href)
355
+ } catch {
356
+ console.error('[gotry] benchmark environment configuration unavailable')
357
+ process.exit(1)
358
+ }
359
+ }
122
360
  // dsh-map-tools 宿主插件(地图/路线/POI,零 key 走 OSM/OSRM):repo 用 vendored,
123
361
  // npm 用依赖解析;都找不到就整块剔除 patch 条目(缺地图不挡旅行规划)
124
362
  let mapEntry = ''
125
- const vendoredMap = join(repoRoot, 'ts/dsh-runtime/node_modules/dsh-map-tools/lib/index.js')
126
- if (existsSync(vendoredMap)) {
127
- mapEntry = vendoredMap
128
- } else {
129
- // npm 布局:子路径可能被 exports 挡(resolve 抛错不能留下旧值),裸包名返回真实入口
130
- try { mapEntry = require_.resolve('dsh-map-tools/lib/index.js') } catch { mapEntry = '' }
131
- if (!mapEntry) { try { mapEntry = require_.resolve('dsh-map-tools') } catch { mapEntry = '' } }
363
+ if (!benchmarkEnvironmentConfig) {
364
+ const vendoredMap = join(repoRoot, 'ts/dsh-runtime/node_modules/dsh-map-tools/lib/index.js')
365
+ if (existsSync(vendoredMap)) {
366
+ mapEntry = vendoredMap
367
+ } else {
368
+ // npm 布局:子路径可能被 exports 挡(resolve 抛错不能留下旧值),裸包名返回真实入口
369
+ try { mapEntry = require_.resolve('dsh-map-tools/lib/index.js') } catch { mapEntry = '' }
370
+ if (!mapEntry) { try { mapEntry = require_.resolve('dsh-map-tools') } catch { mapEntry = '' } }
371
+ }
132
372
  }
133
373
  // 结构化澄清卡(T2):ask_user_question 工具 + user-questions 服务,从 dsh 包
134
374
  // 上下文解析(pnpm 嵌套布局下只有 dsh 自己看得见这些依赖);失败整块剔除
@@ -138,7 +378,7 @@ if (existsSync(vendoredMap)) {
138
378
  const stdioAsk = mode === 'headless' && (process.stdin.isTTY || process.env.GOTRY_ASK_STDIO === '1')
139
379
  ? join(here, 'gotry-stdio-ask.js') : ''
140
380
  let askUserInsert = ''
141
- if (mode === 'web' || stdioAsk) try {
381
+ if (!benchmarkEnvironmentConfig && (mode === 'web' || stdioAsk)) try {
142
382
  const reqFromDsh = createRequire(dshBin)
143
383
  // 裸包名解析(这些包的 exports 不暴露 ./lib/index.js 子路径,但裸名返回真实入口)
144
384
  // userQuestions 服务默认树已注册(重复插入会崩),只插工具消费者
@@ -147,60 +387,181 @@ if (mode === 'web' || stdioAsk) try {
147
387
  + ` - id: dsh-tool-ask-user\n name: '${at}'`
148
388
  } catch { /* 缺件不挡启动 */ }
149
389
 
150
- let patchRaw = readFileSync(staticPatch, 'utf-8')
151
- .replace(/(name:\s*)'[^']*ts\/src\/index\.ts'/, `$1'${pluginEntry}'`)
152
- .replace(/^\s*# \{ask-user-insert\}.*\n(\s*# .*\n)?/m, askUserInsert ? askUserInsert + '\n' : '')
390
+ const unboundPatch = benchmarkPatchProjection ?? readFileSync(staticPatch, 'utf-8')
391
+ let patchRaw = benchmarkPatchProjection ?? unboundPatch.replace(/(name:\s*)'[^']*ts\/src\/index\.ts'/, `$1'${pluginEntry}'`)
392
+ patchRaw = patchRaw.replace(/^\s*# \{ask-user-insert\}.*\n(\s*# .*\n)?/m, askUserInsert ? askUserInsert + '\n' : '')
393
+
394
+ // Owner-local benchmark environment opt-in. Keep the path out of tool input
395
+ // and logs; it is only injected into the local plugin config as YAML data.
153
396
  if (mapEntry) {
154
397
  patchRaw = patchRaw.replace(/(name:\s*)'placeholder\/dsh-map-tools'/, `$1'${mapEntry}'`)
155
398
  } else {
156
399
  patchRaw = patchRaw.replace(/\n\s*- id: dsh-map-tools\n\s*name: 'placeholder\/dsh-map-tools'\n/, '\n')
157
400
  }
158
401
 
159
- // dsh-calendar 宿主插件(CalDAV 工作窗口读取;未配置时工具报错降级,不挡启动)
402
+ // dsh-calendar 宿主插件(CalDAV 工作窗口读取;未配置时工具报错降级,不挡启动)。
403
+ // 离线预算 E2E 显式禁用可选宿主插件,避免测试依赖真实 CalDAV 账号。
160
404
  let calEntry = ''
161
- const vendoredCal = join(repoRoot, 'ts/dsh-runtime/node_modules/dsh-calendar/lib/index.js')
162
- if (existsSync(vendoredCal)) {
163
- calEntry = vendoredCal
164
- } else {
165
- try { calEntry = require_.resolve('dsh-calendar/lib/index.js') } catch { calEntry = '' }
166
- if (!calEntry) { try { calEntry = require_.resolve('dsh-calendar') } catch { calEntry = '' } }
405
+ if (!benchmarkEnvironmentConfig) {
406
+ const vendoredCal = join(repoRoot, 'ts/dsh-runtime/node_modules/dsh-calendar/lib/index.js')
407
+ if (existsSync(vendoredCal)) {
408
+ calEntry = vendoredCal
409
+ } else {
410
+ try { calEntry = require_.resolve('dsh-calendar/lib/index.js') } catch { calEntry = '' }
411
+ if (!calEntry) { try { calEntry = require_.resolve('dsh-calendar') } catch { calEntry = '' } }
412
+ }
167
413
  }
168
- if (calEntry) {
414
+ if (calEntry && process.env.GOTRY_DISABLE_OPTIONAL_CALENDAR !== '1') {
169
415
  patchRaw = patchRaw.replace(/(name:\s*)'placeholder\/dsh-calendar'/, `$1'${calEntry}'`)
170
416
  } else {
171
417
  patchRaw = patchRaw.replace(/\n\s*- id: dsh-calendar\n\s*name: 'placeholder\/dsh-calendar'\n/, '\n')
172
418
  }
173
- const patchPath = join(tmpdir(), `cordis.gotry.${process.pid}.yml`)
174
- writeFileSync(patchPath, patchRaw)
175
- if (!process.env.DEEPSEEK_API_KEY && mode !== 'help') {
176
- console.error('[gotry] 缺少 LLM API key —— 两种方式任选其一后重跑:')
177
- console.error(` 1) 在当前目录创建 .env 写入一行: LLM_API_KEY=<你的 DeepSeek key>(key 从 https://platform.deepseek.com 获取)`)
178
- console.error(' 2) 或临时环境变量: export LLM_API_KEY=<key>')
179
- process.exit(1)
180
- }
181
419
 
182
- // --- 调 vendored dsh 二进制(不走 npx)---
183
- // headless: 第一个非 -- 之后的参数是 task —— gotry argv 0 是 headless 触发,其余都算 task;
420
+ // LLM_MODEL 指定模型(issue #77)②:composition 层 by-id 覆盖。cordis patch 语义
421
+ // (dsh-app-boot applyEntryPatches):非 insert 的 {id, ...overrides} 对目标条目做浅层
422
+ // 键赋值,config 整体替换;id 不存在只 warn+skip 不崩(上游改名时优雅退化)。
423
+ // 两条目标在 headless/web profile 均有(dsh-base 声明 llm-deepseek、两 profile 均声明
424
+ // agent-default-model)。llm-deepseek 目录整体替换为单条目:显式指定模型的场景多为
425
+ // 中转/兼容端点,默认 v4-* 目录对其是误导;不硬编码上游 DEFAULT_MODELS 防漂移。
426
+ if (process.env.GOTRY_LLM_MODEL) {
427
+ const modelYaml = `'${process.env.GOTRY_LLM_MODEL.replace(/'/g, "''")}'`
428
+ patchRaw += `\n# LLM_MODEL 指定模型(issue #77):dsh 会话面默认模型 + 模型目录\n- id: agent-default-model\n config:\n provider: deepseek-official\n model: ${modelYaml}\n- id: llm-deepseek\n config:\n models:\n - id: ${modelYaml}\n name: ${modelYaml}\n`
429
+ }
430
+ // Keep the patch private and short-lived. mkdtempSync creates a 0700 directory;
431
+ // wx prevents accidental reuse/races if a process is started concurrently.
432
+ const patchDir = mkdtempSync(join(tmpdir(), 'gotry-cordis-'))
433
+ const patchPath = join(patchDir, 'patch.yml')
434
+ writeFileSync(patchPath, patchRaw, { mode: 0o600, flag: 'wx' })
435
+ let patchCleaned = false
436
+ const cleanupPatch = () => {
437
+ if (patchCleaned) return
438
+ patchCleaned = true
439
+ rmSync(patchDir, { recursive: true, force: true })
440
+ }
441
+ process.once('exit', cleanupPatch)
442
+ let child = null
443
+ const terminateOnSignal = (signal, listener) => {
444
+ cleanupPatch()
445
+ if (child && !child.killed) child.kill(signal)
446
+ process.off(signal, listener)
447
+ process.kill(process.pid, signal)
448
+ }
449
+ // LLM key 由 dsh 宿主管理(凭证是用户资产,UI 在 dsh 里;gotry 不拦截启动期,
450
+ // 不在用户面前展示任何 key 配置引导),此处直接放手 spawn dsh。
184
451
  const binJs = mode === 'web'
185
452
  ? ['web', '--patch', patchPath, ...(process.argv.includes('--no-open') ? ['--no-open'] : [])]
186
453
  : ['--profile', 'headless', '--patch', patchPath, ...rest]
187
454
 
188
- const child = spawn(process.execPath, [dshBin, ...binJs], {
189
- stdio: 'inherit',
190
- env: process.env,
455
+ const childEnv = { ...process.env }
456
+ // The benchmark config is parent-only metadata. Never make its path visible to
457
+ // dsh or any plugin loaded by the child.
458
+ delete childEnv.GOTRY_BENCHMARK_ENV_CONFIG
459
+ if (benchmarkEnvironmentConfig) {
460
+ for (const key of [
461
+ 'GOTRY_BENCHMARK_BRIDGE_PARENT_SECRET',
462
+ 'DATABASE_URL',
463
+ 'SSH_AUTH_SOCK',
464
+ 'AWS_PROFILE',
465
+ 'HTTPS_PROXY',
466
+ ]) delete childEnv[key]
467
+ }
468
+ const benchmarkStdout = benchmarkEnvironmentConfig ? [] : null
469
+ if (benchmarkEnvironmentConfig) childEnv.GOTRY_BENCHMARK_DIAGNOSTIC_FD = '3'
470
+ const childStdio = mode === 'web'
471
+ ? 'inherit'
472
+ : benchmarkStdout
473
+ ? ['pipe', 'pipe', 'pipe', 'pipe']
474
+ : process.stdin.isTTY
475
+ ? 'inherit'
476
+ : ['pipe', 'inherit', 'inherit']
477
+ let benchmarkCapturedBytes = 0
478
+ let benchmarkDiagnosticBuffer = Buffer.alloc(0)
479
+ let benchmarkOutputTruncated = false
480
+ child = spawn(process.execPath, [dshBin, ...binJs], {
481
+ stdio: childStdio,
482
+ env: childEnv,
191
483
  cwd: dshCwd,
192
484
  })
485
+ if (benchmarkStdout) {
486
+ const maxBytes = Math.max(1, Number(benchmarkTerminalConfig?.max_bytes ?? 1))
487
+ child.stdout?.on('data', chunk => {
488
+ if (benchmarkCapturedBytes + Buffer.byteLength(chunk) > maxBytes) benchmarkOutputTruncated = true
489
+ if (benchmarkCapturedBytes < maxBytes) {
490
+ const bytes = Buffer.from(chunk)
491
+ benchmarkStdout.push(bytes.subarray(0, Math.max(0, maxBytes - benchmarkCapturedBytes)))
492
+ benchmarkCapturedBytes += Math.min(bytes.length, maxBytes - benchmarkCapturedBytes)
493
+ }
494
+ })
495
+ child.stderr?.resume()
496
+ child.stdio?.[3]?.on('data', chunk => {
497
+ benchmarkDiagnosticBuffer = benchmarkChildDiagnostics.appendBoundedChildDiagnostic(
498
+ benchmarkDiagnosticBuffer,
499
+ Buffer.from(chunk),
500
+ )
501
+ })
502
+ }
193
503
  if (process.env.GOTRY_DEBUG) {
194
- console.error('[gotry-debug] exec:', process.execPath, dshBin)
195
- console.error('[gotry-debug] argv:', binJs)
196
- console.error('[gotry-debug] cwd:', dshCwd)
197
- console.error('[gotry-debug] patch exists:', existsSync(patchPath), patchPath)
504
+ if (benchmarkEnvironmentConfig) {
505
+ console.error('[gotry-debug] benchmark headless child configured(details redacted)')
506
+ } else {
507
+ console.error('[gotry-debug] exec:', process.execPath, dshBin)
508
+ console.error('[gotry-debug] argv:', binJs)
509
+ console.error('[gotry-debug] cwd:', dshCwd)
510
+ console.error('[gotry-debug] patch exists:', existsSync(patchPath), patchPath)
511
+ }
512
+ }
513
+ let benchmarkFailureReported = false
514
+ const reportBenchmarkFailure = reason => {
515
+ if (benchmarkFailureReported) return
516
+ benchmarkFailureReported = true
517
+ process.exitCode = 1
518
+ process.stderr.write(`[gotry] benchmark terminal output unavailable (${reason})\n`)
198
519
  }
199
- child.on('exit', (code, signal) => {
520
+
521
+ child.on('close', (code, signal) => {
522
+ cleanupPatch()
523
+ if (benchmarkStdout) {
524
+ const captured = Buffer.concat(benchmarkStdout).toString('utf8')
525
+ if (code !== 0 || signal || benchmarkOutputTruncated) {
526
+ const reason = benchmarkChildDiagnostics.classifyBenchmarkChildFailure({
527
+ code,
528
+ signal,
529
+ diagnostic: benchmarkDiagnosticBuffer.toString('utf8'),
530
+ outputTruncated: benchmarkOutputTruncated,
531
+ })
532
+ reportBenchmarkFailure(reason)
533
+ return
534
+ }
535
+ const parserModule = distModuleMode && existsSync(join(repoRoot, 'dist/src/benchmark-agent-conformance.js'))
536
+ ? join(repoRoot, 'dist/src/benchmark-agent-conformance.js')
537
+ : join(repoRoot, 'ts/src/benchmark-agent-conformance.ts')
538
+ import(pathToFileURL(parserModule).href).then(async ({ parseBenchmarkTerminal }) => {
539
+ const parsed = parseBenchmarkTerminal(captured, benchmarkTerminalConfig)
540
+ if (!parsed || parsed.ok !== true) {
541
+ reportBenchmarkFailure('child_terminal_invalid')
542
+ return
543
+ }
544
+ await new Promise((resolve, reject) => {
545
+ const onError = error => {
546
+ process.stdout.off('error', onError)
547
+ reject(error)
548
+ }
549
+ process.stdout.once('error', onError)
550
+ process.stdout.write(captured, () => {
551
+ process.stdout.off('error', onError)
552
+ resolve()
553
+ })
554
+ })
555
+ process.exitCode = 0
556
+ }).catch(() => {
557
+ reportBenchmarkFailure('child_lifecycle_failure')
558
+ })
559
+ return
560
+ }
200
561
  if (code !== 0 || signal) {
201
562
  // D-NEW 护栏:dsh 异常退出也写一条 incident,留现场而非沉默
202
- // npm 模式 import dist JS(node_modules 下的 .ts 会被 Node 拒 strip)
203
- const incidentModule = npmMode && existsSync(join(repoRoot, 'dist/capabilities/incident-log.js'))
563
+ // dist 模式 import JS(node_modules 下的 .ts 会被 Node 拒 strip)
564
+ const incidentModule = distModuleMode && existsSync(join(repoRoot, 'dist/capabilities/incident-log.js'))
204
565
  ? '../dist/capabilities/incident-log.js'
205
566
  : '../ts/capabilities/incident-log.ts'
206
567
  import(incidentModule).then(({ recordIncident }) => {
@@ -215,7 +576,12 @@ child.on('exit', (code, signal) => {
215
576
  process.exit(code ?? 1)
216
577
  })
217
578
  child.on('error', (e) => {
579
+ cleanupPatch()
580
+ if (benchmarkEnvironmentConfig) {
581
+ reportBenchmarkFailure('child_spawn_failure')
582
+ return
583
+ }
218
584
  console.error(`[gotry] dsh 启动失败: ${e.message}`)
219
- console.error('尝试: node -v 看 Node 版本(需 22+),或查看 ts/dsh-runtime/node_modules/。')
585
+ console.error('尝试: node -v 看 Node 版本(需 22.15+),或查看 ts/dsh-runtime/node_modules/。')
220
586
  process.exit(1)
221
587
  })
@@ -0,0 +1,27 @@
1
+ export type DshResolver = {
2
+ resolve(specifier: string): string
3
+ }
4
+
5
+ export type DshRuntime = {
6
+ bin: string
7
+ version: string
8
+ source: 'root' | 'legacy-vendored'
9
+ }
10
+
11
+ export const REQUIRED_BENCHMARK_DSH_VERSION: '0.1.2-alpha.3'
12
+
13
+ export function readDshPackage(bin: string): Omit<DshRuntime, 'source'> | null
14
+ export function resolveDshPackage(resolver: DshResolver): Omit<DshRuntime, 'source'> | null
15
+ export function supportsNodeVersion(version: string): boolean
16
+ export function selectDshRuntime(options: {
17
+ repoRoot: string
18
+ rootResolver: DshResolver
19
+ benchmark: boolean
20
+ }): DshRuntime | null
21
+ export function benchmarkRuntimeSupported(runtime: { version: string } | null): boolean
22
+ export function selectDshCwd(options: {
23
+ repoRoot: string
24
+ invocationCwd: string
25
+ sourceCheckoutMode: boolean
26
+ benchmark: boolean
27
+ }): string