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

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 (137) hide show
  1. package/README.md +185 -148
  2. package/README.zh-CN.md +195 -154
  3. package/bin/gotry-bootstrap.js +246 -26
  4. package/bin/gotry-inner.js +68 -38
  5. package/bin/gotry-runtime-resolution.d.ts +1 -1
  6. package/bin/gotry-runtime-resolution.js +7 -8
  7. package/cordis.gotry-patch.yml +15 -5
  8. package/data/stations-12306-verify.json +524 -0
  9. package/dist/capabilities/agent-reach.js +4 -4
  10. package/dist/capabilities/channel-health.js +103 -0
  11. package/dist/capabilities/channel-registry.js +213 -0
  12. package/dist/capabilities/doctor.js +285 -0
  13. package/dist/capabilities/effect.js +178 -5
  14. package/dist/capabilities/flyai.js +13 -0
  15. package/dist/capabilities/session/adapters/ctrip-hotel.js +207 -0
  16. package/dist/capabilities/session/adapters/rail-12306.js +285 -0
  17. package/dist/capabilities/session/extension-bridge.js +14 -1
  18. package/dist/capabilities/session/extension-channel.js +4 -2
  19. package/dist/capabilities/session/extension-distribution.js +1 -1
  20. package/dist/capabilities/session-search.js +311 -0
  21. package/dist/capabilities/visa-policy.js +126 -0
  22. package/dist/scripts/agent-planning-turn-deadline-e2e.js +304 -0
  23. package/dist/scripts/agent-planning-turn-deadline-tests.js +281 -0
  24. package/dist/scripts/benchmark-environment-bridge-e2e.js +5 -10
  25. package/dist/scripts/benchmark-environment-bridge-tests.js +148 -142
  26. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +26 -26
  27. package/dist/scripts/{booking-copilot-availability-policy-v2-tests.js → booking-copilot-availability-policy-tests.js} +115 -115
  28. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +41 -1
  29. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +75 -120
  30. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +370 -61
  31. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +2 -2
  32. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +7 -3
  33. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +15 -9
  34. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +19 -11
  35. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +16 -52
  36. package/dist/scripts/booking-copilot-runtime-proof-tests.js +3953 -155
  37. package/dist/scripts/booking-copilot-server-proof-tests.js +58 -23
  38. package/dist/scripts/booking-copilot-startup-proof-tests.js +10 -119
  39. package/dist/scripts/booking-surface-contract-proof-tests.js +1154 -326
  40. package/dist/scripts/bootstrap-tests.js +115 -44
  41. package/dist/scripts/build-metrics-report.js +339 -0
  42. package/dist/scripts/channel-probe-tests.js +148 -0
  43. package/dist/scripts/channel-probe.js +252 -0
  44. package/dist/scripts/channel-registry-tests.js +262 -0
  45. package/dist/scripts/doctor-tests.js +121 -0
  46. package/dist/scripts/effect-tests.js +274 -1
  47. package/dist/scripts/extension-tests.js +129 -11
  48. package/dist/scripts/fact-gate-tests.js +91 -2
  49. package/dist/scripts/flyai-tests.js +17 -1
  50. package/dist/scripts/hbcli-e2e-tests.js +1 -3
  51. package/dist/scripts/metrics-report-tests.js +288 -0
  52. package/dist/scripts/persona-surface-guard-tests.js +19 -0
  53. package/dist/scripts/session-tests.js +401 -1
  54. package/dist/scripts/smoke.js +100 -83
  55. package/dist/scripts/turn-handoff-collect-tests.js +216 -0
  56. package/dist/scripts/turn-handoff-collect.js +189 -0
  57. package/dist/scripts/turn-policy-tests.js +56 -0
  58. package/dist/scripts/typed-contract-canary.js +261 -0
  59. package/dist/scripts/visa-policy-tests.js +75 -0
  60. package/dist/scripts/wish-channel-gate-tests.js +111 -0
  61. package/dist/src/artifact-gate.js +91 -3
  62. package/dist/src/benchmark-agent-conformance.js +7 -4
  63. package/dist/src/benchmark-environment-bridge.js +68 -7
  64. package/dist/src/bookable-facts.js +62 -3
  65. package/dist/src/booking-surface/{availability-policy-v2.js → availability-policy.js} +89 -89
  66. package/dist/src/booking-surface/canonical-schema.js +8 -22
  67. package/dist/src/booking-surface/contracts.js +70 -19
  68. package/dist/src/booking-surface/dsh-planner.js +310 -177
  69. package/dist/src/booking-surface/dsh-plugin.js +3 -3
  70. package/dist/src/booking-surface/index.js +1 -5
  71. package/dist/src/booking-surface/profile.js +1 -1
  72. package/dist/src/booking-surface/runtime.js +1682 -247
  73. package/dist/src/booking-surface/server.js +371 -185
  74. package/dist/src/booking-surface/startup.js +17 -27
  75. package/dist/src/booking-surface/validation.js +270 -760
  76. package/dist/src/index.js +735 -149
  77. package/dist/src/turn-deadline.js +292 -0
  78. package/dist/src/turn-policy.js +146 -0
  79. package/dist/src/wish-pool.js +5 -0
  80. package/extension/background.js +23 -4
  81. package/extension/content-main.js +47 -9
  82. package/extension/manifest.json +27 -9
  83. package/package.json +6 -26
  84. package/schemas/booking.surface.schema.json +2593 -0
  85. package/ts/capabilities/agent-reach.ts +4 -4
  86. package/ts/capabilities/flyai.ts +21 -3
  87. package/ts/capabilities/session/action-cache.ts +1 -1
  88. package/ts/capabilities/session/adapters/ctrip-hotel.ts +238 -0
  89. package/ts/capabilities/session/adapters/rail-12306.ts +293 -0
  90. package/ts/capabilities/session/extension-bridge.ts +32 -2
  91. package/ts/capabilities/session/extension-channel.ts +9 -8
  92. package/ts/capabilities/session/extension-distribution.ts +3 -1
  93. package/ts/capabilities/session/health-watch.ts +1 -1
  94. package/ts/capabilities/session/read-guard.ts +1 -1
  95. package/ts/capabilities/session/wizard.ts +1 -1
  96. package/ts/capabilities/session-search.ts +323 -1
  97. package/ts/package.json +1 -0
  98. package/ts/scripts/turn-handoff-collect.ts +178 -0
  99. package/ts/src/artifact-gate.ts +82 -2
  100. package/ts/src/benchmark-agent-conformance.ts +14 -6
  101. package/ts/src/benchmark-environment-bridge.ts +29 -10
  102. package/ts/src/bookable-facts.ts +109 -5
  103. package/ts/src/booking-saga.ts +1 -1
  104. package/ts/src/booking-surface/{availability-policy-v2.ts → availability-policy.ts} +124 -125
  105. package/ts/src/booking-surface/canonical-schema.js +8 -22
  106. package/ts/src/booking-surface/contracts.ts +278 -239
  107. package/ts/src/booking-surface/dsh-planner.ts +333 -194
  108. package/ts/src/booking-surface/dsh-plugin.js +3 -3
  109. package/ts/src/booking-surface/index.ts +1 -5
  110. package/ts/src/booking-surface/profile.ts +11 -11
  111. package/ts/src/booking-surface/runtime.ts +1351 -351
  112. package/ts/src/booking-surface/server.ts +282 -175
  113. package/ts/src/booking-surface/startup.ts +36 -48
  114. package/ts/src/booking-surface/validation.ts +194 -442
  115. package/ts/src/contracts.ts +1 -1
  116. package/ts/src/index.ts +489 -171
  117. package/ts/src/loop.ts +1 -1
  118. package/ts/src/state-ledger.ts +1 -1
  119. package/ts/src/turn-deadline.ts +361 -0
  120. package/ts/src/turn-policy.ts +154 -0
  121. package/ts/src/wish-pool.ts +17 -4
  122. package/dist/scripts/agent-planning-budget-e2e.js +0 -227
  123. package/dist/scripts/agent-planning-budget-tests.js +0 -173
  124. package/dist/scripts/booking-copilot-v2-runtime-proof-tests.js +0 -3935
  125. package/dist/scripts/booking-surface-v2-contract-proof-tests.js +0 -1174
  126. package/dist/src/booking-surface/contracts-v2.js +0 -89
  127. package/dist/src/booking-surface/runtime-v2.js +0 -1771
  128. package/dist/src/booking-surface/server-v2.js +0 -334
  129. package/dist/src/booking-surface/validation-v2.js +0 -319
  130. package/dist/src/tool-budget.js +0 -136
  131. package/schemas/booking.surface.v1.schema.json +0 -927
  132. package/schemas/booking.surface.v2.schema.json +0 -61
  133. package/ts/src/booking-surface/contracts-v2.ts +0 -118
  134. package/ts/src/booking-surface/runtime-v2.ts +0 -1466
  135. package/ts/src/booking-surface/server-v2.ts +0 -247
  136. package/ts/src/booking-surface/validation-v2.ts +0 -205
  137. package/ts/src/tool-budget.ts +0 -165
