@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.
- package/CONFIG_STRUCTURE.md +9 -2
- package/README.md +18 -0
- package/bin/commands/mock/add.js +59 -21
- package/bin/commands/mock/delete.js +16 -24
- package/bin/commands/mock/index.js +59 -25
- package/bin/commands/mock/list.js +35 -18
- package/bin/commands/mock/toggle.js +17 -27
- package/bin/commands/mock/update.js +39 -30
- package/bin/commands/route/active.js +20 -3
- package/bin/commands/route/add.js +113 -94
- package/bin/commands/route/create.js +8 -1
- package/bin/commands/route/delete.js +87 -53
- package/bin/commands/route/index.js +74 -26
- package/bin/commands/route/list.js +2 -2
- package/bin/commands/route/preview.js +2 -2
- package/bin/commands/route/show.js +12 -3
- package/bin/commands/route/update.js +135 -92
- package/bin/commands/status.js +27 -11
- package/bin/commands/update.js +4 -0
- package/bin/doctor.js +8 -4
- package/bin/index.js +11 -7
- package/bin/lib/api-client.js +14 -2
- package/bin/lib/file-access.js +234 -35
- package/bin/lib/meddle-home.js +14 -1
- package/bin/lib/output.js +30 -4
- package/bin/lib/proxy-detect.js +99 -20
- package/bin/lib/proxy-or-file.js +93 -0
- package/bin/lib/route-write-args.js +150 -0
- package/bin/lib/rule-file-edit.js +263 -0
- package/bin/lib/ruleset-readonly.js +61 -0
- package/bin/lib/update-check.js +67 -6
- package/dist/cert.d.ts +2 -0
- package/dist/cert.d.ts.map +1 -1
- package/dist/cert.js +53 -1
- package/dist/cert.js.map +1 -1
- package/dist/core/browser.d.ts +10 -1
- package/dist/core/browser.d.ts.map +1 -1
- package/dist/core/browser.js +252 -36
- package/dist/core/browser.js.map +1 -1
- package/dist/core/config-diagnostics.d.ts.map +1 -1
- package/dist/core/config-diagnostics.js +6 -1
- package/dist/core/config-diagnostics.js.map +1 -1
- package/dist/core/custom-plugin-loader.d.ts.map +1 -1
- package/dist/core/custom-plugin-loader.js +2 -0
- package/dist/core/custom-plugin-loader.js.map +1 -1
- package/dist/core/mock-handler.d.ts +63 -0
- package/dist/core/mock-handler.d.ts.map +1 -1
- package/dist/core/mock-handler.js +641 -29
- package/dist/core/mock-handler.js.map +1 -1
- package/dist/core/mock-utils.d.ts +58 -0
- package/dist/core/mock-utils.d.ts.map +1 -0
- package/dist/core/mock-utils.js +167 -0
- package/dist/core/mock-utils.js.map +1 -0
- package/dist/core/plugin-bootstrap-runner.d.ts +1 -0
- package/dist/core/plugin-bootstrap-runner.d.ts.map +1 -1
- package/dist/core/plugin-bootstrap-runner.js +37 -1
- package/dist/core/plugin-bootstrap-runner.js.map +1 -1
- package/dist/core/plugin-source.d.ts +24 -0
- package/dist/core/plugin-source.d.ts.map +1 -0
- package/dist/core/plugin-source.js +47 -0
- package/dist/core/plugin-source.js.map +1 -0
- package/dist/core/proxy-record.d.ts +14 -0
- package/dist/core/proxy-record.d.ts.map +1 -1
- package/dist/core/proxy-record.js +39 -0
- package/dist/core/proxy-record.js.map +1 -1
- package/dist/core/rule-file-edit.d.ts +81 -0
- package/dist/core/rule-file-edit.d.ts.map +1 -0
- package/dist/core/rule-file-edit.js +243 -0
- package/dist/core/rule-file-edit.js.map +1 -0
- package/dist/core/static-server.d.ts.map +1 -1
- package/dist/core/static-server.js +15 -13
- package/dist/core/static-server.js.map +1 -1
- package/dist/core/types.d.ts +15 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/plugins/builtin/mock-plugin.d.ts.map +1 -1
- package/dist/plugins/builtin/mock-plugin.js +6 -5
- package/dist/plugins/builtin/mock-plugin.js.map +1 -1
- package/dist/server/agent/tools.d.ts.map +1 -1
- package/dist/server/agent/tools.js +24 -18
- package/dist/server/agent/tools.js.map +1 -1
- package/dist/server/app-info.d.ts +33 -0
- package/dist/server/app-info.d.ts.map +1 -0
- package/dist/server/app-info.js +116 -0
- package/dist/server/app-info.js.map +1 -0
- package/dist/server/config.d.ts +1 -0
- package/dist/server/config.d.ts.map +1 -1
- package/dist/server/config.js +114 -16
- package/dist/server/config.js.map +1 -1
- package/dist/server/health.d.ts.map +1 -1
- package/dist/server/health.js +8 -0
- package/dist/server/health.js.map +1 -1
- package/dist/server/index.d.ts +16 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +10 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/json-parse-error.d.ts +7 -0
- package/dist/server/json-parse-error.d.ts.map +1 -0
- package/dist/server/json-parse-error.js +28 -0
- package/dist/server/json-parse-error.js.map +1 -0
- package/dist/server/mocks.d.ts +26 -0
- package/dist/server/mocks.d.ts.map +1 -1
- package/dist/server/mocks.js +110 -13
- package/dist/server/mocks.js.map +1 -1
- package/dist/server/plugins.d.ts.map +1 -1
- package/dist/server/plugins.js +56 -11
- package/dist/server/plugins.js.map +1 -1
- package/dist/server/rule-files.d.ts +36 -0
- package/dist/server/rule-files.d.ts.map +1 -1
- package/dist/server/rule-files.js +388 -107
- package/dist/server/rule-files.js.map +1 -1
- package/dist/server/ruleset-lock.d.ts +62 -0
- package/dist/server/ruleset-lock.d.ts.map +1 -0
- package/dist/server/ruleset-lock.js +234 -0
- package/dist/server/ruleset-lock.js.map +1 -0
- package/index.js +128 -16
- package/mcp-server.js +108 -93
- package/package.json +1 -1
- package/plugins/builtin/mock-plugin.ts +9 -5
- package/web/dist/assets/alert-B514Frte.js +1 -0
- package/web/dist/assets/arrow-right-BNum_060.js +1 -0
- package/web/dist/assets/{body-diff-view-B-4XWmL6.js → body-diff-view-SfLCLzs9.js} +2 -2
- package/web/dist/assets/button-BFNlGZ5p.js +1 -0
- package/web/dist/assets/check-BkfHQ6re.js +1 -0
- package/web/dist/assets/checkbox-CQyFsZzS.js +1 -0
- package/web/dist/assets/chevron-down-DHwUtL4O.js +1 -0
- package/web/dist/assets/{circle-alert-Q8cZk9Rv.js → circle-alert-zMagrFuI.js} +1 -1
- package/web/dist/assets/{circle-x-C9USji_R.js → circle-x-BEkinhez.js} +1 -1
- package/web/dist/assets/code-DD-AYxxu.js +1 -0
- package/web/dist/assets/{copy-BYjq1hdN.js → copy-BRnscYy1.js} +1 -1
- package/web/dist/assets/detail-panel-DZRKJG2W.js +4 -0
- package/web/dist/assets/dist-BQN0rzUS.js +1 -0
- package/web/dist/assets/{dist-CEd_wnRC.js → dist-C-E2R2jk.js} +1 -1
- package/web/dist/assets/{dist-C6eaYCUo.js → dist-CFZ4xMSF.js} +1 -1
- package/web/dist/assets/{dist-DWmDaXVN.js → dist-CRmX_NvH.js} +1 -1
- package/web/dist/assets/{dist-C8RHq5At.js → dist-DLQKRjuW.js} +1 -1
- package/web/dist/assets/{dist-Blyulj4_.js → dist-LiAcI8CK.js} +1 -1
- package/web/dist/assets/{dist-Ce4ujolc.js → dist-fzPL-8F9.js} +1 -1
- package/web/dist/assets/{dist-DoE7uybN.js → dist-wv9ebJK_.js} +1 -1
- package/web/dist/assets/editor-pane-actions-_ckwcCII.js +1 -0
- package/web/dist/assets/{es2015-DG46NGN7.js → es2015-DPJTBYa9.js} +1 -1
- package/web/dist/assets/global-panel-shell-Iir-c_pQ.js +6 -0
- package/web/dist/assets/health-panel-Dmf-EhJz.js +1 -0
- package/web/dist/assets/index-C4dIj1nT.js +21 -0
- package/web/dist/assets/index-CdYMl1M7.css +2 -0
- package/web/dist/assets/{mobile-proxy-panel-tU0NyJ3P.js → mobile-proxy-panel-CFcwa11A.js} +6 -6
- package/web/dist/assets/mock-config-CGl50MIr.js +1 -0
- package/web/dist/assets/mock-editor-panel-xcCk23oH.js +20 -0
- package/web/dist/assets/mocks-file-missing-note-CW_ONVLA.js +1 -0
- package/web/dist/assets/monaco-editor-BkP7envQ.js +11 -0
- package/web/dist/assets/pane-bar-5j9Nf7W_.js +1 -0
- package/web/dist/assets/pane-split-C0OSe365.js +1 -0
- package/web/dist/assets/panel-heading-izT5q0ZN.js +1 -0
- package/web/dist/assets/{pencil-line-Cqe41E4h.js → pencil-line-CEZ7D-de.js} +1 -1
- package/web/dist/assets/plugin-code-editor-DKE9dFEE.js +2 -0
- package/web/dist/assets/plugin-config-Cg09TsuV.js +2 -0
- package/web/dist/assets/plugin-generator-CT1f1tu0.js +2 -0
- package/web/dist/assets/plugin-test-dialog-BbLIMnkJ.js +7 -0
- package/web/dist/assets/plus-ixZQRprm.js +1 -0
- package/web/dist/assets/{refresh-cw-CG3HaiQT.js → refresh-cw-BfLoHqg5.js} +1 -1
- package/web/dist/assets/rotate-ccw-CRguJya4.js +1 -0
- package/web/dist/assets/route-preview-CKO7nF4W.js +1 -0
- package/web/dist/assets/{rule-ai-assistant-panel-DMh0F9dr.js → rule-ai-assistant-panel-CtfUqdYG.js} +2 -2
- package/web/dist/assets/rule-config-Cb4ejIXI.js +15 -0
- package/web/dist/assets/rule-overview-panel-xtHTS27O.js +1 -0
- package/web/dist/assets/{server-Cpj4KNwN.js → server-eYXq3PbE.js} +1 -1
- package/web/dist/assets/settings-panel-CpQuE6nr.js +1 -0
- package/web/dist/assets/{shield-check-OqUmVLQt.js → shield-check-hQXjdfZ7.js} +1 -1
- package/web/dist/assets/smartphone-D4Q7cQfm.js +1 -0
- package/web/dist/assets/switch-94vgsMHm.js +1 -0
- package/web/dist/assets/{syntax-highlight-Dk0-ewLI.js → syntax-highlight-BFgN_C0Y.js} +1 -1
- package/web/dist/assets/{table-2-BR5e3z8m.js → table-2-DvGHhMm4.js} +1 -1
- package/web/dist/assets/table-DaXD6jJv.js +1 -0
- package/web/dist/assets/textarea-9wB8Hsz-.js +1 -0
- package/web/dist/assets/toast-9yeTf7d9.js +1 -0
- package/web/dist/assets/use-mocks-file-status-BUIUJ4oQ.js +1 -0
- package/web/dist/assets/{wand-sparkles-CJ7fSnom.js → wand-sparkles-Bmt4dDOf.js} +1 -1
- package/web/dist/assets/{wifi-CCLKyySQ.js → wifi-Cjj_Br1u.js} +1 -1
- package/web/dist/assets/{zap-DR-xiUMe.js → zap-BuOtogAv.js} +1 -1
- package/web/dist/index.html +16 -10
- package/web/dist/assets/alert-DCvlZAiv.js +0 -1
- package/web/dist/assets/arrow-right-B4Mrlj2-.js +0 -1
- package/web/dist/assets/button-BTToMaBW.js +0 -1
- package/web/dist/assets/checkbox-D1Z9DtjF.js +0 -1
- package/web/dist/assets/chevron-down-CPngAx4M.js +0 -1
- package/web/dist/assets/chevron-right-V1DGIDbL.js +0 -1
- package/web/dist/assets/code-5_eJ_XpJ.js +0 -1
- package/web/dist/assets/code-fixer-D48sCDtp.js +0 -20
- package/web/dist/assets/detail-panel-BSFz9CUw.js +0 -4
- package/web/dist/assets/file-system-access-_og-Tg9o.js +0 -1
- package/web/dist/assets/global-panel-shell-5cJneeis.js +0 -6
- package/web/dist/assets/health-panel-BS3L2e5d.js +0 -1
- package/web/dist/assets/index-CvTR1IVi.js +0 -18
- package/web/dist/assets/index-bfV7sxx5.css +0 -2
- package/web/dist/assets/mock-config-CULJP6Vt.js +0 -1
- package/web/dist/assets/mock-editor-panel-9wlDaySA.js +0 -1
- package/web/dist/assets/monaco-editor-BttaUoOz.js +0 -11
- package/web/dist/assets/plugin-code-editor-BAIBp4lg.js +0 -2
- package/web/dist/assets/plugin-config-ButUV2SZ.js +0 -1
- package/web/dist/assets/plugin-generator-uceUPSCv.js +0 -2
- package/web/dist/assets/plugin-test-dialog-BrJ4HfRX.js +0 -7
- package/web/dist/assets/plus-tVxSydJ8.js +0 -1
- package/web/dist/assets/rotate-ccw-CAYM6Vv0.js +0 -1
- package/web/dist/assets/route-preview-C4cGrS1-.js +0 -1
- package/web/dist/assets/rule-config-BTTGNwMs.js +0 -15
- package/web/dist/assets/rule-overview-panel-SFR2OloL.js +0 -1
- package/web/dist/assets/select-aBXQ0zNa.js +0 -1
- package/web/dist/assets/settings-panel-BMReDG8b.js +0 -1
- package/web/dist/assets/smartphone-DKeDuJTU.js +0 -1
- package/web/dist/assets/switch-Bes_YiJu.js +0 -1
- package/web/dist/assets/table-nW7uQVcl.js +0 -1
- package/web/dist/assets/textarea-BSI0rnW6.js +0 -1
- /package/web/dist/assets/{babel-DCr4VA1c.js → babel-bwZm0qi3.js} +0 -0
- /package/web/dist/assets/{clipboard-VhPXNN5m.js → clipboard-Bt1qY01V.js} +0 -0
- /package/web/dist/assets/{estree-qO_m2Qaw.js → estree-MatbZDwq.js} +0 -0
- /package/web/dist/assets/{html-VDGahtyk.js → html-CCrsbFYX.js} +0 -0
- /package/web/dist/assets/{postcss-BUm8EKMw.js → postcss-BxN7Gr8n.js} +0 -0
- /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
|
|
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
|
-
|
|
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 =
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
|
|
348
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
382
|
-
app.
|
|
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
|
-
|
|
386
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
406
|
-
|
|
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
|
|
409
|
-
if (!
|
|
410
|
-
res.
|
|
411
|
-
|
|
412
|
-
|
|
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
|
-
|
|
417
|
-
|
|
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
|
|
449
|
-
res.write(JSON.stringify({ error: err.message }));
|
|
664
|
+
sendError(res, err);
|
|
450
665
|
}
|
|
451
|
-
res.end();
|
|
452
666
|
});
|
|
453
|
-
//
|
|
454
|
-
app.
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
res
|
|
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
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
|
477
|
-
res.write(JSON.stringify({ error: err.message }));
|
|
691
|
+
sendError(res, err);
|
|
478
692
|
}
|
|
479
|
-
|
|
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
|