@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
@@ -33,20 +33,36 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.INVALID_RULE_CONTENT_FIELD = exports.INVALID_RULE_CONTENT_JSON = void 0;
37
+ exports.getRulesDir = getRulesDir;
38
+ exports.resolveRuleFileOrder = resolveRuleFileOrder;
39
+ exports.syncActiveRuleFilesToOrder = syncActiveRuleFilesToOrder;
40
+ exports.setRuleFileOrder = setRuleFileOrder;
36
41
  exports.getActiveRuleFileNames = getActiveRuleFileNames;
37
42
  exports.readRuleFileContent = readRuleFileContent;
38
43
  exports.writeRuleFileContent = writeRuleFileContent;
39
44
  exports.createRuleFile = createRuleFile;
40
45
  exports.setActiveRuleFileNames = setActiveRuleFileNames;
41
46
  exports.listRuleFiles = listRuleFiles;
47
+ exports.getRuleFileDetail = getRuleFileDetail;
48
+ exports.updateRuleFile = updateRuleFile;
49
+ exports.deleteRuleFile = deleteRuleFile;
42
50
  exports.mergeActiveRules = mergeActiveRules;
43
51
  exports.buildRuleOverview = buildRuleOverview;
44
52
  exports.ensureRouteRules = ensureRouteRules;
45
53
  exports.registerRuleFilesRoutes = registerRuleFilesRoutes;
54
+ exports.registerUiTokenRoute = registerUiTokenRoute;
46
55
  const fs = __importStar(require("fs"));
47
56
  const path = __importStar(require("path"));
48
57
  const helpers_1 = require("../helpers");
58
+ const ruleset_lock_1 = require("./ruleset-lock");
59
+ const json_parse_error_1 = require("./json-parse-error");
49
60
  const DEFAULT_RULE_NAME = '默认规则';
61
+ /** PUT /api/rule-files/:name/content: body was not JSON. */
62
+ exports.INVALID_RULE_CONTENT_JSON = '请求体不是合法的 JSON';
63
+ /** PUT /api/rule-files/:name/content: `content` is missing or not a string. */
64
+ exports.INVALID_RULE_CONTENT_FIELD = 'content 必须是字符串';
65
+ /** Directory the server reads/writes route-rule files from (<MEDDLE_HOME>/route-rules). */
50
66
  function getRulesDir(ctx) {
51
67
  return path.join(ctx.meddleDir, 'route-rules');
52
68
  }
@@ -91,6 +107,107 @@ function setActiveFileNames(ctx, names) {
91
107
  settings.activeRuleFiles = names;
92
108
  saveSettings(ctx, settings);
93
109
  }
