@jonathanleelx/meddle 0.5.0-beta.9 → 0.5.1

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 (217) hide show
  1. package/CONFIG_STRUCTURE.md +9 -2
  2. package/README.md +18 -0
  3. package/bin/commands/mock/add.js +59 -21
  4. package/bin/commands/mock/delete.js +16 -24
  5. package/bin/commands/mock/index.js +59 -25
  6. package/bin/commands/mock/list.js +35 -18
  7. package/bin/commands/mock/toggle.js +17 -27
  8. package/bin/commands/mock/update.js +39 -30
  9. package/bin/commands/route/active.js +20 -3
  10. package/bin/commands/route/add.js +113 -94
  11. package/bin/commands/route/create.js +8 -1
  12. package/bin/commands/route/delete.js +87 -53
  13. package/bin/commands/route/index.js +74 -26
  14. package/bin/commands/route/list.js +2 -2
  15. package/bin/commands/route/preview.js +2 -2
  16. package/bin/commands/route/show.js +12 -3
  17. package/bin/commands/route/update.js +135 -92
  18. package/bin/commands/status.js +27 -11
  19. package/bin/commands/update.js +4 -0
  20. package/bin/doctor.js +8 -4
  21. package/bin/index.js +11 -7
  22. package/bin/lib/api-client.js +14 -2
  23. package/bin/lib/file-access.js +234 -35
  24. package/bin/lib/meddle-home.js +14 -1
  25. package/bin/lib/output.js +30 -4
  26. package/bin/lib/proxy-detect.js +99 -20
  27. package/bin/lib/proxy-or-file.js +93 -0
  28. package/bin/lib/route-write-args.js +150 -0
  29. package/bin/lib/rule-file-edit.js +263 -0
  30. package/bin/lib/ruleset-readonly.js +61 -0
  31. package/bin/lib/update-check.js +67 -6
  32. package/dist/cert.d.ts +2 -0
  33. package/dist/cert.d.ts.map +1 -1
  34. package/dist/cert.js +53 -1
  35. package/dist/cert.js.map +1 -1
  36. package/dist/core/browser.d.ts +10 -1
  37. package/dist/core/browser.d.ts.map +1 -1
  38. package/dist/core/browser.js +252 -36
  39. package/dist/core/browser.js.map +1 -1
  40. package/dist/core/config-diagnostics.d.ts.map +1 -1
  41. package/dist/core/config-diagnostics.js +6 -1
  42. package/dist/core/config-diagnostics.js.map +1 -1
  43. package/dist/core/custom-plugin-loader.d.ts.map +1 -1
  44. package/dist/core/custom-plugin-loader.js +2 -0
  45. package/dist/core/custom-plugin-loader.js.map +1 -1
  46. package/dist/core/mock-handler.d.ts +63 -0
  47. package/dist/core/mock-handler.d.ts.map +1 -1
  48. package/dist/core/mock-handler.js +641 -29
  49. package/dist/core/mock-handler.js.map +1 -1
  50. package/dist/core/mock-utils.d.ts +58 -0
  51. package/dist/core/mock-utils.d.ts.map +1 -0
  52. package/dist/core/mock-utils.js +167 -0
  53. package/dist/core/mock-utils.js.map +1 -0
  54. package/dist/core/plugin-bootstrap-runner.d.ts +1 -0
  55. package/dist/core/plugin-bootstrap-runner.d.ts.map +1 -1
  56. package/dist/core/plugin-bootstrap-runner.js +37 -1
  57. package/dist/core/plugin-bootstrap-runner.js.map +1 -1
  58. package/dist/core/plugin-source.d.ts +24 -0
  59. package/dist/core/plugin-source.d.ts.map +1 -0
  60. package/dist/core/plugin-source.js +47 -0
  61. package/dist/core/plugin-source.js.map +1 -0
  62. package/dist/core/proxy-record.d.ts +14 -0
  63. package/dist/core/proxy-record.d.ts.map +1 -1
  64. package/dist/core/proxy-record.js +39 -0
  65. package/dist/core/proxy-record.js.map +1 -1
  66. package/dist/core/rule-file-edit.d.ts +81 -0
  67. package/dist/core/rule-file-edit.d.ts.map +1 -0
  68. package/dist/core/rule-file-edit.js +243 -0
  69. package/dist/core/rule-file-edit.js.map +1 -0
  70. package/dist/core/static-server.d.ts.map +1 -1
  71. package/dist/core/static-server.js +15 -13
  72. package/dist/core/static-server.js.map +1 -1
  73. package/dist/core/types.d.ts +15 -1
  74. package/dist/core/types.d.ts.map +1 -1
  75. package/dist/plugins/builtin/mock-plugin.d.ts.map +1 -1
  76. package/dist/plugins/builtin/mock-plugin.js +6 -5
  77. package/dist/plugins/builtin/mock-plugin.js.map +1 -1
  78. package/dist/server/agent/tools.d.ts.map +1 -1
  79. package/dist/server/agent/tools.js +24 -18
  80. package/dist/server/agent/tools.js.map +1 -1
  81. package/dist/server/app-info.d.ts +33 -0
  82. package/dist/server/app-info.d.ts.map +1 -0
  83. package/dist/server/app-info.js +116 -0
  84. package/dist/server/app-info.js.map +1 -0
  85. package/dist/server/config.d.ts +1 -0
  86. package/dist/server/config.d.ts.map +1 -1
  87. package/dist/server/config.js +114 -16
  88. package/dist/server/config.js.map +1 -1
  89. package/dist/server/health.d.ts.map +1 -1
  90. package/dist/server/health.js +8 -0
  91. package/dist/server/health.js.map +1 -1
  92. package/dist/server/index.d.ts +16 -0
  93. package/dist/server/index.d.ts.map +1 -1
  94. package/dist/server/index.js +10 -0
  95. package/dist/server/index.js.map +1 -1
  96. package/dist/server/json-parse-error.d.ts +7 -0
  97. package/dist/server/json-parse-error.d.ts.map +1 -0
  98. package/dist/server/json-parse-error.js +28 -0
  99. package/dist/server/json-parse-error.js.map +1 -0
  100. package/dist/server/mocks.d.ts +26 -0
  101. package/dist/server/mocks.d.ts.map +1 -1
  102. package/dist/server/mocks.js +110 -13
  103. package/dist/server/mocks.js.map +1 -1
  104. package/dist/server/plugins.d.ts.map +1 -1
  105. package/dist/server/plugins.js +56 -11
  106. package/dist/server/plugins.js.map +1 -1
  107. package/dist/server/rule-files.d.ts +36 -0
  108. package/dist/server/rule-files.d.ts.map +1 -1
  109. package/dist/server/rule-files.js +388 -107
  110. package/dist/server/rule-files.js.map +1 -1
  111. package/dist/server/ruleset-lock.d.ts +62 -0
  112. package/dist/server/ruleset-lock.d.ts.map +1 -0
  113. package/dist/server/ruleset-lock.js +234 -0
  114. package/dist/server/ruleset-lock.js.map +1 -0
  115. package/index.js +128 -16
  116. package/mcp-server.js +108 -93
  117. package/package.json +1 -1
  118. package/plugins/builtin/mock-plugin.ts +9 -5
  119. package/web/dist/assets/alert-B514Frte.js +1 -0
  120. package/web/dist/assets/arrow-right-BNum_060.js +1 -0
  121. package/web/dist/assets/{body-diff-view-B-4XWmL6.js → body-diff-view-SfLCLzs9.js} +2 -2
  122. package/web/dist/assets/button-BFNlGZ5p.js +1 -0
  123. package/web/dist/assets/check-BkfHQ6re.js +1 -0
  124. package/web/dist/assets/checkbox-CQyFsZzS.js +1 -0
  125. package/web/dist/assets/chevron-down-DHwUtL4O.js +1 -0
  126. package/web/dist/assets/{circle-alert-Q8cZk9Rv.js → circle-alert-zMagrFuI.js} +1 -1
  127. package/web/dist/assets/{circle-x-C9USji_R.js → circle-x-BEkinhez.js} +1 -1
  128. package/web/dist/assets/code-DD-AYxxu.js +1 -0
  129. package/web/dist/assets/{copy-BYjq1hdN.js → copy-BRnscYy1.js} +1 -1
  130. package/web/dist/assets/detail-panel-DZRKJG2W.js +4 -0
  131. package/web/dist/assets/dist-BQN0rzUS.js +1 -0
  132. package/web/dist/assets/{dist-CEd_wnRC.js → dist-C-E2R2jk.js} +1 -1
  133. package/web/dist/assets/{dist-C6eaYCUo.js → dist-CFZ4xMSF.js} +1 -1
  134. package/web/dist/assets/{dist-DWmDaXVN.js → dist-CRmX_NvH.js} +1 -1
  135. package/web/dist/assets/{dist-C8RHq5At.js → dist-DLQKRjuW.js} +1 -1
  136. package/web/dist/assets/{dist-Blyulj4_.js → dist-LiAcI8CK.js} +1 -1
  137. package/web/dist/assets/{dist-Ce4ujolc.js → dist-fzPL-8F9.js} +1 -1
  138. package/web/dist/assets/{dist-DoE7uybN.js → dist-wv9ebJK_.js} +1 -1
  139. package/web/dist/assets/editor-pane-actions-_ckwcCII.js +1 -0
  140. package/web/dist/assets/{es2015-DG46NGN7.js → es2015-DPJTBYa9.js} +1 -1
  141. package/web/dist/assets/global-panel-shell-Iir-c_pQ.js +6 -0
  142. package/web/dist/assets/health-panel-Dmf-EhJz.js +1 -0
  143. package/web/dist/assets/index-C4dIj1nT.js +21 -0
  144. package/web/dist/assets/index-CdYMl1M7.css +2 -0
  145. package/web/dist/assets/{mobile-proxy-panel-tU0NyJ3P.js → mobile-proxy-panel-CFcwa11A.js} +6 -6
  146. package/web/dist/assets/mock-config-CGl50MIr.js +1 -0
  147. package/web/dist/assets/mock-editor-panel-xcCk23oH.js +20 -0
  148. package/web/dist/assets/mocks-file-missing-note-CW_ONVLA.js +1 -0
  149. package/web/dist/assets/monaco-editor-BkP7envQ.js +11 -0
  150. package/web/dist/assets/pane-bar-5j9Nf7W_.js +1 -0
  151. package/web/dist/assets/pane-split-C0OSe365.js +1 -0
  152. package/web/dist/assets/panel-heading-izT5q0ZN.js +1 -0
  153. package/web/dist/assets/{pencil-line-Cqe41E4h.js → pencil-line-CEZ7D-de.js} +1 -1
  154. package/web/dist/assets/plugin-code-editor-DKE9dFEE.js +2 -0
  155. package/web/dist/assets/plugin-config-Cg09TsuV.js +2 -0
  156. package/web/dist/assets/plugin-generator-CT1f1tu0.js +2 -0
  157. package/web/dist/assets/plugin-test-dialog-BbLIMnkJ.js +7 -0
  158. package/web/dist/assets/plus-ixZQRprm.js +1 -0
  159. package/web/dist/assets/{refresh-cw-CG3HaiQT.js → refresh-cw-BfLoHqg5.js} +1 -1
  160. package/web/dist/assets/rotate-ccw-CRguJya4.js +1 -0
  161. package/web/dist/assets/route-preview-CKO7nF4W.js +1 -0
  162. package/web/dist/assets/{rule-ai-assistant-panel-DMh0F9dr.js → rule-ai-assistant-panel-CtfUqdYG.js} +2 -2
  163. package/web/dist/assets/rule-config-Cb4ejIXI.js +15 -0
  164. package/web/dist/assets/rule-overview-panel-xtHTS27O.js +1 -0
  165. package/web/dist/assets/{server-Cpj4KNwN.js → server-eYXq3PbE.js} +1 -1
  166. package/web/dist/assets/settings-panel-CpQuE6nr.js +1 -0
  167. package/web/dist/assets/{shield-check-OqUmVLQt.js → shield-check-hQXjdfZ7.js} +1 -1
  168. package/web/dist/assets/smartphone-D4Q7cQfm.js +1 -0
  169. package/web/dist/assets/switch-94vgsMHm.js +1 -0
  170. package/web/dist/assets/{syntax-highlight-Dk0-ewLI.js → syntax-highlight-BFgN_C0Y.js} +1 -1
  171. package/web/dist/assets/{table-2-BR5e3z8m.js → table-2-DvGHhMm4.js} +1 -1
  172. package/web/dist/assets/table-DaXD6jJv.js +1 -0
  173. package/web/dist/assets/textarea-9wB8Hsz-.js +1 -0
  174. package/web/dist/assets/toast-9yeTf7d9.js +1 -0
  175. package/web/dist/assets/use-mocks-file-status-BUIUJ4oQ.js +1 -0
  176. package/web/dist/assets/{wand-sparkles-CJ7fSnom.js → wand-sparkles-Bmt4dDOf.js} +1 -1
  177. package/web/dist/assets/{wifi-CCLKyySQ.js → wifi-Cjj_Br1u.js} +1 -1
  178. package/web/dist/assets/{zap-DR-xiUMe.js → zap-BuOtogAv.js} +1 -1
  179. package/web/dist/index.html +16 -10
  180. package/web/dist/assets/alert-DCvlZAiv.js +0 -1
  181. package/web/dist/assets/arrow-right-B4Mrlj2-.js +0 -1
  182. package/web/dist/assets/button-BTToMaBW.js +0 -1
  183. package/web/dist/assets/checkbox-D1Z9DtjF.js +0 -1
  184. package/web/dist/assets/chevron-down-CPngAx4M.js +0 -1
  185. package/web/dist/assets/chevron-right-V1DGIDbL.js +0 -1
  186. package/web/dist/assets/code-5_eJ_XpJ.js +0 -1
  187. package/web/dist/assets/code-fixer-D48sCDtp.js +0 -20
  188. package/web/dist/assets/detail-panel-BSFz9CUw.js +0 -4
  189. package/web/dist/assets/file-system-access-_og-Tg9o.js +0 -1
  190. package/web/dist/assets/global-panel-shell-5cJneeis.js +0 -6
  191. package/web/dist/assets/health-panel-BS3L2e5d.js +0 -1
  192. package/web/dist/assets/index-CvTR1IVi.js +0 -18
  193. package/web/dist/assets/index-bfV7sxx5.css +0 -2
  194. package/web/dist/assets/mock-config-CULJP6Vt.js +0 -1
  195. package/web/dist/assets/mock-editor-panel-9wlDaySA.js +0 -1
  196. package/web/dist/assets/monaco-editor-BttaUoOz.js +0 -11
  197. package/web/dist/assets/plugin-code-editor-BAIBp4lg.js +0 -2
  198. package/web/dist/assets/plugin-config-ButUV2SZ.js +0 -1
  199. package/web/dist/assets/plugin-generator-uceUPSCv.js +0 -2
  200. package/web/dist/assets/plugin-test-dialog-BrJ4HfRX.js +0 -7
  201. package/web/dist/assets/plus-tVxSydJ8.js +0 -1
  202. package/web/dist/assets/rotate-ccw-CAYM6Vv0.js +0 -1
  203. package/web/dist/assets/route-preview-C4cGrS1-.js +0 -1
  204. package/web/dist/assets/rule-config-BTTGNwMs.js +0 -15
  205. package/web/dist/assets/rule-overview-panel-SFR2OloL.js +0 -1
  206. package/web/dist/assets/select-aBXQ0zNa.js +0 -1
  207. package/web/dist/assets/settings-panel-BMReDG8b.js +0 -1
  208. package/web/dist/assets/smartphone-DKeDuJTU.js +0 -1
  209. package/web/dist/assets/switch-Bes_YiJu.js +0 -1
  210. package/web/dist/assets/table-nW7uQVcl.js +0 -1
  211. package/web/dist/assets/textarea-BSI0rnW6.js +0 -1
  212. /package/web/dist/assets/{babel-DCr4VA1c.js → babel-bwZm0qi3.js} +0 -0
  213. /package/web/dist/assets/{clipboard-VhPXNN5m.js → clipboard-Bt1qY01V.js} +0 -0
  214. /package/web/dist/assets/{estree-qO_m2Qaw.js → estree-MatbZDwq.js} +0 -0
  215. /package/web/dist/assets/{html-VDGahtyk.js → html-CCrsbFYX.js} +0 -0
  216. /package/web/dist/assets/{postcss-BUm8EKMw.js → postcss-BxN7Gr8n.js} +0 -0
  217. /package/web/dist/assets/{standalone-Bj8SSd8Q.js → standalone-DsnFnsqM.js} +0 -0
