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