@@ -23,10 +23,18 @@
23
23
  * # 扩展改走 GitHub Releases 下载通道(ADR-21):
24
24
  * # dist-manifest → tar.gz → SHA256 → key 钉扎 → 原子交换;
25
25
  * # 任何失败显式降级回包内副本(离线确定性不变)。
26
+ * node bin/gotry-bootstrap.js calendar # 可选日历挂载开关(D-9,setup 状态面):
27
+ * # 默认写 ~/.gotry/calendar.json {"enabled":true};
28
+ * # --off 删除恢复默认不挂载;--status 只读查看。
29
+ * node bin/gotry-bootstrap.js doctor # 可选依赖体检(2026-09-02 迪拜 session 复盘):
30
+ * # 扩展/agent-reach/.venv/hbcli/flyai key/sidebar/calendar
31
+ * # 逐项只读检查 + 精确补装指引;
32
+ * # 报告落 gotry-state/doctor-report.md(侧栏工作台可预览)。
33
+ * node bin/gotry-bootstrap.js doctor --fix # 体检后按缺失项补装(复用下方三个安装器;LLM key 永不管)。
26
34
  * node bin/gotry-bootstrap.js wizard # 会话扩展 onboarding 闭环(issue #21 onboarding UX,§3.3):
27
35
  * # 5 步编排 + 后台 health-watch 等扩展心跳,
28
36
  * # 扩展一就位 stdout 翻绿并自动重放同 query。
29
- * # 详见 docs/user-session-data-rfc.md §3.3 / RFC P3.6。
37
+ * # 详见 docs/rfc/user-session-data-rfc.md §3.3 / RFC P3.6。
30
38
  *
31
39
  * 环境开关:
32
40
  * GOTRY_SETUP_SKIP=1 全部跳过
@@ -46,7 +54,7 @@
46
54
  */
47
55
 
48
56
  import { spawn } from 'node:child_process'