package/index.js CHANGED
@@ -120,8 +120,39 @@ const { createPluginBootstrapRunner } = require('./dist/core/plugin-bootstrap-ru
120
120
  const { openBrowserWithProxy } = require('./dist/core/browser')
121
121
  const { handleLocalRequest } = require('./dist/core/static-server')
122
122
  const { createConfigDiagnostics } = require('./dist/core/config-diagnostics')
123
- const { appendProxyRecord } = require('./dist/core/proxy-record')
123
+ const { appendProxyRecord, appendUpstreamErrorRecord } = require('./dist/core/proxy-record')
124
124
  const { createRuntimeHealthMonitor, parseWatchdogConfig } = require('./dist/core/runtime-health')
125
+
126
+ function previewRequestBody(reqBody) {
127
+ if (!reqBody) return ''
128
+ try {
129
+ if (Buffer.isBuffer(reqBody)) {
130
+ const slice = reqBody.length > 65536 ? reqBody.subarray(0, 65536) : reqBody
131
+ return slice.toString('utf8')
132
+ }
133
+ return String(reqBody)
134
+ } catch (_) {
135
+ return ''
136
+ }
137
+ }
138
+
139
+ /** Send 502 and put the failed request in the traffic list (issue #160). */
140
+ function failProxyWithUpstreamError(res, info) {
141
+ finishResponseWithProxyError(res, info.statusCode || 502)
142
+ try {
143
+ appendUpstreamErrorRecord(ctx, {
144
+ method: info.method,
145
+ source: info.source,
146
+ target: info.target,
147
+ startTime: info.startTime || Date.now(),
148
+ clientIdentity: info.clientIdentity,
149
+ requestHeaders: info.requestHeaders,
150
+ requestBody: previewRequestBody(info.reqBody),
151
+ err: info.err,
152
+ statusCode: info.statusCode || 502,
153
+ })
154
+ } catch (_) {}
155
+ }
125
156
  const { createRateLimitedLogger } = require('./dist/core/log-rate-limit')
126
157
 
127
158
  const remoteAccess = buildRemoteAccessConfig()
@@ -200,7 +231,10 @@ const { createApp } = require('./dist/server/index')
200
231
  let configDiag = null
201
232
 
202
233
  const serverContext = {
203
- currentMocksPath: ctx.currentMocksPath,
234
+ // Issue #118: the mocks path must also live only on ctx. refresh-config
235
+ // reset a stale serverContext copy, so the proxy kept the old file.
236
+ get currentMocksPath() { return ctx.currentMocksPath },
237
+ set currentMocksPath(p) { ctx.currentMocksPath = p },
204
238
  routeRules: ctx.routeRules,
205
239
  ruleMap: ctx.ruleMap,
206
240
  excludeMap: ctx.excludeMap,
@@ -208,8 +242,14 @@ const serverContext = {
208
242
  proxyRecordArr: ctx.proxyRecordArr,
209
243
  proxyRecordDetailMap: ctx.proxyRecordDetailMap,
210
244
  recordIdSeq: ctx.recordIdSeq,
211
- mockRules: ctx.mockRules,
212
- mockIdSeq: ctx.mockIdSeq,
245
+ // Issue #115: mock state must live in exactly one place. Plain copies went
246
+ // stale because /api handlers run asynchronously (after body parsing),
247
+ // after handleLocalRequest had already synced serverContext -> ctx, so
248
+ // every quick create reused the same id and overwrote the previous rule.
249
+ get mockRules() { return ctx.mockRules },
250
+ set mockRules(rules) { ctx.mockRules = rules },
251
+ get mockIdSeq() { return ctx.mockIdSeq },
252
+ set mockIdSeq(seq) { ctx.mockIdSeq = seq },
213
253
  requestPipeline: ctx.requestPipeline,
214
254
  builtinLoggerPlugin: ctx.builtinLoggerPlugin,
215
255
  shadowCompareTracker: ctx.shadowCompareTracker,
@@ -218,6 +258,7 @@ const serverContext = {
218
258
  hookDispatcher: ctx.hookDispatcher,
219
259
  settingsPath: ctx.settingsPath,
220
260
  meddleDir: ctx.meddleDir,
261
+ appInfo: { name: require('./package.json').name, version: require('./package.json').version },
221
262
  settings: null,
222
263
  loadMockRules: () => {
223
264
  mockHandler.loadMockRules()
@@ -225,7 +266,10 @@ const serverContext = {
225
266
  serverContext.mockIdSeq = ctx.mockIdSeq
226
267
  },
227
268
  saveMockRules: () => mockHandler.saveMockRules(),
269
+ assertMocksWritable: () => mockHandler.assertMocksWritable(),
270
+ getMocksParseError: () => mockHandler.getMocksParseError(),
228
271
  reloadCustomPlugins: () => pluginBoot.reloadCustomPlugins(),
272
+ unloadCustomPlugin: (pluginId) => pluginBoot.unloadCustomPlugin(pluginId),
229
273
  logRuleMap: () => routeLoader.logRuleMap(),
230
274
  reloadAllRuleFiles: () => routeLoader.reloadAllRuleFiles(),
231
275
  broadcastToAllClients: (data) => {
@@ -264,7 +308,20 @@ const expressApp = createApp(serverContext)
264
308
 
265
309
  // ===== 加载 Mock 和路由规则 =====
266
310
  serverContext.loadMockRules()
311
+ {
312
+ // Issue #121: relative mocksFilePath now resolves against MEDDLE_HOME; warn once
313
+ // if the file only exists under the old rule (start directory). No auto-migration.
314
+ const { detectLegacyMocksPath, readConfiguredMocksPath, formatLegacyMocksNotice } = require('./dist/core/mock-handler')
315
+ const legacy = detectLegacyMocksPath(readConfiguredMocksPath(ctx.settingsPath), ctx.meddleDir)
316
+ if (legacy) console.warn(formatLegacyMocksNotice(legacy))
317
+ }
267
318
  routeLoader.initRouteRules()
319
+ // Hot-reload mocks when the file is edited externally (CLI file-mode / hand edit). Issue #85.
320
+ mockHandler.initMockFileWatcher(() => {
321
+ serverContext.mockRules = ctx.mockRules
322
+ serverContext.mockIdSeq = ctx.mockIdSeq
323
+ serverContext.broadcastToAllClients({ type: 'mocksUpdated', rules: ctx.mockRules })
324
+ })
268
325
 
269
326
  // ===== Cross-Origin 插件 =====
270
327
  const plugins = [{
@@ -400,6 +457,24 @@ const proxyServer = http.createServer(async (req, res) => {
400
457
  const url = new URL(target.startsWith('http') ? target : req.url, 'http://' + req.headers.host)
401
458
  const routeChanged = source !== url.href
402
459
  const startTime = Date.now()
460
+ let upstreamFailureRecorded = false
461
+ const failUpstream = (err) => {
462
+ if (upstreamFailureRecorded) {
463
+ finishResponseWithProxyError(res)
464
+ return
465
+ }
466
+ upstreamFailureRecorded = true
467
+ failProxyWithUpstreamError(res, {
468
+ method: req.method,
469
+ source,
470
+ target: url.href,
471
+ startTime,
472
+ clientIdentity,
473
+ requestHeaders: req.headers,
474
+ reqBody,
475
+ err,
476
+ })
477
+ }
403
478
  const proxyReq = http.request(url, { method: req.method, headers: stripProxyHeaders(req.headers) }, (proxyRes) => {
404
479
  const resChunks = []
405
480
  const pluginResponseInterception = pluginIntercept.shouldInterceptResponse()
@@ -411,7 +486,7 @@ const proxyServer = http.createServer(async (req, res) => {
411
486
  proxyResponseSettled = true
412
487
  pluginIntercept.emitLegacyErrorToPlugins('onBeforeResponse', err)
413
488
  logRuntimeError(`http:proxy-response:${getErrorKey(err)}`, 'HTTP proxy response error:', getErrorMessage(err))
414
- finishResponseWithProxyError(res)
489
+ failUpstream(err)
415
490
  }
416
491
  if (routeChanged) proxyRes.headers['x-real-url'] = url.href
417
492
  if (!intercepting) res.writeHead(proxyRes.statusCode, proxyRes.headers)
@@ -474,7 +549,7 @@ const proxyServer = http.createServer(async (req, res) => {
474
549
  } catch (err) {
475
550
  pluginIntercept.emitLegacyErrorToPlugins('onBeforeResponse', err)
476
551
  logRuntimeError(`http:response-handler:${getErrorKey(err)}`, 'HTTP proxy response handling error:', getErrorMessage(err))
477
- finishResponseWithProxyError(res)
552
+ failUpstream(err)
478
553
  }
479
554
  })
480
555
  })
@@ -490,14 +565,22 @@ const proxyServer = http.createServer(async (req, res) => {
490
565
  } else {
491
566
  logRuntimeError(`http:proxy-request:${getErrorKey(err)}`, 'HTTP proxy error:', getErrorMessage(err))
492
567
  }
493
- finishResponseWithProxyError(res)
568
+ failUpstream(err)
494
569
  })
495
570
  proxyReq.write(reqBody)
496
571
  proxyReq.end()
497
572
  } catch (err) {
498
573
  pluginIntercept.emitLegacyErrorToPlugins('onBeforeResponse', err)
499
574
  logRuntimeError(`http:request-handler:${getErrorKey(err)}`, 'HTTP proxy request failed:', getErrorMessage(err))
500
- finishResponseWithProxyError(res)
575
+ failProxyWithUpstreamError(res, {
576
+ method: req.method,
577
+ source,
578
+ target: source,
579
+ startTime: Date.now(),
580
+ clientIdentity,
581
+ requestHeaders: req.headers,
582
+ err,
583
+ })
501
584
  }
502
585
  })
503
586
  })
@@ -523,7 +606,7 @@ localWSServer.on('connection', (client, req) => {})
523
606
  await ensureRootCA({ trustCheckAsync: true })
524
607
  await pluginBoot.bootstrapBuiltinPlugins()
525
608
  const port = await getFreePort()
526
- proxyServer.listen(port, remoteAccess.bindHost, () => {
609
+ proxyServer.listen(port, remoteAccess.bindHost, async () => {
527
610
  const proxyUrl = `http://127.0.0.1:${port}`
528
611
  console.log(chalk.green('代理服务器已启动:'), chalk.cyan(proxyUrl))
529
612
  proxyDebug('proxy-server start on ' + chalk.green(proxyUrl))
@@ -531,16 +614,19 @@ localWSServer.on('connection', (client, req) => {})
531
614
  if (ctx.requestPipeline.mode === 'on') {
532
615
  proxyDebug('plugin on host allowlist: ' + (ctx.PLUGIN_ON_HOSTS.size > 0 ? Array.from(ctx.PLUGIN_ON_HOSTS).join(',') : '(all)'))
533
616
  }
534
- if (process.env.MEDDLE_MCP) {
617
+ // Always write probe file so CLI can find this instance (issue #85).
618
+ // Previously only wrote under MEDDLE_MCP, leaving stale ports after normal starts.
619
+ {
535
620
  const mcpFile = path.join(ctx.meddleDir, 'mcp-proxy-url.json')
536
- const mcpData = { proxyUrl }
621
+ const mcpData = { proxyUrl, pid: process.pid, startedAt: new Date().toISOString() }
537
622
  if (process.env.MEDDLE_OPEN_CHROMEDEVTOOLS) mcpData.remoteDebuggingPort = 9222
538
623
  fs.writeFileSync(mcpFile, JSON.stringify(mcpData), 'utf8')
539
624
  }
540
625
  if (ctx.AUTO_OPEN) {
541
626
  const proxyAddr = `127.0.0.1:${port}`
542
627
  const remotePort = process.env.MEDDLE_OPEN_CHROMEDEVTOOLS ? 9222 : undefined
543
- if (openBrowserWithProxy(proxyUrl, proxyAddr, ctx.meddleDir, remotePort)) {
628
+ const opened = await openBrowserWithProxy(proxyUrl, proxyAddr, ctx.meddleDir, remotePort).catch(() => false)
629
+ if (opened) {
544
630
  console.log(chalk.green('已启动浏览器(代理:'), proxyAddr + chalk.green(')'))
545
631
  } else {
546
632
  console.log(chalk.yellow('浏览器并未启动,请手动打开'), chalk.cyan(proxyUrl), chalk.yellow('并设置代理'), proxyAddr)
@@ -661,6 +747,24 @@ proxyServer.on('connect', async (req, socket, head) => {
661
747
 
662
748
  const routeChanged = source !== target
663
749
  const startTime = Date.now()
750
+ let upstreamFailureRecorded = false
751
+ const failUpstream = (err) => {
752
+ if (upstreamFailureRecorded) {
753
+ finishResponseWithProxyError(res)
754
+ return
755
+ }
756
+ upstreamFailureRecorded = true
757
+ failProxyWithUpstreamError(res, {
758
+ method: req.method,
759
+ source,
760
+ target,
761
+ startTime,
762
+ clientIdentity: requestClientIdentity,
763
+ requestHeaders: req.headers,
764
+ reqBody,
765
+ err,
766
+ })
767
+ }
664
768
  try {
665
769
  const proxyRes = await makeProxyRequest(target, req.method, stripProxyHeaders(req.headers), reqBody)
666
770
  const resChunks = []
@@ -673,7 +777,7 @@ proxyServer.on('connect', async (req, socket, head) => {
673
777
  proxyResponseSettled = true
674
778
  pluginIntercept.emitLegacyErrorToPlugins('onBeforeResponse', err)
675
779
  logRuntimeError(`https:upstream-stream:${originHost}:${getErrorKey(err)}`, '[proxy] upstream stream %s: %s', originHost + req.url, getErrorMessage(err))
676
- finishResponseWithProxyError(res)
780
+ failUpstream(err)
677
781
  }
678
782
  if (routeChanged) proxyRes.headers['x-real-url'] = target
679
783
  if (!intercepting) res.writeHead(proxyRes.statusCode, cleanHeadersForH2(proxyRes.headers))
@@ -737,7 +841,7 @@ proxyServer.on('connect', async (req, socket, head) => {
737
841
  } catch (err) {
738
842
  pluginIntercept.emitLegacyErrorToPlugins('onBeforeResponse', err)
739
843
  logRuntimeError(`https:response-handler:${originHost}:${getErrorKey(err)}`, '[proxy] upstream response handling %s: %s', originHost + req.url, getErrorMessage(err))
740
- finishResponseWithProxyError(res)
844
+ failUpstream(err)
741
845
  }
742
846
  })
743
847
  } catch (err) {
@@ -751,12 +855,20 @@ proxyServer.on('connect', async (req, socket, head) => {
751
855
  logRuntimeError(`https:upstream:${originHost}:${getErrorKey(err)}`, '[proxy] upstream %s: %s', originHost + req.url, getErrorMessage(err))
752
856
  }
753
857
  pluginIntercept.emitLegacyErrorToPlugins('onBeforeResponse', err)
754
- finishResponseWithProxyError(res)
858
+ failUpstream(err)
755
859
  }
756
860
  } catch (err) {
757
861
  pluginIntercept.emitLegacyErrorToPlugins('onBeforeResponse', err)
758
862
  logRuntimeError(`https:request-handler:${originHost}:${getErrorKey(err)}`, '[proxy] HTTPS request handling %s: %s', originHost + req.url, getErrorMessage(err))
759
- finishResponseWithProxyError(res)
863
+ failProxyWithUpstreamError(res, {
864
+ method: req.method,
865
+ source,
866
+ target: source,
867
+ startTime: Date.now(),
868
+ clientIdentity: requestClientIdentity,
869
+ requestHeaders: req.headers,
870
+ err,
871
+ })
760
872
  }
761
873
  })
762
874
  })