110
+ function getStoredRuleFileOrder(ctx) {
111
+ const settings = loadSettings(ctx);
112
+ const arr = settings.ruleFileOrder;
113
+ return Array.isArray(arr) ? arr.filter((name) => typeof name === 'string') : [];
114
+ }
115
+ function setStoredRuleFileOrder(ctx, names) {
116
+ const settings = loadSettings(ctx);
117
+ settings.ruleFileOrder = names;
118
+ saveSettings(ctx, settings);
119
+ }
120
+ /**
121
+ * Resolve display/tab order for rule files.
122
+ * Prefer persisted ruleFileOrder; when empty, seed as activeRuleFiles then remaining disk names
123
+ * so existing merge order is preserved for enabled files.
124
+ */
125
+ function resolveRuleFileOrder(diskNames, storedOrder, activeNames) {
126
+ const diskSet = new Set(diskNames);
127
+ const ordered = [];
128
+ const seen = new Set();
129
+ const pushUnique = (name) => {
130
+ if (!diskSet.has(name) || seen.has(name))
131
+ return;
132
+ ordered.push(name);
133
+ seen.add(name);
134
+ };
135
+ if (storedOrder.length > 0) {
136
+ for (const name of storedOrder)
137
+ pushUnique(name);
138
+ }
139
+ else {
140
+ for (const name of activeNames)
141
+ pushUnique(name);
142
+ }
143
+ for (const name of diskNames)
144
+ pushUnique(name);
145
+ return ordered;
146
+ }
147
+ /** Keep activeRuleFiles relative order aligned with tab/file order. */
148
+ function syncActiveRuleFilesToOrder(activeNames, fileOrder) {
149
+ const activeSet = new Set(activeNames);
150
+ const synced = fileOrder.filter((name) => activeSet.has(name));
151
+ for (const name of activeNames) {
152
+ if (!synced.includes(name))
153
+ synced.push(name);
154
+ }
155
+ return synced;
156
+ }
157
+ function readDiskRuleFileNames(ctx) {
158
+ const dir = getRulesDir(ctx);
159
+ ensureRulesDir(ctx);
160
+ return fs.readdirSync(dir)
161
+ .filter((f) => f.endsWith('.txt'))
162
+ .map((f) => f.replace(/\.txt$/, ''));
163
+ }
164
+ function ensureRuleFileInOrder(ctx, name) {
165
+ const diskNames = readDiskRuleFileNames(ctx);
166
+ const ordered = resolveRuleFileOrder(diskNames, getStoredRuleFileOrder(ctx), getActiveFileNames(ctx));
167
+ if (!ordered.includes(name))
168
+ ordered.push(name);
169
+ setStoredRuleFileOrder(ctx, ordered);
170
+ return ordered;
171
+ }
172
+ function renameRuleFileInOrder(ctx, oldName, newName) {
173
+ const diskNames = readDiskRuleFileNames(ctx);
174
+ const ordered = resolveRuleFileOrder(diskNames, getStoredRuleFileOrder(ctx), getActiveFileNames(ctx));
175
+ const next = [];
176
+ const seen = new Set();
177
+ for (const name of ordered) {
178
+ const resolved = name === oldName ? newName : name;
179
+ if (seen.has(resolved))
180
+ continue;
181
+ next.push(resolved);
182
+ seen.add(resolved);
183
+ }
184
+ if (!seen.has(newName))
185
+ next.push(newName);
186
+ setStoredRuleFileOrder(ctx, next);
187
+ }
188
+ function removeRuleFileFromOrder(ctx, name) {
189
+ const ordered = getStoredRuleFileOrder(ctx).filter((item) => item !== name);
190
+ setStoredRuleFileOrder(ctx, ordered);
191
+ }
192
+ function setRuleFileOrder(ctx, names) {
193
+ ensureRulesDir(ctx);
194
+ const diskNames = readDiskRuleFileNames(ctx);
195
+ const diskSet = new Set(diskNames);
196
+ const safeNames = names.map((name) => normalizeRuleFileName(name));
197
+ if (safeNames.length !== diskNames.length
198
+ || new Set(safeNames).size !== safeNames.length
199
+ || safeNames.some((name) => !diskSet.has(name))) {
200
+ throw new Error('规则文件顺序必须包含全部现有文件且无重复');
201
+ }
202
+ const activeNames = getActiveFileNames(ctx);
203
+ const syncedActive = syncActiveRuleFilesToOrder(activeNames, safeNames);
204
+ const settings = loadSettings(ctx);
205
+ settings.ruleFileOrder = safeNames;
206
+ settings.activeRuleFiles = syncedActive;
207
+ saveSettings(ctx, settings);
208
+ ctx.reloadAllRuleFiles();
209
+ return safeNames;
210
+ }
94
211
  function getActiveRuleFileNames(ctx) {
95
212
  return getActiveFileNames(ctx);
96
213
  }
@@ -108,8 +225,9 @@ function writeRuleFileContent(ctx, name, content) {
108
225
  const safeName = normalizeRuleFileName(name);
109
226
  const filePath = ruleFilePath(ctx, safeName);
110
227
  if (!fs.existsSync(filePath)) {
111
- throw new Error(`规则文件 "${safeName}" 不存在`);
228
+ throw ruleFileNotFound(safeName);
112
229
  }
230
+ (0, ruleset_lock_1.assertRuleFileWritable)(ctx, safeName);
113
231
  fs.writeFileSync(filePath, content, 'utf8');
114
232
  ctx.reloadAllRuleFiles();
115
233
  }