49
- import { copyFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs'
57
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
50
58
  import { homedir } from 'node:os'
51
59
  import { join, dirname } from 'node:path'
52
60
  import { fileURLToPath } from 'node:url'
@@ -158,13 +166,231 @@ async function setupSidebar() { say('[gotry-setup] dsh-better-sidebar(dsh web
158
166
  if (!launched) say(' 本包未携带 @deepseek-ai/dsh,走 npx 途径安装')
159
167
  const r2 = await run('npx', ['-y', '--package', '@deepseek-ai/dsh', 'dsh', 'plugin', '--profile', 'web', 'add', SIDEBAR_PKG], { timeoutMs: 300_000 })
160
168
  if (!r2.ok) {
161
- say(' ✗ 安装失败——不影响 gotry:产物仍可在对话里说「看看我生成的行程」经 gotry_artifacts_list/read 查看;可稍后重试: npx gotry setup')
169
+ say(' ✗ 安装失败——不影响 gotry:产物仍可在对话里说「看看我生成的行程」经 gotry_artifacts_list/read 查看;可稍后重试: npx gotry doctor --fix')
162
170
  return { ok: false }
163
171
  }
164
172
  say(' ✓ 安装完成(gotry web 刷新浏览器即见右侧工作台)')
165
173
  return { ok: true }
166
174
  }
167
175
 
176
+ // ---------------------------------------------------------------------------
177
+ // doctor(可选依赖体检;与 ts/capabilities/doctor.ts 同一状态面,-bootstrap 是
178
+ // CLI 执行面纯 JS、capabilities 是 MCP 工具面 TS——两边清单/口径成对修改)
179
+ // ---------------------------------------------------------------------------
180
+
181
+ const DOCTOR = process.argv.includes('doctor')
182
+ const DOCTOR_FIX = process.argv.includes('--fix')
183
+ // calendar 子命令(issue #106/D-9):可选日历挂载的 setup 状态管理面。
184
+ // `gotry setup calendar`=开启;`--off`=关闭(删状态文件恢复默认);`--status`=只读查看。
185
+ const CALENDAR_CMD = process.argv.includes('calendar')
186
+ const CALENDAR_OFF = process.argv.includes('--off')
187
+ const CALENDAR_STATUS = process.argv.includes('--status')
188
+
189
+ // --- calendar setup 状态面(与扩展 manifest 同居 ~/.gotry;运行时 inner 与
190
+ // doctor 两端同读这一份,禁止 env 控制产品行为——founder 2026-09-03 纠偏)---
191
+ function calendarStatePath() { return join(homedir(), '.gotry', 'calendar.json') }
192
+ function readCalendarState() {
193
+ try { return JSON.parse(readFileSync(calendarStatePath(), 'utf8')) } catch { return null }
194
+ }
195
+ function calendarProfileConfigured() {
196
+ try {
197
+ const patch = readFileSync(join(homedir(), '.dsh', 'profiles', 'web', 'cordis.patch.yml'), 'utf8')
198
+ return /calendar/.test(patch) && /username\s*:/.test(patch)
199
+ } catch { return false }
200
+ }
201
+
202
+ /** 挂载/配置双态人话(bootstrap doctor 与 calendar 子命令共用) */
203
+ function calendarDetail(state) {
204
+ if (!state || state.enabled !== true) return '默认未挂载(D-9:未配置的日历工具不进工具箱;工作窗口由访谈覆盖,不影响任何检索)'
205
+ return calendarProfileConfigured()
206
+ ? `已挂载且已配置(${calendarStatePath()})`
207
+ : '已挂载但 calendar 未配置 username——日历工具会话中会报「未配置」'
208
+ }
209
+
210
+ async function runCalendar() {
211
+ say('[gotry-setup] dsh-calendar(可选日历,CalDAV 工作窗口读取;默认不挂载)')
212
+ if (CALENDAR_STATUS) {
213
+ const state = readCalendarState()
214
+ say(` 状态: ${calendarDetail(state)}`)
215
+ say(` 状态文件: ${calendarStatePath()}`)
216
+ say(' 说明: 挂载=`npx gotry setup calendar`;关闭=`npx gotry setup calendar --off`;配置在 dsh profile 的 cordis.patch.yml 覆盖 calendar 行 config 填 username')
217
+ return 0
218
+ }
219
+ if (CALENDAR_OFF) {
220
+ try { rmSync(calendarStatePath()) } catch { /* 本就未开启 */ }
221
+ say(' ✓ 已关闭——恢复默认不挂载(状态文件已删除;如需再开: npx gotry setup calendar)')
222
+ return 0
223
+ }
224
+ mkdirSync(dirname(calendarStatePath()), { recursive: true })
225
+ writeFileSync(calendarStatePath(), `${JSON.stringify({ enabled: true, updatedAt: new Date().toISOString() }, null, 2)}\n`, 'utf8')
226
+ say(` ✓ 已开启挂载(状态文件 ${calendarStatePath()};重启 gotry web/headless 生效)`)
227
+ if (calendarProfileConfigured()) {
228
+ say(' ✓ calendar 已在 dsh profile 配置 username——全部就绪')
229
+ } else {
230
+ say(' ⚠ 还差最后一步:在 dsh profile 的 cordis.patch.yml 覆盖 calendar 行的 config,填你的 CalDAV username,例如:')
231
+ say(' - id: dsh-calendar')
232
+ say(' config:')
233
+ say(' username: <你的日历账号>')
234
+ say(' 未配置时日历工具会话中会报「未配置」;不需要日历时可用 --off 恢复默认不挂载。')
235
+ }
236
+ return 0
237
+ }
238
+
239
+ /** 逐项只读检查(永不抛错;LLM key 显式让渡给 dsh 宿主,不体检)。
240
+ * level 与 ts/capabilities/doctor.ts DoctorStatus 同构:ok / missing / degraded。 */
241
+ async function doctorChecks() {
242
+ const items = []
243
+ // Node 运行时
244
+ const nv = process.versions.node
245
+ const [maj, min] = nv.split('.').map(Number)
246
+ const nodeFine = maj > 22 || (maj === 22 && min >= 15)
247
+ items.push({ label: 'Node 运行时', ok: nodeFine, level: nodeFine ? 'ok' : 'missing', detail: `Node ${nv}(需 ≥22.15)`, fix: nodeFine ? undefined : '升级 Node.js 至 22.15+(https://nodejs.org)' })
248
+ // 扩展
249
+ const extManifest = join(homedir(), '.gotry', 'extension', 'manifest.json')
250
+ const extOk = existsSync(extManifest)
251
+ items.push({ label: 'GoTry Session Bridge 扩展', ok: extOk, level: extOk ? 'ok' : 'missing', detail: extOk ? `已就位(${extManifest})` : '未安装——gotry_session_search / gotry_session_login(账号会话通道)不可用,其余工具不受影响', fix: extOk ? undefined : '在 Chrome 应用商店一键安装(自动更新): https://chromewebstore.google.com/detail/gotry-session-bridge/oeajpiccmonococjcegddlooeeohlbgd' })
252
+ // agent-reach(.venv 装在包内)
253
+ const venvPython = join(repoRoot, '.venv/bin/python')
254
+ const reachBin = join(repoRoot, '.venv/bin/agent-reach')
255
+ const reachOk = existsSync(reachBin)
256
+ const reachLevel = reachOk ? 'ok' : existsSync(venvPython) ? 'degraded' : 'missing'
257
+ items.push({ label: 'Agent Reach(网页/社媒读取)', ok: reachOk, level: reachLevel, detail: reachOk ? `已安装(${reachBin})` : reachLevel === 'degraded' ? '.venv 在但缺 agent-reach 包——gotry_agent_reach / gotry_web_search 读页会失败' : '未装配——gotry_agent_reach / gotry_web_search(读网页)/ gotry_video_subtitle / gotry_github_search 全部不可用', fix: reachOk ? undefined : 'npx gotry doctor --fix' })
258
+ // hbcli(裸名靠 PATH 探测;绝对路径 existsSync)
259
+ let hbBin = ''
260
+ for (const p of ['hbcli', join(homedir(), '.local/bin/hbcli'), join(homedir(), '.staicli/current/hbcli')]) {
261
+ if (p === 'hbcli') { if (await probe(p, ['version'])) { hbBin = 'hbcli(PATH)'; break } } else if (existsSync(p)) { hbBin = p; break }
262
+ }
263
+ if (hbBin) {
264
+ const whoami = await probe(hbBin === 'hbcli(PATH)' ? 'hbcli' : hbBin, ['auth', 'whoami'])
265
+ items.push({ label: 'hbcli(酒店实时源)', ok: whoami, level: whoami ? 'ok' : 'degraded', detail: whoami ? `已安装且凭证有效(${hbBin})` : '二进制在,但凭证未配置/失效——酒店检索将降级静态包(非实时)', fix: whoami ? undefined : 'hbcli auth set-credentials --app-key hotelbyte_api_demo --app-secret hotelbyte_api_demo(快速试用沙箱;正式 key 向 HotelByte 申请)' })
266
+ } else {
267
+ items.push({ label: 'hbcli(酒店实时源)', ok: false, level: 'missing', detail: '未安装——酒店检索降级静态包(公开渠道估算,非实时,仅覆盖内置场景)', fix: 'npx gotry doctor --fix' })
268
+ }
269
+ // flyai key(匿名试用额度共享易达限;正式 key 即免)
270
+ const flyaiKey = (process.env.FLYAI_API_KEY ?? '').trim()
271
+ items.push({ label: 'FlyAI(飞猪官方检索)', ok: Boolean(flyaiKey), level: flyaiKey ? 'ok' : 'degraded', detail: flyaiKey ? 'FLYAI_API_KEY 已配(正式 key,无试用额度限制)' : '未配 FLYAI_API_KEY——走匿名试用额度(共享,易达限;达限报 "Trial limit reached")', fix: flyaiKey ? undefined : '到 flyai.open.fliggy.com 控制台申请正式 key,配进环境变量 FLYAI_API_KEY' })
272
+ // sidebar
273
+ const sidebarPkg = join(homedir(), '.dsh/profiles/web/node_modules/dsh-better-sidebar/package.json')
274
+ const sbOk = existsSync(sidebarPkg)
275
+ items.push({ label: 'dsh-better-sidebar(侧栏工作台)', ok: sbOk, level: sbOk ? 'ok' : 'missing', detail: sbOk ? '已安装——web UI 右侧工作台可预览产物与 doctor 报告(gotry-state/doctor-report.md)' : '未安装——dsh web 无右侧工作台,产物与 doctor 报告只能在对话里看(gotry_artifacts_list)', fix: sbOk ? undefined : 'npx gotry doctor --fix' })
276
+ // dsh-calendar(setup 状态面;默认不挂载=ok 是合法态,opt-in 未配置才 degraded)
277
+ const calState = readCalendarState()
278
+ const calOn = calState?.enabled === true
279
+ const calConfigured = calOn && calendarProfileConfigured()
280
+ items.push({ label: 'dsh-calendar(日历工作窗口)', ok: !calOn || calConfigured, level: !calOn ? 'ok' : calConfigured ? 'ok' : 'degraded', detail: calendarDetail(calState), fix: !calOn ? undefined : calConfigured ? undefined : '在 ~/.dsh/profiles/web/cordis.patch.yml 覆盖 calendar 行 config 填 username(或 npx gotry setup calendar --off 恢复默认不挂载)' })
281
+ // dsh-map-tools(patch 分发面宿主插件,issue #139):解析失败启动时整块静默剔除,
282
+ // doctor 把两态照亮。候选清单与 bin/gotry-inner.js 解析链、ts/capabilities/doctor.ts 同口径。
283
+ const mapCandidates = [
284
+ join(repoRoot, 'ts/dsh-runtime/node_modules/dsh-map-tools/lib/index.js'),
285
+ join(repoRoot, 'node_modules/dsh-map-tools/package.json'),
286
+ join(repoRoot, 'ts/node_modules/dsh-map-tools/package.json'),
287
+ join(homedir(), '.dsh/profiles/web/node_modules/dsh-map-tools/package.json'),
288
+ ]
289
+ const mapHit = mapCandidates.find((p) => existsSync(p))
290
+ items.push(mapHit
291
+ ? { label: 'dsh-map-tools(地图/路线/POI)', ok: true, level: 'ok', detail: `已就位(${mapHit})——地图工具可用(零 key,走 OSM/OSRM)`, fix: undefined }
292
+ : { label: 'dsh-map-tools(地图/路线/POI)', ok: false, level: 'missing', detail: '未随包解析——启动时该 patch 条目被静默剔除,地图/路线/POI 工具不会出现在模型工具箱(缺地图不挡旅行规划,只少能力)', fix: 'npx gotry doctor --fix 不覆盖此项:source 布局把 dsh-map-tools 放进 ts/dsh-runtime/node_modules(或 ts/node_modules);npm 布局重装 @danceiny/gotry(随发行版依赖提供)' })
293
+ // dsh-tool-ask-user(结构化澄清卡,人格契约 (5) 的卡片形态载体)
294
+ const askCandidates = [
295
+ join(repoRoot, 'ts/dsh-runtime/vendor/deepseek-ai-dsh-tool-ask-user/package.json'),
296
+ join(repoRoot, 'node_modules/@deepseek-ai/dsh-tool-ask-user/package.json'),
297
+ join(homedir(), '.dsh/profiles/web/node_modules/@deepseek-ai/dsh-tool-ask-user/package.json'),
298
+ ]
299
+ const askHit = askCandidates.find((p) => existsSync(p))
300
+ items.push(askHit
301
+ ? { label: 'dsh-tool-ask-user(结构化澄清卡)', ok: true, level: 'ok', detail: `已就位(${askHit})——ask_user_question 澄清卡可用(web 原生卡片;headless+TTY 用 stdio 提供方)`, fix: undefined }
302
+ : { label: 'dsh-tool-ask-user(结构化澄清卡)', ok: false, level: 'missing', detail: '未解析——启动时澄清卡注入被静默剔除,模型只能散文追问(人格契约 (5) 退化文本形态)', fix: '重装 @danceiny/gotry——该依赖随 dsh 闭包自带,缺失多为安装不完整' })
303
+ // LLM key:显式让渡(founder 2026-09-02:doctor 不管 key)
304
+ items.push({ label: 'LLM key', ok: true, level: 'ok', detail: '由 dsh 宿主 UI 管理——不在体检范围(gotry 不接触、不回显凭证)', fix: undefined })
305
+ return items
306
+ }
307
+
308
+ const doctorIcon = { ok: '✅', missing: '❌', degraded: '⚠️' }
309
+
310
+ /** 修复指引入表:命令类才加反引号( prose 类如「到控制台申请 key」原样) */
311
+ const fixCell = (fix) => (!fix ? '—' : /^(npx|hbcli|curl|pip|python|\$)/.test(fix) ? `\`${fix}\`` : fix)
312
+
313
+ /** 启动一次性摘要行(issue #114,design §3.1③):全 ok 返回 null(静默零输出);
314
+ * 有待处理项给一行人话 + 指路(对话里 gotry_doctor 看详情 / 终端 npx gotry doctor)。
315
+ * 纯函数,bootstrap-tests 直接断言。 */
316
+ function startupDoctorLine(items) {
317
+ const broken = (items ?? []).filter((i) => i.level && i.level !== 'ok')
318
+ if (broken.length === 0) return null
319
+ const human = (lv) => (lv === 'missing' ? '缺' : '半可用')
320
+ return `[gotry] doctor: ${broken.length} 项待处理(${broken.map((i) => `${i.label}=${human(i.level)}`).join('、')})——对话里让助手调 gotry_doctor 看详情与指引,或终端跑 npx gotry doctor`
321
+ }
322
+
323
+ /** 体检报告 markdown(与 ts/capabilities/doctor.ts renderDoctorReportMd 同形) */
324
+ function renderDoctorReportMd(items) {
325
+ const broken = items.filter((i) => i.level !== 'ok' && i.label !== 'LLM key')
326
+ const lines = [
327
+ '# GoTry 依赖体检报告(doctor)',
328
+ '',
329
+ `> 生成于 ${new Date().toISOString()};重新生成:终端 \`npx gotry doctor\`,或在对话里让助手调 gotry_doctor。`,
330
+ '',
331
+ '| 状态 | 依赖 | 现状 | 修复指引 |',
332
+ '|---|---|---|---|',
333
+ ...items.map((i) => `| ${doctorIcon[i.level]} | ${i.label} | ${i.detail} | ${fixCell(i.fix)} |`),
334
+ '',
335
+ `**结论**:${broken.length === 0 ? '全部就绪(可选依赖齐,LLM key 归 dsh 宿主管)。' : `${broken.length} 项待处理:${broken.map((i) => i.label).join('、')}。补装: npx gotry doctor --fix`}`,
336
+ '',
337
+ '---',
338
+ '',
339
+ '- `npx gotry doctor` 随时可重跑(只读,不改任何东西);',
340
+ '- `npx gotry doctor --fix` 按上表补装(hbcli 官方脚本 / agent-reach pip / dsh-better-sidebar 插件);',
341
+ '- LLM key 由 dsh 宿主 UI 管理,gotry 永不体检、不回显。',
342
+ '',
343
+ ]
344
+ return lines.join('\n')
345
+ }
346
+
347
+ /** doctor 主流程:体检 → 打印 →(可选)fix → 报告落盘。
348
+ * exit 0=就绪(或仅剩有自动回退的降级项);exit 1=仍有缺失类问题。 */
349
+ async function runDoctor() {
350
+ let items = await doctorChecks()
351
+ // 启动一次性摘要(issue #114):inner 分离子进程带 --summary 调用——只读体检,
352
+ // 有待处理项打一行 stderr,零写盘零 header;全 ok 静默。与完整体检面(逐项/报告落盘/exit 语义)分离。
353
+ if (process.argv.includes('--summary')) {
354
+ const line = startupDoctorLine(items)
355
+ if (line) console.error(line)
356
+ return 0
357
+ }
358
+ say('[gotry-doctor] GoTry 可选依赖体检(只读;LLM key 归 dsh 宿主管,不在范围)')
359
+ for (const i of items) {
360
+ say(` ${doctorIcon[i.level]} ${i.label}:${i.detail}`)
361
+ if (i.level !== 'ok' && i.fix) say(` ↳ 修复: ${i.fix}`)
362
+ }
363
+ if (DOCTOR_FIX) {
364
+ if (process.platform === 'win32') {
365
+ say('[gotry-doctor] --fix 在 Windows 暂不支持自动安装(hbcli/agent-reach 上游无 win 安装面);请按各项修复指引手动处理')
366
+ } else {
367
+ say('[gotry-doctor] 开始补装缺失项(--fix)…')
368
+ const results = []
369
+ if (process.env.GOTRY_SETUP_HBCLI !== '0') results.push(await setupHbcli())
370
+ if (process.env.GOTRY_SETUP_REACH !== '0') results.push(await setupReach())
371
+ if (process.env.GOTRY_SETUP_SIDEBAR !== '0') results.push(await setupSidebar())
372
+ const failedFix = results.filter((r) => !r.ok).length
373
+ say(failedFix === 0 ? '[gotry-doctor] 补装完成;下面是补装后复检。' : `[gotry-doctor] ${failedFix} 项补装失败——见上方安装器输出;可重跑 npx gotry doctor --fix`)
374
+ items = await doctorChecks()
375
+ for (const i of items) {
376
+ say(` ${doctorIcon[i.level]} ${i.label}:${i.detail}`)
377
+ }
378
+ }
379
+ }
380
+ // 报告落盘(侧栏工作台预览面;写失败不挡体检结论)
381
+ try {
382
+ const stateDir = join(repoRoot, 'gotry-state')
383
+ mkdirSync(stateDir, { recursive: true })
384
+ writeFileSync(join(stateDir, 'doctor-report.md'), renderDoctorReportMd(items), 'utf8')
385
+ say(`[gotry-doctor] 报告已写: ${join(stateDir, 'doctor-report.md')}(dsh 侧栏工作台可直接预览)`)
386
+ } catch (e) {
387
+ say(`[gotry-doctor] 报告写盘失败(${e.message})——体检结论仍有效`)
388
+ }
389
+ // 降级类(凭证未配/flyai 试用)不挡 exit 0——降级有自动回退路径,缺失类才挡
390
+ const stillMissing = items.filter((i) => i.level === 'missing').length
391
+ return stillMissing === 0 ? 0 : 1
392
+ }
393
+
168
394
  const EXTENSION_FILES = ['manifest.json', 'background.js', 'content-main.js', 'content-bridge.js', 'README.md']
169
395
 
170
396
  function readManifestVersion(manifestPath) {
@@ -440,12 +666,17 @@ async function runInlineHealthWatch(timeoutMs) {
440
666
  }
441
667
 
442
668
  async function main() {
443
- // wizard 子命令(2026-09-02 商店上架后退化):**只走 stdout 提示 + 健康探活等待**;
444
- // 不 spawn 任何 GUI 工具(不动 pbcopy / osascript / open / xdg-open / zenity),
445
- // 不打开 chrome://extensions,不动扩展路径——浏览器自己当安装器,gotry 不越界。
446
- // 期望用户路径:`npx gotry web` → dsh UI → 调 gotry_session_search 遇 needs-extension
447
- // → dsh UI 渲商店 URL → 用户点链接 → 装好 → 同会话内自动 retry(health-watch)。
448
- if (WIZARD) {
669
+ // calendar 子命令(issue #106/D-9):可选日历挂载的 setup 状态管理(on/off/status)
670
+ if (CALENDAR_CMD) process.exit(await runCalendar())
671
+
672
+ // doctor 子命令(2026-09-02 迪拜 session 复盘):可选依赖体检 + 补装指引/补装执行;
673
+ // 只读体检零副作用(--fix 才装),win32 也可跑体检(fix 面另有提示)。
674
+ if (DOCTOR) process.exit(await runDoctor())
675
+
676
+ // wizard 子命令(2026-09-02 商店上架后退化):**只走 stdout 提示 + 健康探活等待**;
677
+ // 不 spawn 任何 GUI 工具(不动 pbcopy / osascript / open / xdg-open / zenity),
678
+ // 不打开 chrome://extensions,不动扩展路径——浏览器自己当安装器,gotry 不越界。
679
+ if (WIZARD) {
449
680
  if (WIZARD_DRY_RUN) {
450
681
  say('[gotry-wizard] dry-run(零网络零浏览器零剪贴板;run-all §40 走这条)')
451
682
  say(' 步骤: ensure-extension-files → watch-extension-ready(2 步纯 Node 端)')
@@ -467,44 +698,33 @@ async function main() {
467
698
  const watchResult = await runHealthWatch()
468
699
  if (watchResult.ready) {
469
700
  say(`[gotry-wizard] ✅ 扩展就绪(${watchResult.attempts} 次探活,等待 ${watchResult.waitedMs}ms)`)
470
- say('[gotry-wizard] 现在 `npx gotry web` → dsh UI → 调 gotry_session_search 即可拿到结果。')
471
701
  process.exit(0)
472
702
  } else {
473
703
  say(`[gotry-wizard] ✗ ${watchResult.reason}——未在 ${watchResult.timeoutMs}ms 内就绪`)
474
- say('[gotry-wizard] dsh UI 中 `gotry_session_search` 的 needs-extension 仍带商店链接;无需重跑,wizard 完全幂等。')
704
+ say('[gotry-wizard] 不需重跑;扩展未装时 dsh UI 中 gotry_session_search 的 needs-extension 会带商店链接,wizard 完全幂等。')
475
705
  process.exit(1)
476
706
  }
477
707
  }
478
708
 
479
709
  if (process.platform === 'win32') {
480
- say('[gotry-setup] Windows 暂不支持自动安装(hbcli 上游仅 darwin/linux)。手动指引:')
481
- say(` hbcli: ${HBCLI_INSTALL_CMD}(WSL);agent-reach: python -m venv .venv && .venv/Scripts/pip install ${REACH_INSTALL_URL}`)
482
- say(' dsh-better-sidebar: npx -y --package @deepseek-ai/dsh dsh plugin --profile web add dsh-better-sidebar@latest')
483
- say(' GoTry Session Bridge 扩展:推荐 Chrome 应用商店一键安装 https://chromewebstore.google.com/detail/gotry-session-bridge/oeajpiccmonococjcegddlooeeohlbgd ;本地通道:手动把包内 extension/ 目录拷到 %USERPROFILE%\\.gotry\\extension,再在 chrome://extensions 开发者模式「加载已解压的扩展程序」')
710
+ say('[gotry-setup] GoTry Session Bridge 扩展:推荐 Chrome 应用商店一键安装 https://chromewebstore.google.com/detail/gotry-session-bridge/oeajpiccmonococjcegddlooeeohlbgd ;本地通道:手动把包内 extension/ 目录拷到 %USERPROFILE%\\.gotry\\extension,再在 chrome://extensions 开发者模式「加载已解压的扩展程序」')
484
711
  process.exit(AUTO ? 0 : 1)
485
712
  }
486
713
  if (AUTO && (process.env.CI || process.env.GOTRY_SETUP_SKIP === '1')) {
487
- say('[gotry-setup] CI/GOTRY_SETUP_SKIP 检测——跳过外部依赖自举(可随时手动: npx gotry setup)')
488
- say('GoTry installed. Run: npx gotry web (dsh Web UI on :3080)')
714
+ say('[gotry-setup] CI/GOTRY_SETUP_SKIP 检测——跳过')
489
715
  process.exit(0)
490
716
  }
491
717
  if (!AUTO && process.env.GOTRY_SETUP_SKIP === '1') { say('[gotry-setup] GOTRY_SETUP_SKIP=1——跳过'); process.exit(0) }
492
718
  const results = []
493
- if (process.env.GOTRY_SETUP_HBCLI !== '0') results.push(await setupHbcli())
494
- else say('[gotry-setup] hbcli:GOTRY_SETUP_HBCLI=0 跳过')
495
- if (process.env.GOTRY_SETUP_REACH !== '0') results.push(await setupReach())
496
- else say('[gotry-setup] agent-reach:GOTRY_SETUP_REACH=0 跳过')
497
- if (process.env.GOTRY_SETUP_SIDEBAR !== '0') results.push(await setupSidebar())
498
- else say('[gotry-setup] dsh-better-sidebar:GOTRY_SETUP_SIDEBAR=0 跳过')
719
+ // gotry 自留面只剩扩展是否就位;hbcli/agent-reach/dsh-better-sidebar 由各自宿主生态自管。
499
720
  if (process.env.GOTRY_SETUP_EXTENSION !== '0') results.push(await (EXTENSION_FROM === 'github' ? setupExtensionFromGithub() : setupExtension()))
500
721
  else say('[gotry-setup] GoTry Session Bridge 扩展:GOTRY_SETUP_EXTENSION=0 跳过')
501
- say('[gotry-setup] flyai:无需安装(npx 每次自拉 @fly-ai/flyai-cli,免 key)')
502
722
  const failed = results.filter((r) => !r.ok).length
503
723
  if (failed > 0) {
504
- say(`[gotry-setup] ${failed} 项未就绪——gotry 本体不受影响(各能力均有降级路径);可稍后重跑: npx gotry setup`)
724
+ say(`[gotry-setup] ${failed} 项未就绪——gotry 本体不受影响;可稍后重跑: npx gotry setup`)
505
725
  process.exit(AUTO ? 0 : 1)
506
726
  }
507
- say('[gotry-setup] 全部就绪。Run: npx gotry web')
727
+ say('[gotry-setup] 扩展就绪检查完成。')
508
728
  process.exit(0)
509
729
  }
510
730
 
@@ -9,8 +9,8 @@
9
9
  * 1. 解析 argv + .env(provider-neutral → DEEPSEEK_API_KEY/DEEPSEEK_BASE_URL)
10
10
  * 2. 定位 dsh runtime:
11
11
  * a. repo checkout / npm 安装 → root package 解析锁定的 @deepseek-ai/dsh
12
- * b. 非 benchmark 且 root 缺失时 → legacy vendored node_modules fallback(不走 npx:
13
- * dsh cordis-loader 在子 cwd 求值 plugin name,必须绝对路径 patch)
12
+ * b. legacy vendored fallback 已按 D-27 移除(issue #120)——root 缺失即
13
+ * fail-closed 报错指重装,不再有「解析成功却跑不起来」的玄学形态
14
14
  * 3. 运行时生成 patch(os.tmpdir):把 gotry-tools 插件路径重写为按本包
15
15
  * 位置解析的绝对路径 —— 仓内 cordis.gotry-patch.yml 里的 name 行只是
16
16
  * 占位(本机绝对路径),随 tarball 分发后对其他机器必错。
@@ -20,7 +20,7 @@ import { spawn, spawnSync } from 'node:child_process'
20
20
  import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
21
21
  import { fileURLToPath, pathToFileURL } from 'node:url'
22
22
  import { dirname, join } from 'node:path'
23
- import { tmpdir } from 'node:os'
23
+ import { homedir, tmpdir } from 'node:os'
24
24
  import { createRequire } from 'node:module'
25
25
  import {
26
26
  benchmarkRuntimeSupported,
@@ -39,8 +39,7 @@ const rootRequire = createRequire(join(repoRoot, 'package.json'))
39
39
  // --- 环境 .env 加载(provider-neutral) ---
40
40
  // npm 安装模式优先读用户当前目录的 .env(包目录内不该有凭证);repo 检出读仓根。
41
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
42
+ const envCandidates = sourceCheckoutMode || sourceDshEarly
44
43
  ? [join(repoRoot, '.env')]
45
44
  : [join(process.cwd(), '.env'), join(repoRoot, '.env')]
46
45
  const envLines = []
@@ -82,28 +81,37 @@ if (help) {
82
81
 
83
82
  Usage:
84
83
  gotry web # dsh Web UI on http://127.0.0.1:3080
85
- gotry setup # 安装可选外部依赖(hbcli 官方脚本 / agent-reach pip;装时已自动跑过)
84
+ gotry setup # 扩展就位检查/指引(商店一键装)
85
+ <<<<<<< HEAD
86
+ gotry setup calendar # 可选日历(CalDAV 工作窗口)挂载开关:默认关;--off 关闭;--status 查看
87
+ =======
88
+ >>>>>>> origin/main
89
+ gotry doctor # 可选依赖体检:扩展/agent-reach/hbcli/flyai/sidebar 状态 + 补装指引
90
+ gotry doctor --fix # 体检 + 按报告补装(hbcli 官方脚本 / agent-reach pip / sidebar 插件)
86
91
  gotry "一段完整任务..." # headless 一问一答
87
92
  gotry help # this help
88
-
89
- Prerequisites:
90
- • Node 22.15+
91
- • \`.env\` 里 LLM_API_KEY (DeepSeek / OpenAI 兼容均可;自定义端点另配 LLM_BASE_URL,一般以 /v1 结尾;指定模型另配 LLM_MODEL,同时驱动 dsh 会话面与仓内脚本)
93
+ gotry --version # 版本号(bug 报告必填项)
92
94
 
93
95
  Detail: https://github.com/Danceiny/gotry — README
94
96
  `)
95
97
  process.exit(0)
96
98
  }
97
99
 
98
- // mode 决定路径: 'web'/'setup'/'help' 是字面命令;否则第一段 args[0] 是任务本身的一部分
99
- const literal = new Set(['web', 'setup', 'help', '-h', '--help'])
100
+ // repoRoot 在 npm 安装与源码检出两种形态下都是包根,version 统一从这里取
101
+ if (args[0] === '-v' || args[0] === '--version') {
102
+ console.log(JSON.parse(readFileSync(join(repoRoot, 'package.json'), 'utf-8')).version)
103
+ process.exit(0)
104
+ }
105
+
106
+ // mode 决定路径: 'web'/'setup'/'doctor'/'help' 是字面命令;否则第一段 args[0] 是任务本身的一部分
107
+ const literal = new Set(['web', 'setup', 'doctor', 'help', '-h', '--help'])
100
108
  const isLiteral = literal.has(args[0])
101
109
  const mode = isLiteral ? args[0] : 'headless'
102
110
  const rest = isLiteral ? args.slice(1) : args
103
111
 
104
- // setup:外部依赖自举(hbcli/agent-reach),不需要 dsh runtime 与 LLM key,同步分发后即退
105
- if (mode === 'setup') {
106
- const r = spawnSync(process.execPath, [join(here, 'gotry-bootstrap.js'), ...rest], { stdio: 'inherit' })
112
+ // setup/doctor:外部依赖自举与体检,不需要 dsh runtime 与 LLM key,同步分发后即退
113
+ if (mode === 'setup' || mode === 'doctor') {
114
+ const r = spawnSync(process.execPath, [join(here, 'gotry-bootstrap.js'), mode, ...rest], { stdio: 'inherit' })
107
115
  process.exit(r.status ?? (r.error ? 1 : 0))
108
116
  }
109
117
 
@@ -113,8 +121,7 @@ if (!supportsNodeVersion(process.versions.node)) {
113
121
  process.exit(1)
114
122
  }
115
123
 
116
- // --- dsh runtime 定位:root manifest/require.resolve 优先;旧 vendored 仅作 legacy fallback ---
117
- const vendoredDsh = join(repoRoot, 'ts/dsh-runtime/node_modules/@deepseek-ai/dsh/lib/bin.js')
124
+ // --- dsh runtime 定位(D-27 清偿,issue #120):仅 root manifest/依赖解析;legacy vendored 回退已移除 ---
118
125
  const installedPackageMode = !sourceCheckoutMode
119
126
  const selectedDsh = selectDshRuntime({
120
127
  repoRoot,
@@ -137,7 +144,7 @@ if (!dshBin) {
137
144
  console.error('[gotry] benchmark dsh runtime unavailable')
138
145
  process.exit(1)
139
146
  }
140
- console.error(`[gotry] 找不到 dsh runtime(既无 vendored ${vendoredDsh},依赖里也没有 @deepseek-ai/dsh)。`)
147
+ console.error('[gotry] 找不到 dsh runtime(依赖里没有 @deepseek-ai/dsh;legacy vendored 回退已按 D-27 移除)。')
141
148
  console.error('repo checkout: npm ci && npm --prefix ts ci && node scripts/build-dist.mjs;npm 安装: npm install(检查 node_modules)。')
142
149
  process.exit(1)
143
150
  }
@@ -148,12 +155,12 @@ if (benchmarkEnvironmentConfig && !benchmarkRuntimeSupported(selectedDsh)) {
148
155
 
149
156
  // --- 运行时 patch:插件路径按本包位置重写为绝对路径 ---
150
157
  // 安装包始终指向 dist/ 纯 JS(Node 拒绝 strip node_modules 下的 .ts)。
151
- // repo 检出仅在 vendored runtime 或显式 tsx loader 可用时走 .ts;否则
158
+ // repo 检出仅在显式 tsx loader 可用时走 .ts;否则
152
159
  // 使用当前 worktree 已构建的 dist,避免 Node strip-only 拒绝参数属性语法。
153
160
  const distEntry = join(repoRoot, 'dist/src/index.js')
154
161
  const tsEntry = join(repoRoot, 'ts/src/index.ts')
155
162
  const tsxLoaderActive = /(?:^|\s)--(?:import|loader)(?:=|\s)(?:"[^"]*tsx[^"]*"|'[^']*tsx[^']*'|\S*tsx\S*)/.test(process.env.NODE_OPTIONS ?? '')
156
- const sourceTypeScriptMode = !installedPackageMode && (dshSource === 'legacy-vendored' || tsxLoaderActive)
163
+ const sourceTypeScriptMode = !installedPackageMode && tsxLoaderActive
157
164
  const pluginEntry = !sourceTypeScriptMode && existsSync(distEntry) ? distEntry : tsEntry
158
165
  const distModuleMode = pluginEntry === distEntry
159
166
  const staticPatch = join(repoRoot, 'cordis.gotry-patch.yml')
@@ -372,6 +379,10 @@ if (!benchmarkEnvironmentConfig) {
372
379
  // npm 布局:子路径可能被 exports 挡(resolve 抛错不能留下旧值),裸包名返回真实入口
373
380
  try { mapEntry = require_.resolve('dsh-map-tools/lib/index.js') } catch { mapEntry = '' }
374
381
  if (!mapEntry) { try { mapEntry = require_.resolve('dsh-map-tools') } catch { mapEntry = '' } }
382
+ if (!mapEntry) {
383
+ // ts/node_modules 回退(source 模式:dsh-map-tools 安装在 ts/package.json)
384
+ try { mapEntry = require_.resolve(join(repoRoot, 'ts/node_modules/dsh-map-tools')) } catch { mapEntry = '' }
385
+ }
375
386
  }
376
387
  }
377
388
  // 结构化澄清卡(T2):ask_user_question 工具 + user-questions 服务,从 dsh 包
@@ -403,8 +414,16 @@ if (mapEntry) {
403
414
  patchRaw = patchRaw.replace(/\n\s*- id: dsh-map-tools\n\s*name: 'placeholder\/dsh-map-tools'\n/, '\n')
404
415
  }
405
416
 
406
- // dsh-calendar 宿主插件(CalDAV 工作窗口读取;未配置时工具报错降级,不挡启动)。
407
- // 离线预算 E2E 显式禁用可选宿主插件,避免测试依赖真实 CalDAV 账号。
417
+ // dsh-calendar 宿主插件(CalDAV 工作窗口读取)——D-9 拍板(issue #106):默认不挂载。
418
+ // 未配置的日历工具是纯负资产(会话中段才撞「未配置 username」报错),gotry 对它的
419
+ // 唯一诉求(工作窗口)由访谈首轮覆盖。挂载与否由 **setup 状态面**决定(founder
420
+ // 2026-09-03 纠偏:禁止环境变量控制产品行为;可选依赖进 setup 状态管理,与扩展
421
+ // manifest 同居 ~/.gotry)——`npx gotry setup calendar` 写 ~/.gotry/calendar.json,
422
+ // `--off` 删除恢复默认;doctor 报告三态。
423
+ let calEnabled = false
424
+ try {
425
+ calEnabled = JSON.parse(readFileSync(join(homedir(), '.gotry', 'calendar.json'), 'utf-8'))?.enabled === true
426
+ } catch { /* 缺文件/坏文件 = 默认不挂载 */ }
408
427
  let calEntry = ''
409
428
  if (!benchmarkEnvironmentConfig) {
410
429
  const vendoredCal = join(repoRoot, 'ts/dsh-runtime/node_modules/dsh-calendar/lib/index.js')
@@ -415,7 +434,7 @@ if (!benchmarkEnvironmentConfig) {
415
434
  if (!calEntry) { try { calEntry = require_.resolve('dsh-calendar') } catch { calEntry = '' } }
416
435
  }
417
436
  }
418
- if (calEntry && process.env.GOTRY_DISABLE_OPTIONAL_CALENDAR !== '1') {
437
+ if (calEntry && calEnabled) {
419
438
  patchRaw = patchRaw.replace(/(name:\s*)'placeholder\/dsh-calendar'/, `$1'${calEntry}'`)
420
439
  } else {
421
440
  patchRaw = patchRaw.replace(/\n\s*- id: dsh-calendar\n\s*name: 'placeholder\/dsh-calendar'\n/, '\n')
@@ -427,9 +446,21 @@ if (calEntry && process.env.GOTRY_DISABLE_OPTIONAL_CALENDAR !== '1') {
427
446
  // 两条目标在 headless/web profile 均有(dsh-base 声明 llm-deepseek、两 profile 均声明
428
447
  // agent-default-model)。llm-deepseek 目录整体替换为单条目:显式指定模型的场景多为
429
448
  // 中转/兼容端点,默认 v4-* 目录对其是误导;不硬编码上游 DEFAULT_MODELS 防漂移。
449
+ let llmMaxTokens
430
450
  if (process.env.GOTRY_LLM_MODEL) {
451
+ const rawMax = process.env.LLM_MAX_TOKENS
452
+ if (rawMax !== undefined && rawMax !== '') {
453
+ llmMaxTokens = Number.parseInt(rawMax, 10)
454
+ if (!Number.isSafeInteger(llmMaxTokens) || llmMaxTokens <= 0) {
455
+ throw new Error(`invalid LLM_MAX_TOKENS: ${JSON.stringify(rawMax)} (expected a positive safe integer)`)
456
+ }
457
+ }
431
458
  const modelYaml = `'${process.env.GOTRY_LLM_MODEL.replace(/'/g, "''")}'`
432
- 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`
459
+ // LLM_MAX_TOKENS(Round 9,#100/#102):中转/兼容端点模型常带低于 dsh 未知模型
460
+ // 默认 256K 预算的输出上限(MiniMax 2013 实测);目录条目 maxTokens 元数据是
461
+ // dsh-llm-deepseek defaultMaxTokens 的官方通道(web UI 模型同源)。
462
+ const maxTokensYaml = llmMaxTokens === undefined ? '' : `\n maxTokens: ${llmMaxTokens}`
463
+ 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}${maxTokensYaml}\n`
433
464
  }
434
465
  // Keep the patch private and short-lived. mkdtempSync creates a 0700 directory;
435
466
  // wx prevents accidental reuse/races if a process is started concurrently.
@@ -450,20 +481,8 @@ const terminateOnSignal = (signal, listener) => {
450
481
  process.off(signal, listener)
451
482
  process.kill(process.pid, signal)
452
483
  }
453
- const onSigint = () => terminateOnSignal('SIGINT', onSigint)
454
- const onSigterm = () => terminateOnSignal('SIGTERM', onSigterm)
455
- process.once('SIGINT', onSigint)
456
- process.once('SIGTERM', onSigterm)
457
- if (!process.env.DEEPSEEK_API_KEY && mode !== 'help') {
458
- console.error('[gotry] 缺少 LLM API key —— 两种方式任选其一后重跑:')
459
- console.error(` 1) 在当前目录创建 .env 写入一行: LLM_API_KEY=<你的 DeepSeek key>(key 从 https://platform.deepseek.com 获取)`)
460
- console.error(' 2) 或临时环境变量: export LLM_API_KEY=<key>')
461
- cleanupPatch()
462
- process.exit(1)
463
- }
464
-
465
- // --- 调 vendored dsh 二进制(不走 npx)---
466
- // headless: 第一个非 -- 之后的参数是 task —— gotry argv 0 是 headless 触发,其余都算 task;
484
+ // LLM key 由 dsh 宿主管理(凭证是用户资产,UI 在 dsh 里;gotry 不拦截启动期,
485
+ // 不在用户面前展示任何 key 配置引导),此处直接放手 spawn dsh。
467
486
  const binJs = mode === 'web'
468
487
  ? ['web', '--patch', patchPath, ...(process.argv.includes('--no-open') ? ['--no-open'] : [])]
469
488
  : ['--profile', 'headless', '--patch', patchPath, ...rest]
@@ -493,6 +512,17 @@ const childStdio = mode === 'web'
493
512
  let benchmarkCapturedBytes = 0
494
513
  let benchmarkDiagnosticBuffer = Buffer.alloc(0)
495
514
  let benchmarkOutputTruncated = false
515
+ // 启动一次性 doctor 摘要(issue #114,design §3.1③):分离子进程后台跑只读体检,
516
+ // 有待处理项打一行 stderr(stderr 继承,不污染 stdout;benchmark 面保持零杂音)。
517
+ // detached+unref:不阻塞不拖慢启动;inner 生命周期天然一次,不重复刷;失败静默。
518
+ if (!benchmarkEnvironmentConfig) {
519
+ try {
520
+ spawn(process.execPath, [join(here, 'gotry-bootstrap.js'), 'doctor', '--summary'], {
521
+ detached: true,
522
+ stdio: ['ignore', 'ignore', 'inherit'],
523
+ }).unref()
524
+ } catch { /* 体检不可用不挡启动 */ }
525
+ }
496
526
  child = spawn(process.execPath, [dshBin, ...binJs], {
497
527
  stdio: childStdio,
498
528
  env: childEnv,
@@ -5,7 +5,7 @@ export type DshResolver = {
5
5
  export type DshRuntime = {
6
6
  bin: string
7
7
  version: string
8
- source: 'root' | 'legacy-vendored'
8
+ source: 'root'
9
9
  }
10
10
 
11
11
  export const REQUIRED_BENCHMARK_DSH_VERSION: '0.1.2-alpha.3'
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync } from 'node:fs'
1
+ import { readFileSync } from 'node:fs'
2
2
  import { dirname, join } from 'node:path'
3
3
 
4
4
  export const REQUIRED_BENCHMARK_DSH_VERSION = '0.1.2-alpha.3'
@@ -29,15 +29,14 @@ export function supportsNodeVersion(version) {
29
29
  return major > 22 || (major === 22 && minor >= 15)
30
30
  }
31
31
 
32
- export function selectDshRuntime({ repoRoot, rootResolver, benchmark }) {
32
+ export function selectDshRuntime({ rootResolver }) {
33
33
  const root = resolveDshPackage(rootResolver)
34
34
  if (root) return { ...root, source: 'root' }
35
- if (benchmark) return null
36
-
37
- const vendoredBin = join(repoRoot, 'ts/dsh-runtime/node_modules/@deepseek-ai/dsh/lib/bin.js')
38
- if (!existsSync(vendoredBin)) return null
39
- const vendored = readDshPackage(vendoredBin)
40
- return vendored ? { ...vendored, source: 'legacy-vendored' } : null
35
+ // D-27 清偿(issue #120):legacy vendored 回退路径已移除——该形态的 Node 兼容窗口
36
+ // 断裂(Round 5 起「不承诺可运行」),解析成功只会把「dsh 缺失」变成下游玄学失败。
37
+ // 唯一受支持形态 = root manifest / 依赖解析(216 精确依赖闭包);找不到即 fail-closed,
38
+ // 由调用方给明确重装指引。benchmark 原本就禁用回退,语义不变(入参保留以稳调用面)。
39
+ return null
41
40
  }
42
41
 
43
42
  export function benchmarkRuntimeSupported(runtime) {