@agile-team/wl-skills-kit 2.4.2 → 2.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/CHANGELOG.md +37 -1
- package/README.md +65 -44
- package/bin/wl-skills.js +196 -12
- package/files/.github/copilot-instructions.md +361 -322
- package/files/.github/guides/architecture.md +1 -1
- package/files/.github/guides/usage.md +32 -10
- package/files/.github/skills/_registry.md +18 -16
- package/files/.github/skills/core/page-codegen/SKILL.md +149 -74
- package/files/.github/skills/core/page-codegen/USAGE.md +33 -12
- package/files/.github/skills/core/page-codegen/templates/universal/TPL-DETAIL-TABS.md +80 -48
- package/files/.github/skills/core/page-codegen/templates/universal/TPL-FORM-ROUTE.md +183 -55
- package/files/.github/skills/core/page-codegen/templates/universal/TPL-LIST.md +110 -21
- package/files/.github/skills/core/page-codegen/templates/universal/TPL-MASTER-DETAIL.md +29 -9
- package/files/.github/skills/core/page-codegen/templates/universal/TPL-RECORD-FORM.md +93 -48
- package/files/.github/skills/core/page-codegen/templates/universal/TPL-TREE-LIST.md +49 -29
- package/files/.github/skills/sync/menu-sync/SKILL.md +27 -13
- package/mcp/server.js +279 -195
- package/mcp/tools/menuSync.js +416 -96
- package/mcp/tools/projectTools.js +336 -124
- package/package.json +2 -2
package/mcp/tools/menuSync.js
CHANGED
|
@@ -1,96 +1,416 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { queryMenuTree, saveMenu } = require("../api/menuApi");
|
|
6
|
+
|
|
7
|
+
function getProjectRoot() {
|
|
8
|
+
return process.env.WL_PROJECT_ROOT
|
|
9
|
+
? path.resolve(process.env.WL_PROJECT_ROOT)
|
|
10
|
+
: process.cwd();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function cleanCell(value) {
|
|
14
|
+
return String(value || "")
|
|
15
|
+
.replace(/^`|`$/g, "")
|
|
16
|
+
.replace(/\*\*/g, "")
|
|
17
|
+
.trim();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function splitMarkdownRow(line) {
|
|
21
|
+
return line
|
|
22
|
+
.trim()
|
|
23
|
+
.replace(/^\|/, "")
|
|
24
|
+
.replace(/\|$/, "")
|
|
25
|
+
.split("|")
|
|
26
|
+
.map(cleanCell);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isDividerRow(line) {
|
|
30
|
+
return /^\|\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)+\|?$/.test(line.trim());
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parseBoolean(value) {
|
|
34
|
+
const text = cleanCell(value).toLowerCase();
|
|
35
|
+
return ["true", "yes", "y", "1", "是", "隐藏", "hidden"].includes(text);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeTree(data) {
|
|
39
|
+
if (Array.isArray(data)) return data;
|
|
40
|
+
if (Array.isArray(data && data.records)) return data.records;
|
|
41
|
+
if (Array.isArray(data && data.list)) return data.list;
|
|
42
|
+
if (Array.isArray(data && data.children)) return data.children;
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function flattenMenus(tree, parentId, result) {
|
|
47
|
+
for (const node of normalizeTree(tree)) {
|
|
48
|
+
result.push({ ...node, parentId: node.parentId || parentId });
|
|
49
|
+
flattenMenus(
|
|
50
|
+
node.children || node.childList || node.childrenList || [],
|
|
51
|
+
node.id,
|
|
52
|
+
result,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function findExisting(flatMenus, item) {
|
|
59
|
+
return flatMenus.find((menu) => {
|
|
60
|
+
const sameParent =
|
|
61
|
+
!item.parentId || String(menu.parentId || "") === String(item.parentId);
|
|
62
|
+
const samePath =
|
|
63
|
+
item.path && menu.path && String(menu.path) === String(item.path);
|
|
64
|
+
const sameName =
|
|
65
|
+
item.menuName &&
|
|
66
|
+
menu.menuName &&
|
|
67
|
+
String(menu.menuName) === String(item.menuName);
|
|
68
|
+
return sameParent && (samePath || sameName);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function findLatestReport(root) {
|
|
73
|
+
const reportsDir = path.join(root, ".github", "reports");
|
|
74
|
+
if (!fs.existsSync(reportsDir)) return null;
|
|
75
|
+
return fs
|
|
76
|
+
.readdirSync(reportsDir)
|
|
77
|
+
.filter((name) => /^SYS_MENU_INFO.*\.md$/.test(name))
|
|
78
|
+
.map((name) => path.join(reportsDir, name))
|
|
79
|
+
.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs)[0];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function resolveReportPath(inputPath) {
|
|
83
|
+
const root = getProjectRoot();
|
|
84
|
+
if (inputPath) {
|
|
85
|
+
const full = path.isAbsolute(inputPath)
|
|
86
|
+
? inputPath
|
|
87
|
+
: path.join(root, inputPath);
|
|
88
|
+
return fs.existsSync(full) ? full : null;
|
|
89
|
+
}
|
|
90
|
+
return findLatestReport(root);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function parseReport(content) {
|
|
94
|
+
const lines = content.split(/\r?\n/);
|
|
95
|
+
const dirs = [];
|
|
96
|
+
const pages = [];
|
|
97
|
+
let section = "";
|
|
98
|
+
let currentDirName = "";
|
|
99
|
+
let header = null;
|
|
100
|
+
|
|
101
|
+
for (const line of lines) {
|
|
102
|
+
if (/^##\s+.*一级目录/.test(line)) {
|
|
103
|
+
section = "dir";
|
|
104
|
+
header = null;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (/^##\s+.*二级菜单/.test(line)) {
|
|
108
|
+
section = "page";
|
|
109
|
+
header = null;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const subMenu = line.match(/^###\s+\d+\.\s+(.+?)\s*子菜单/);
|
|
113
|
+
if (subMenu) currentDirName = cleanCell(subMenu[1]);
|
|
114
|
+
if (!line.trim().startsWith("|") || isDividerRow(line)) continue;
|
|
115
|
+
const cells = splitMarkdownRow(line);
|
|
116
|
+
if (!header) {
|
|
117
|
+
header = cells;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const row = {};
|
|
121
|
+
header.forEach((key, index) => {
|
|
122
|
+
row[key] = cells[index] || "";
|
|
123
|
+
});
|
|
124
|
+
if (section === "dir") {
|
|
125
|
+
const menuName = row["菜单名"] || row.menuName;
|
|
126
|
+
const menuPath = row.path || row["菜单路径"];
|
|
127
|
+
if (
|
|
128
|
+
menuName &&
|
|
129
|
+
menuPath &&
|
|
130
|
+
!menuName.includes("目录名") &&
|
|
131
|
+
!menuPath.includes("目录path")
|
|
132
|
+
) {
|
|
133
|
+
dirs.push({
|
|
134
|
+
type: "M",
|
|
135
|
+
menuName,
|
|
136
|
+
path: menuPath,
|
|
137
|
+
orderNum: Number(row.orderNum || row["显示排序"] || dirs.length + 1),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (section === "page") {
|
|
142
|
+
const menuName = row["菜单名"] || row.menuName;
|
|
143
|
+
const menuPath = row.path || row["菜单路径"];
|
|
144
|
+
const component = row.component || row["组件路径"];
|
|
145
|
+
const permission = row.permission || row["权限标识"];
|
|
146
|
+
if (menuName && menuPath && component && !menuName.includes("页面名称")) {
|
|
147
|
+
pages.push({
|
|
148
|
+
type: "C",
|
|
149
|
+
menuName,
|
|
150
|
+
path: menuPath,
|
|
151
|
+
component,
|
|
152
|
+
permission: permission || "",
|
|
153
|
+
hidden: parseBoolean(row.hidden || row["是否隐藏"]),
|
|
154
|
+
parentMenuName: currentDirName,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return { dirs, pages };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function buildMenuBody(item, config, parentId, parentMenuNameCode, orderNum) {
|
|
164
|
+
const codePrefix = parentMenuNameCode ? `${parentMenuNameCode}:` : "";
|
|
165
|
+
return {
|
|
166
|
+
useCache: 1,
|
|
167
|
+
icon: item.icon || "list",
|
|
168
|
+
common: 2,
|
|
169
|
+
hidden: Boolean(item.hidden),
|
|
170
|
+
editMode: false,
|
|
171
|
+
type: item.type,
|
|
172
|
+
parentId,
|
|
173
|
+
sysAppNo: config.sysAppNo,
|
|
174
|
+
orderNum,
|
|
175
|
+
menuName: item.menuName,
|
|
176
|
+
menuNameCode: item.menuNameCode || `${codePrefix}${item.path}`,
|
|
177
|
+
path: item.path,
|
|
178
|
+
component: item.type === "C" ? item.component : undefined,
|
|
179
|
+
permission: item.type === "C" ? item.permission : undefined,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* wls_menu_query 工具处理器
|
|
185
|
+
* 自动从 config.menu.domainId 读取应用域 ID,查询完整菜单树
|
|
186
|
+
*
|
|
187
|
+
* @param {{ menu: { domainId?: string } }} config
|
|
188
|
+
* @returns {Promise<string>} 返回给 AI 的文本内容
|
|
189
|
+
*/
|
|
190
|
+
async function handleMenuQuery(config) {
|
|
191
|
+
const domainId = config.menu && config.menu.domainId;
|
|
192
|
+
|
|
193
|
+
if (!domainId || domainId.toString().includes("domainId")) {
|
|
194
|
+
return [
|
|
195
|
+
"❌ 请在 env.local.json 的 menu.domainId 字段填写真实的应用域 ID",
|
|
196
|
+
"",
|
|
197
|
+
"获取方式:打开菜单管理后台,在 Network 面板找到类似",
|
|
198
|
+
" getMenuTreeByDomainId?domainId=1777597797627056130",
|
|
199
|
+
"中的数字即为 domainId,填入 env.local.json:",
|
|
200
|
+
' "menu": { "domainId": "1777597797627056130", ... }',
|
|
201
|
+
].join("\n");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const result = await queryMenuTree(domainId, config);
|
|
205
|
+
|
|
206
|
+
if (!result.ok) {
|
|
207
|
+
return `❌ 查询菜单树失败: ${result.error} (code: ${result.code})`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const tree = result.data;
|
|
211
|
+
const isEmpty = !tree || (Array.isArray(tree) && tree.length === 0);
|
|
212
|
+
if (isEmpty) {
|
|
213
|
+
return `✅ 菜单树查询成功,当前应用域(domainId=${domainId})暂无菜单数据`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return `✅ 菜单树查询成功(domainId=${domainId})\n\n${JSON.stringify(tree, null, 2)}`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* wls_menu_upsert 工具处理器
|
|
221
|
+
* 批量新增(无 id)或更新(有 id)菜单项
|
|
222
|
+
* 新增时从响应 data 取服务端生成的 id,可用于链式创建子菜单
|
|
223
|
+
*
|
|
224
|
+
* @param {{ items: object[] }} args
|
|
225
|
+
* @param {object} config
|
|
226
|
+
* @returns {Promise<string>}
|
|
227
|
+
*/
|
|
228
|
+
async function handleMenuUpsert(args, config) {
|
|
229
|
+
const { items } = args;
|
|
230
|
+
|
|
231
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
232
|
+
return "❌ 参数错误:items 必须是非空数组";
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const results = [];
|
|
236
|
+
|
|
237
|
+
for (const item of items) {
|
|
238
|
+
const isUpdate = Boolean(item.id);
|
|
239
|
+
const action = isUpdate ? "更新" : "新增";
|
|
240
|
+
|
|
241
|
+
const result = await saveMenu(item, config);
|
|
242
|
+
|
|
243
|
+
if (result.ok) {
|
|
244
|
+
const saved = result.data;
|
|
245
|
+
results.push({
|
|
246
|
+
action,
|
|
247
|
+
menuName: item.menuName || "(未命名)",
|
|
248
|
+
id: saved ? saved.id : item.id,
|
|
249
|
+
status: "✅ 成功",
|
|
250
|
+
});
|
|
251
|
+
} else {
|
|
252
|
+
results.push({
|
|
253
|
+
action,
|
|
254
|
+
menuName: item.menuName || "(未命名)",
|
|
255
|
+
id: item.id || "(新增)",
|
|
256
|
+
status: `❌ 失败: ${result.error}`,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const successCount = results.filter((r) => r.status.startsWith("✅")).length;
|
|
262
|
+
const failCount = results.length - successCount;
|
|
263
|
+
|
|
264
|
+
let output = `菜单操作完成:成功 ${successCount} 条,失败 ${failCount} 条\n\n`;
|
|
265
|
+
output += "| 操作 | 菜单名 | ID | 状态 |\n";
|
|
266
|
+
output += "|---|---|---|---|\n";
|
|
267
|
+
for (const r of results) {
|
|
268
|
+
output += `| ${r.action} | ${r.menuName} | ${r.id} | ${r.status} |\n`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return output;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function handleMenuSyncFromReport(args, config) {
|
|
275
|
+
const domainId = config.menu && config.menu.domainId;
|
|
276
|
+
const rootParentId =
|
|
277
|
+
(config.menu && config.menu.parentMenuId) || config.parentMenuId;
|
|
278
|
+
if (!domainId || domainId.toString().includes("domainId")) {
|
|
279
|
+
return "❌ 请先在 .github/skills/sync/env.local.json 填写 menu.domainId";
|
|
280
|
+
}
|
|
281
|
+
if (!rootParentId || rootParentId.toString().includes("parentMenuId")) {
|
|
282
|
+
return "❌ 请先在 .github/skills/sync/env.local.json 填写 menu.parentMenuId";
|
|
283
|
+
}
|
|
284
|
+
if (!config.sysAppNo)
|
|
285
|
+
return "❌ 请先在 .github/skills/sync/env.local.json 填写 sysAppNo";
|
|
286
|
+
|
|
287
|
+
const reportPath = resolveReportPath(args && args.reportPath);
|
|
288
|
+
if (!reportPath)
|
|
289
|
+
return "❌ 未找到 SYS_MENU_INFO*.md,请传 reportPath 或先生成 .github/reports/SYS_MENU_INFO.md";
|
|
290
|
+
|
|
291
|
+
const parsed = parseReport(fs.readFileSync(reportPath, "utf8"));
|
|
292
|
+
if (parsed.dirs.length === 0 && parsed.pages.length === 0) {
|
|
293
|
+
return `❌ 未从报告解析到菜单数据:${path.relative(getProjectRoot(), reportPath)}`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const query = await queryMenuTree(domainId, config);
|
|
297
|
+
if (!query.ok)
|
|
298
|
+
return `❌ 查询菜单树失败: ${query.error} (code: ${query.code})`;
|
|
299
|
+
|
|
300
|
+
const flatMenus = flattenMenus(query.data, null, []);
|
|
301
|
+
const parentNode =
|
|
302
|
+
flatMenus.find((item) => String(item.id) === String(rootParentId)) || {};
|
|
303
|
+
const parentMenuNameCode =
|
|
304
|
+
parentNode.menuNameCode || parentNode.nameCode || "";
|
|
305
|
+
const resultRows = [];
|
|
306
|
+
const dirIdMap = new Map();
|
|
307
|
+
const dryRun = Boolean(args && args.dryRun);
|
|
308
|
+
|
|
309
|
+
for (let index = 0; index < parsed.dirs.length; index += 1) {
|
|
310
|
+
const dir = parsed.dirs[index];
|
|
311
|
+
const existing = findExisting(flatMenus, {
|
|
312
|
+
...dir,
|
|
313
|
+
parentId: rootParentId,
|
|
314
|
+
});
|
|
315
|
+
const body = buildMenuBody(
|
|
316
|
+
dir,
|
|
317
|
+
config,
|
|
318
|
+
rootParentId,
|
|
319
|
+
parentMenuNameCode,
|
|
320
|
+
dir.orderNum || index + 1,
|
|
321
|
+
);
|
|
322
|
+
if (existing) body.id = existing.id;
|
|
323
|
+
if (dryRun) {
|
|
324
|
+
dirIdMap.set(
|
|
325
|
+
dir.menuName,
|
|
326
|
+
existing ? existing.id : `[dry-run:${dir.path}]`,
|
|
327
|
+
);
|
|
328
|
+
resultRows.push({
|
|
329
|
+
action: existing ? "update(dry-run)" : "create(dry-run)",
|
|
330
|
+
item: body,
|
|
331
|
+
status: "预览",
|
|
332
|
+
});
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const saved = await saveMenu(body, config);
|
|
336
|
+
if (saved.ok) {
|
|
337
|
+
const savedData = saved.data || body;
|
|
338
|
+
dirIdMap.set(dir.menuName, savedData.id || body.id);
|
|
339
|
+
flatMenus.push({ ...body, ...savedData });
|
|
340
|
+
resultRows.push({
|
|
341
|
+
action: existing ? "update" : "create",
|
|
342
|
+
item: body,
|
|
343
|
+
status: "✅ 成功",
|
|
344
|
+
});
|
|
345
|
+
} else {
|
|
346
|
+
resultRows.push({
|
|
347
|
+
action: existing ? "update" : "create",
|
|
348
|
+
item: body,
|
|
349
|
+
status: `❌ ${saved.error}`,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
for (let index = 0; index < parsed.pages.length; index += 1) {
|
|
355
|
+
const page = parsed.pages[index];
|
|
356
|
+
const parentId = dirIdMap.get(page.parentMenuName) || rootParentId;
|
|
357
|
+
const parent =
|
|
358
|
+
flatMenus.find((item) => String(item.id) === String(parentId)) || {};
|
|
359
|
+
const existing = findExisting(flatMenus, { ...page, parentId });
|
|
360
|
+
const body = buildMenuBody(
|
|
361
|
+
page,
|
|
362
|
+
config,
|
|
363
|
+
parentId,
|
|
364
|
+
parent.menuNameCode || parentMenuNameCode,
|
|
365
|
+
index + 1,
|
|
366
|
+
);
|
|
367
|
+
if (existing) body.id = existing.id;
|
|
368
|
+
if (dryRun) {
|
|
369
|
+
resultRows.push({
|
|
370
|
+
action: existing ? "update(dry-run)" : "create(dry-run)",
|
|
371
|
+
item: body,
|
|
372
|
+
status: "预览",
|
|
373
|
+
});
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
const saved = await saveMenu(body, config);
|
|
377
|
+
if (saved.ok) {
|
|
378
|
+
flatMenus.push({ ...body, ...(saved.data || {}) });
|
|
379
|
+
resultRows.push({
|
|
380
|
+
action: existing ? "update" : "create",
|
|
381
|
+
item: body,
|
|
382
|
+
status: "✅ 成功",
|
|
383
|
+
});
|
|
384
|
+
} else {
|
|
385
|
+
resultRows.push({
|
|
386
|
+
action: existing ? "update" : "create",
|
|
387
|
+
item: body,
|
|
388
|
+
status: `❌ ${saved.error}`,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const rel = path.relative(getProjectRoot(), reportPath).replace(/\\/g, "/");
|
|
394
|
+
const lines = [
|
|
395
|
+
`✅ SYS_MENU_INFO 同步完成:${rel}`,
|
|
396
|
+
"",
|
|
397
|
+
`- 一级目录:${parsed.dirs.length}`,
|
|
398
|
+
`- 二级菜单:${parsed.pages.length}`,
|
|
399
|
+
`- dryRun:${dryRun ? "是" : "否"}`,
|
|
400
|
+
"",
|
|
401
|
+
"| 操作 | 类型 | 菜单名 | path | parentId | 状态 |",
|
|
402
|
+
"|---|---|---|---|---|---|",
|
|
403
|
+
];
|
|
404
|
+
for (const row of resultRows) {
|
|
405
|
+
lines.push(
|
|
406
|
+
`| ${row.action} | ${row.item.type} | ${row.item.menuName} | ${row.item.path} | ${row.item.parentId} | ${row.status} |`,
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
return lines.join("\n");
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
module.exports = {
|
|
413
|
+
handleMenuQuery,
|
|
414
|
+
handleMenuUpsert,
|
|
415
|
+
handleMenuSyncFromReport,
|
|
416
|
+
};
|