@@ -120,12 +238,15 @@ function createRuleFile(ctx, name, content = '', enabled = true) {
120
238
  if (fs.existsSync(filePath)) {
121
239
  throw new Error(`规则文件 "${safeName}" 已存在`);
122
240
  }
241
+ // Drop stale lock entries first so a new file never inherits an old lock.
242
+ (0, ruleset_lock_1.getReadonlyRuleFileNames)(ctx);
123
243
  fs.writeFileSync(filePath, content, 'utf8');
244
+ ensureRuleFileInOrder(ctx, safeName);
124
245
  if (enabled) {
125
246
  const activeNames = getActiveFileNames(ctx);
126
247
  if (!activeNames.includes(safeName)) {
127
- activeNames.push(safeName);
128
- setActiveFileNames(ctx, activeNames);
248
+ const fileOrder = resolveRuleFileOrder(readDiskRuleFileNames(ctx), getStoredRuleFileOrder(ctx), activeNames);
249
+ setActiveFileNames(ctx, syncActiveRuleFilesToOrder([...activeNames, safeName], fileOrder));
129
250
  }
130
251
  }
131
252
  ctx.reloadAllRuleFiles();
@@ -133,6 +254,7 @@ function createRuleFile(ctx, name, content = '', enabled = true) {
133
254
  return {
134
255
  name: safeName,
135
256
  enabled,
257
+ readonly: false,
136
258
  ruleCount: Object.keys(ruleMap).length,
137
259
  excludeCount: Object.values(excludeMap).reduce((total, exclusions) => total + exclusions.length, 0),
138
260
  };
@@ -144,7 +266,44 @@ function setActiveRuleFileNames(ctx, names) {
144
266
  if (missingNames.length > 0) {
145
267
  throw new Error(`规则文件不存在: ${missingNames.join(', ')}`);
146
268
  }
269
+ // A locked ruleset may not be switched on or off.
270
+ const previous = new Set(getActiveFileNames(ctx));
271
+ const next = new Set(safeNames);
272
+ const changed = [...previous].filter((name) => !next.has(name)).concat(safeNames.filter((name) => !previous.has(name)));
273
+ (0, ruleset_lock_1.assertRuleFilesWritable)(ctx, changed);
147
274
  setActiveFileNames(ctx, safeNames);
275
+ // Keep tab order's enabled-file relative positions aligned with activeRuleFiles
276
+ const diskNames = readDiskRuleFileNames(ctx);
277
+ const currentOrder = resolveRuleFileOrder(diskNames, getStoredRuleFileOrder(ctx), safeNames);
278
+ const activeSet = new Set(safeNames);
279
+ let activeIndex = 0;
280
+ const nextOrder = currentOrder.map((name) => {
281
+ if (!activeSet.has(name))
282
+ return name;
283
+ const replacement = safeNames[activeIndex];
284
+ activeIndex += 1;
285
+ return replacement;
286
+ });
287
+ while (activeIndex < safeNames.length) {
288
+ nextOrder.push(safeNames[activeIndex]);
289
+ activeIndex += 1;
290
+ }
291
+ // Deduplicate while preserving first occurrence
292
+ const deduped = [];
293
+ const seen = new Set();
294
+ for (const name of nextOrder) {
295
+ if (seen.has(name))
296
+ continue;
297
+ deduped.push(name);
298
+ seen.add(name);
299
+ }
300
+ for (const name of diskNames) {
301
+ if (!seen.has(name)) {
302
+ deduped.push(name);
303
+ seen.add(name);
304
+ }
305
+ }
306
+ setStoredRuleFileOrder(ctx, deduped);
148
307
  ctx.reloadAllRuleFiles();
149
308
  return safeNames;
150
309
  }
@@ -152,13 +311,20 @@ function setActiveRuleFileNames(ctx, names) {
152
311
  * Scan the route-rules directory and return file info list.
153
312
  */
154
313
  function listRuleFiles(ctx) {
155
- const dir = getRulesDir(ctx);
156
314
  ensureRulesDir(ctx);
157
315
  const activeNames = getActiveFileNames(ctx);
158
- const files = fs.readdirSync(dir)
159
- .filter(f => f.endsWith('.txt'))
160
- .map(f => f.replace(/\.txt$/, ''));
161
- return files.map(name => {
316
+ const files = readDiskRuleFileNames(ctx);
317
+ const orderedNames = resolveRuleFileOrder(files, getStoredRuleFileOrder(ctx), activeNames);
318
+ const readonlyNames = new Set((0, ruleset_lock_1.getReadonlyRuleFileNames)(ctx));
319
+ // Persist seeded order and newly discovered / deleted names so tabs stay stable
320
+ const stored = getStoredRuleFileOrder(ctx);
321
+ const needsPersist = stored.length === 0
322
+ || orderedNames.some((name) => !stored.includes(name))
323
+ || stored.some((name) => !orderedNames.includes(name));
324
+ if (needsPersist) {
325
+ setStoredRuleFileOrder(ctx, orderedNames);
326
+ }
327
+ return orderedNames.map(name => {
162
328
  const filePath = ruleFilePath(ctx, name);
163
329
  let ruleCount = 0;
164
330
  let excludeCount = 0;
@@ -175,11 +341,83 @@ function listRuleFiles(ctx) {
175
341
  return {
176
342
  name,
177
343
  enabled: activeNames.includes(name),
344
+ readonly: readonlyNames.has(name),
178
345
  ruleCount,
179
346
  excludeCount,
180
347
  };
181
348
  });
182
349
  }
350
+ /** Detail of one ruleset (info + raw content). Read-only operation. */
351
+ function getRuleFileDetail(ctx, name) {
352
+ const content = readRuleFileContent(ctx, name);
353
+ const safeName = normalizeRuleFileName(name);
354
+ const info = listRuleFiles(ctx).find((file) => file.name === safeName);
355
+ if (!info)
356
+ throw new Error(`规则文件 "${safeName}" 不存在`);
357
+ return { ...info, content };
358
+ }
359
+ function ruleFileNotFound(name) {
360
+ return Object.assign(new Error(`规则文件 "${name}" 不存在`), { status: 404 });
361
+ }
362
+ /**
363
+ * Enable / disable and / or rename a ruleset. A locked ruleset is rejected
364
+ * before anything changes (the web UI must unlock it first).
365
+ */
366
+ function updateRuleFile(ctx, rawName, changes) {
367
+ const name = normalizeRuleFileName(rawName);
368
+ const filePath = ruleFilePath(ctx, name);
369
+ if (!fs.existsSync(filePath))
370
+ throw ruleFileNotFound(name);
371
+ (0, ruleset_lock_1.assertRuleFileWritable)(ctx, name);
372
+ const { enabled, newName } = changes;
373
+ let currentName = name;
374
+ const activeNames = getActiveFileNames(ctx);
375
+ if (newName && newName.trim() && newName.trim() !== name) {
376
+ const safeName = normalizeRuleFileName(newName);
377
+ const newPath = ruleFilePath(ctx, safeName);
378
+ if (fs.existsSync(newPath)) {
379
+ throw Object.assign(new Error(`规则文件 "${safeName}" 已存在`), { status: 409 });
380
+ }
381
+ // Drop stale lock entries so the new name does not inherit an old lock.
382
+ (0, ruleset_lock_1.getReadonlyRuleFileNames)(ctx);
383
+ fs.renameSync(filePath, newPath);
384
+ const idx = activeNames.indexOf(name);
385
+ if (idx !== -1)
386
+ activeNames[idx] = safeName;
387
+ currentName = safeName;
388
+ renameRuleFileInOrder(ctx, name, safeName);
389
+ }
390
+ if (enabled !== undefined) {
391
+ const idx = activeNames.indexOf(currentName);
392
+ if (enabled && idx === -1) {
393
+ activeNames.push(currentName);
394
+ }
395
+ else if (!enabled && idx !== -1) {
396
+ activeNames.splice(idx, 1);
397
+ }
398
+ }
399
+ const fileOrder = resolveRuleFileOrder(readDiskRuleFileNames(ctx), getStoredRuleFileOrder(ctx), activeNames);
400
+ setActiveFileNames(ctx, syncActiveRuleFilesToOrder(activeNames, fileOrder));
401
+ ctx.reloadAllRuleFiles();
402
+ return { name: currentName };
403
+ }
404
+ /** Delete a ruleset. A locked ruleset is rejected and stays on disk. */
405
+ function deleteRuleFile(ctx, rawName) {
406
+ const name = normalizeRuleFileName(rawName);
407
+ const filePath = ruleFilePath(ctx, name);
408
+ if (!fs.existsSync(filePath))
409
+ throw ruleFileNotFound(name);
410
+ (0, ruleset_lock_1.assertRuleFileWritable)(ctx, name);
411
+ fs.unlinkSync(filePath);
412
+ const activeNames = getActiveFileNames(ctx);
413
+ const idx = activeNames.indexOf(name);
414
+ if (idx !== -1) {
415
+ activeNames.splice(idx, 1);
416
+ setActiveFileNames(ctx, activeNames);
417
+ }
418
+ removeRuleFileFromOrder(ctx, name);
419
+ ctx.reloadAllRuleFiles();
420
+ }
183
421
  /**
184
422
  * 按启用文件顺序合并路由规则;匹配时按 rules 数组顺序逐条验证。
185
423
  */
@@ -303,13 +541,17 @@ function ensureRouteRules(ctx) {
303
541
  fs.writeFileSync(defaultPath, '', 'utf8');
304
542
  }
305
543
  let activeNames = getActiveFileNames(ctx);
544
+ const allFiles = readDiskRuleFileNames(ctx);
306
545
  if (activeNames.length === 0) {
307
- const allFiles = fs.readdirSync(dir).filter(f => f.endsWith('.txt')).map(f => f.replace(/\.txt$/, ''));
308
546
  if (allFiles.length > 0) {
309
547
  activeNames = [allFiles[0]];
310
548
  setActiveFileNames(ctx, activeNames);
311
549
  }
312
550
  }
551
+ const ordered = resolveRuleFileOrder(allFiles, getStoredRuleFileOrder(ctx), activeNames);
552
+ if (getStoredRuleFileOrder(ctx).length === 0 && ordered.length > 0) {
553
+ setStoredRuleFileOrder(ctx, ordered);
554
+ }
313
555
  return activeNames;
314
556
  }
315
557
  function registerRuleFilesRoutes(app, ctx) {
@@ -325,6 +567,26 @@ function registerRuleFilesRoutes(app, ctx) {
325
567
  res.write(JSON.stringify(buildRuleOverview(ctx)));
326
568
  res.end();
327
569
  });
570
+ // PUT /api/rule-files/order - 持久化 Tab / 文件顺序,并同步启用文件合并顺序
571
+ app.put('/api/rule-files/order', (req, res) => {
572
+ res.setHeader('Content-Type', 'application/json');
573
+ try {
574
+ const order = req.body?.order;
575
+ if (!Array.isArray(order) || order.some((name) => typeof name !== 'string')) {
576
+ res.statusCode = 400;
577
+ res.write(JSON.stringify({ error: '缺少有效的 order 数组' }));
578
+ res.end();
579
+ return;
580
+ }
581
+ const nextOrder = setRuleFileOrder(ctx, order);
582
+ res.write(JSON.stringify({ status: 'success', order: nextOrder }));
583
+ }
584
+ catch (err) {
585
+ res.statusCode = 400;
586
+ res.write(JSON.stringify({ error: err.message }));
587
+ }
588
+ res.end();
589
+ });
328
590
  // POST /api/rule-files - 创建新规则文件
329
591
  app.post('/api/rule-files', (req, res) => {
330
592
  res.setHeader('Content-Type', 'application/json');
@@ -336,30 +598,18 @@ function registerRuleFilesRoutes(app, ctx) {
336
598
  res.end();
337
599
  return;
338
600
  }
339
- const safeName = name.trim().replace(/[/\\:*?"<>|]/g, '_');
340
- const filePath = ruleFilePath(ctx, safeName);
341
- if (fs.existsSync(filePath)) {
601
+ const ruleFile = createRuleFile(ctx, name, content, enabled !== false);
602
+ res.write(JSON.stringify({ status: 'success', ruleFile: { name: ruleFile.name, enabled: ruleFile.enabled, ruleCount: ruleFile.ruleCount } }));
603
+ }
604
+ catch (err) {
605
+ const message = err.message;
606
+ if (message.includes('已存在')) {
342
607
  res.statusCode = 409;
343
- res.write(JSON.stringify({ error: `规则文件 "${safeName}" 已存在` }));
344
- res.end();
345
- return;
346
608
  }
347
- ensureRulesDir(ctx);
348
- fs.writeFileSync(filePath, content, 'utf8');
349
- if (enabled) {
350
- const activeNames = getActiveFileNames(ctx);
351
- if (!activeNames.includes(safeName)) {
352
- activeNames.push(safeName);
353
- setActiveFileNames(ctx, activeNames);
354
- }
609
+ else {
610
+ res.statusCode = 500;
355
611
  }
356
- ctx.reloadAllRuleFiles();
357
- const ruleCount = Object.keys((0, helpers_1.parseEprcWithExclusions)(content).ruleMap).length;
358
- res.write(JSON.stringify({ status: 'success', ruleFile: { name: safeName, enabled, ruleCount } }));
359
- }
360
- catch (err) {
361
- res.statusCode = 500;
362
- res.write(JSON.stringify({ error: err.message }));
612
+ res.write(JSON.stringify({ error: message }));
363
613
  }
364
614
  res.end();
365
615
  });
@@ -375,108 +625,139 @@ function registerRuleFilesRoutes(app, ctx) {
375
625
  return;
376
626
  }
377
627
  res.setHeader('Content-Type', 'text/plain; charset=utf-8');
628
+ res.setHeader('X-Meddle-Ruleset-Readonly', (0, ruleset_lock_1.isRuleFileReadonly)(ctx, name) ? 'true' : 'false');
378
629
  res.write(fs.readFileSync(filePath, 'utf8'));
379
630
  res.end();
380
631
  });
381
- // PUT /api/rule-files/:name/content - 保存规则文件内容
382
- app.put('/api/rule-files/:name/content', (req, res) => {
383
- res.setHeader('Content-Type', 'application/json');
632
+ // GET /api/rule-files/:name - 规则集详情(含 readonly 与内容)
633
+ app.get('/api/rule-files/:name', (req, res) => {
384
634
  const name = decodeURIComponent(req.params.name);
385
- const filePath = ruleFilePath(ctx, name);
386
- if (!fs.existsSync(filePath)) {
387
- res.statusCode = 404;
388
- res.write(JSON.stringify({ error: '规则文件不存在' }));
389
- res.end();
635
+ if (!fs.existsSync(ruleFilePath(ctx, name))) {
636
+ sendJson(res, 404, { error: '规则文件不存在' });
390
637
  return;
391
638
  }
392
639
  try {
393
- const text = typeof req.body === 'string' ? req.body : (req.body.content ?? '');
394
- fs.writeFileSync(filePath, text, 'utf8');
395
- ctx.reloadAllRuleFiles();
396
- res.write(JSON.stringify({ status: 'success' }));
640
+ sendJson(res, 200, getRuleFileDetail(ctx, name));
397
641
  }
398
642
  catch (err) {
399
- res.statusCode = 500;
400
- res.write(JSON.stringify({ error: err.message }));
643
+ sendError(res, err);
401
644
  }
402
- res.end();
403
645
  });
404
- // PUT /api/rule-files/:name - 更新规则文件属性(启用/禁用、重命名)
405
- app.put('/api/rule-files/:name', (req, res) => {
406
- res.setHeader('Content-Type', 'application/json');
646
+ // PUT /api/rule-files/:name/content - 保存规则文件内容(锁住时拒绝)
647
+ // A missing or non-string `content` used to fall through to '' and empty the file.
648
+ app.put('/api/rule-files/:name/content', (req, res) => {
407
649
  const name = decodeURIComponent(req.params.name);
408
- const filePath = ruleFilePath(ctx, name);
409
- if (!fs.existsSync(filePath)) {
410
- res.statusCode = 404;
411
- res.write(JSON.stringify({ error: '规则文件不存在' }));
412
- res.end();
650
+ const contentType = String(req.headers['content-type'] || '').toLowerCase();
651
+ if (!contentType.includes('application/json')) {
652
+ sendJson(res, 400, { error: exports.INVALID_RULE_CONTENT_JSON });
653
+ return;
654
+ }
655
+ if (req.body == null || typeof req.body !== 'object' || Array.isArray(req.body) || typeof req.body.content !== 'string') {
656
+ sendJson(res, 400, { error: exports.INVALID_RULE_CONTENT_FIELD });
413
657
  return;
414
658
  }
415
659
  try {
416
- const { enabled, newName } = req.body;
417
- let currentName = name;
418
- const activeNames = getActiveFileNames(ctx);
419
- if (newName && newName.trim() && newName.trim() !== name) {
420
- const safeName = newName.trim().replace(/[/\\:*?"<>|]/g, '_');
421
- const newPath = ruleFilePath(ctx, safeName);
422
- if (fs.existsSync(newPath)) {
423
- res.statusCode = 409;
424
- res.write(JSON.stringify({ error: `规则文件 "${safeName}" 已存在` }));
425
- res.end();
426
- return;
427
- }
428
- fs.renameSync(filePath, newPath);
429
- const idx = activeNames.indexOf(name);
430
- if (idx !== -1)
431
- activeNames[idx] = safeName;
432
- currentName = safeName;
433
- }
434
- if (enabled !== undefined) {
435
- const idx = activeNames.indexOf(currentName);
436
- if (enabled && idx === -1) {
437
- activeNames.push(currentName);
438
- }
439
- else if (!enabled && idx !== -1) {
440
- activeNames.splice(idx, 1);
441
- }
442
- }
443
- setActiveFileNames(ctx, activeNames);
444
- ctx.reloadAllRuleFiles();
445
- res.write(JSON.stringify({ status: 'success', name: currentName }));
660
+ writeRuleFileContent(ctx, name, req.body.content);
661
+ sendJson(res, 200, { status: 'success' });
446
662
  }
447
663
  catch (err) {
448
- res.statusCode = 500;
449
- res.write(JSON.stringify({ error: err.message }));
664
+ sendError(res, err);
450
665
  }
451
- res.end();
452
666
  });
453
- // DELETE /api/rule-files/:name - 删除规则文件
454
- app.delete('/api/rule-files/:name', (req, res) => {
455
- res.setHeader('Content-Type', 'application/json');
456
- const name = decodeURIComponent(req.params.name);
457
- const filePath = ruleFilePath(ctx, name);
458
- if (!fs.existsSync(filePath)) {
459
- res.statusCode = 404;
460
- res.write(JSON.stringify({ error: '规则文件不存在' }));
461
- res.end();
667
+ // PUT /api/rule-files/:name/readonly - 加锁 / 解锁(仅 Web 界面:页面令牌 + 同源)
668
+ app.put('/api/rule-files/:name/readonly', (req, res) => {
669
+ const rawName = decodeURIComponent(req.params.name);
670
+ if (!(0, ruleset_lock_1.isSameOriginRequest)(req)) {
671
+ sendJson(res, 403, { error: '只能在 Meddle 界面中加锁或解锁', code: 'UI_ORIGIN_REQUIRED' });
672
+ return;
673
+ }
674
+ if (!(0, ruleset_lock_1.hasValidUiToken)(req, ctx)) {
675
+ sendJson(res, 403, { error: '界面令牌无效,请刷新 Meddle 页面后重试', code: 'UI_TOKEN_INVALID' });
676
+ return;
677
+ }
678
+ const readonly = req.body?.readonly;
679
+ if (typeof readonly !== 'boolean') {
680
+ sendJson(res, 400, { error: '缺少 readonly 布尔值' });
462
681
  return;
463
682
  }
464
683
  try {
465
- fs.unlinkSync(filePath);
466
- const activeNames = getActiveFileNames(ctx);
467
- const idx = activeNames.indexOf(name);
468
- if (idx !== -1) {
469
- activeNames.splice(idx, 1);
470
- setActiveFileNames(ctx, activeNames);
471
- }
472
- ctx.reloadAllRuleFiles();
473
- res.write(JSON.stringify({ status: 'success' }));
684
+ const name = normalizeRuleFileName(rawName);
685
+ if (!fs.existsSync(ruleFilePath(ctx, name)))
686
+ throw ruleFileNotFound(name);
687
+ (0, ruleset_lock_1.setRuleFileReadonly)(ctx, name, readonly);
688
+ sendJson(res, 200, { status: 'success', name, readonly });
474
689
  }
475
690
  catch (err) {
476
- res.statusCode = 500;
477
- res.write(JSON.stringify({ error: err.message }));
691
+ sendError(res, err);
478
692
  }
479
- res.end();
693
+ });
694
+ // PUT /api/rule-files/:name - 更新规则文件属性(启用/禁用、重命名;锁住时拒绝)
695
+ app.put('/api/rule-files/:name', (req, res) => {
696
+ const name = decodeURIComponent(req.params.name);
697
+ try {
698
+ const { enabled, newName } = req.body || {};
699
+ const result = updateRuleFile(ctx, name, { enabled, newName });
700
+ sendJson(res, 200, { status: 'success', name: result.name });
701
+ }
702
+ catch (err) {
703
+ sendError(res, err);
704
+ }
705
+ });
706
+ // DELETE /api/rule-files/:name - 删除规则文件(锁住时拒绝)
707
+ app.delete('/api/rule-files/:name', (req, res) => {
708
+ const name = decodeURIComponent(req.params.name);
709
+ try {
710
+ deleteRuleFile(ctx, name);
711
+ sendJson(res, 200, { status: 'success' });
712
+ }
713
+ catch (err) {
714
+ sendError(res, err);
715
+ }
716
+ });
717
+ // express.json() rejects invalid JSON before this route runs and would
718
+ // otherwise answer with an HTML 400. A non-JSON content type never reaches
719
+ // that parser, so the route itself rejects it before any write.
720
+ app.use((err, req, res, next) => {
721
+ if (isRuleFileContentPut(req) && (0, json_parse_error_1.isJsonParseFailure)(err)) {
722
+ if (res.headersSent) {
723
+ next(err);
724
+ return;
725
+ }
726
+ sendJson(res, 400, { error: exports.INVALID_RULE_CONTENT_JSON });
727
+ return;
728
+ }
729
+ next(err);
730
+ });
731
+ }
732
+ function isRuleFileContentPut(req) {
733
+ if (req.method !== 'PUT')
734
+ return false;
735
+ const pathOnly = String(req.path || req.url || '').split('?')[0];
736
+ return /^\/api\/rule-files\/[^/]+\/content$/.test(pathOnly);
737
+ }
738
+ function sendJson(res, status, body) {
739
+ res.statusCode = status;
740
+ res.setHeader('Content-Type', 'application/json');
741
+ res.write(JSON.stringify(body));
742
+ res.end();
743
+ }
744
+ function sendError(res, err) {
745
+ if ((0, ruleset_lock_1.isRulesetReadonlyError)(err)) {
746
+ sendJson(res, ruleset_lock_1.RULESET_READONLY_STATUS, (0, ruleset_lock_1.rulesetReadonlyBody)(err));
747
+ return;
748
+ }
749
+ const status = typeof err?.status === 'number' ? err.status : 500;
750
+ sendJson(res, status, { error: err.message });
751
+ }
752
+ /** Page bootstrap for the UI token (fallback when index.html was not served by Meddle, e.g. Vite dev). */
753
+ function registerUiTokenRoute(app, ctx) {
754
+ app.get('/api/ui-token', (req, res) => {
755
+ res.setHeader('Cache-Control', 'no-store');
756
+ if (!(0, ruleset_lock_1.isSameOriginRequest)(req)) {
757
+ sendJson(res, 403, { error: '只能在 Meddle 界面中获取', code: 'UI_ORIGIN_REQUIRED' });
758
+ return;
759
+ }
760
+ sendJson(res, 200, { token: (0, ruleset_lock_1.getUiToken)(ctx) });
480
761
  });
481
762
  }
482
763
  //# sourceMappingURL=rule-files.js.map