@agile-team/wl-skills-kit 2.4.1 → 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 +53 -1
- package/README.md +104 -39
- package/bin/wl-skills.js +344 -44
- package/docs/ai/345/205/250/346/231/257/345/210/206/346/236/220.md +3 -3
- package/files/.github/copilot-instructions.md +361 -322
- package/files/.github/guides/architecture.md +6 -3
- package/files/.github/guides/usage.md +48 -13
- package/files/.github/skills/_compat/README.md +4 -2
- 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 +31 -2
|
@@ -1,228 +1,440 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
|
|
3
|
-
const fs = require(
|
|
4
|
-
const path = require(
|
|
5
|
-
const { execFileSync } = require(
|
|
6
|
-
const https = require(
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { execFileSync } = require("child_process");
|
|
6
|
+
const https = require("https");
|
|
7
7
|
|
|
8
8
|
function getProjectRoot() {
|
|
9
|
-
return process.env.WL_PROJECT_ROOT
|
|
9
|
+
return process.env.WL_PROJECT_ROOT
|
|
10
|
+
? path.resolve(process.env.WL_PROJECT_ROOT)
|
|
11
|
+
: process.cwd();
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
function normalizePath(p) {
|
|
13
|
-
return p.replace(/\\/g,
|
|
15
|
+
return p.replace(/\\/g, "/");
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
function safeResolve(root, inputPath) {
|
|
17
|
-
const full = inputPath ? path.resolve(root, inputPath) : root
|
|
19
|
+
const full = inputPath ? path.resolve(root, inputPath) : root;
|
|
18
20
|
if (full !== root && !full.startsWith(root + path.sep)) {
|
|
19
|
-
throw new Error(
|
|
21
|
+
throw new Error("路径越界:只能扫描项目根目录内的文件");
|
|
20
22
|
}
|
|
21
|
-
return full
|
|
23
|
+
return full;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
function walkFiles(dir, baseDir, files) {
|
|
25
|
-
files = files || []
|
|
26
|
-
if (!fs.existsSync(dir)) return files
|
|
27
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
27
|
+
files = files || [];
|
|
28
|
+
if (!fs.existsSync(dir)) return files;
|
|
29
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
28
30
|
for (const entry of entries) {
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
if (
|
|
32
|
+
entry.name === "node_modules" ||
|
|
33
|
+
entry.name === ".git" ||
|
|
34
|
+
entry.name === "dist"
|
|
35
|
+
)
|
|
36
|
+
continue;
|
|
37
|
+
const full = path.join(dir, entry.name);
|
|
38
|
+
if (entry.isDirectory()) walkFiles(full, baseDir, files);
|
|
39
|
+
else files.push(normalizePath(path.relative(baseDir, full)));
|
|
33
40
|
}
|
|
34
|
-
return files
|
|
41
|
+
return files;
|
|
35
42
|
}
|
|
36
43
|
|
|
37
44
|
function findPageDirs(root, scanRel) {
|
|
38
|
-
const scanDir = safeResolve(root, scanRel ||
|
|
39
|
-
const files = walkFiles(scanDir, root)
|
|
40
|
-
const dirs = new Map()
|
|
45
|
+
const scanDir = safeResolve(root, scanRel || "src/views");
|
|
46
|
+
const files = walkFiles(scanDir, root);
|
|
47
|
+
const dirs = new Map();
|
|
41
48
|
for (const rel of files) {
|
|
42
|
-
const name = path.basename(rel)
|
|
43
|
-
const dir = normalizePath(path.dirname(rel))
|
|
44
|
-
if (!dirs.has(dir)) dirs.set(dir, new Set())
|
|
45
|
-
dirs.get(dir).add(name)
|
|
49
|
+
const name = path.basename(rel);
|
|
50
|
+
const dir = normalizePath(path.dirname(rel));
|
|
51
|
+
if (!dirs.has(dir)) dirs.set(dir, new Set());
|
|
52
|
+
dirs.get(dir).add(name);
|
|
46
53
|
}
|
|
47
|
-
const pages = []
|
|
54
|
+
const pages = [];
|
|
48
55
|
for (const [dir, names] of dirs.entries()) {
|
|
49
|
-
if (!names.has(
|
|
50
|
-
const dataPath = path.join(root, dir,
|
|
51
|
-
let apiConfigCount = 0
|
|
56
|
+
if (!names.has("index.vue")) continue;
|
|
57
|
+
const dataPath = path.join(root, dir, "data.ts");
|
|
58
|
+
let apiConfigCount = 0;
|
|
52
59
|
if (fs.existsSync(dataPath)) {
|
|
53
|
-
const content = fs.readFileSync(dataPath,
|
|
54
|
-
apiConfigCount = (content.match(/API_CONFIG/g) || []).length
|
|
60
|
+
const content = fs.readFileSync(dataPath, "utf8");
|
|
61
|
+
apiConfigCount = (content.match(/API_CONFIG/g) || []).length;
|
|
55
62
|
}
|
|
56
63
|
pages.push({
|
|
57
64
|
dir,
|
|
58
|
-
hasIndexVue: names.has(
|
|
59
|
-
hasDataTs: names.has(
|
|
60
|
-
hasIndexScss: names.has(
|
|
61
|
-
hasApiMd: names.has(
|
|
65
|
+
hasIndexVue: names.has("index.vue"),
|
|
66
|
+
hasDataTs: names.has("data.ts"),
|
|
67
|
+
hasIndexScss: names.has("index.scss"),
|
|
68
|
+
hasApiMd: names.has("api.md"),
|
|
62
69
|
apiConfigCount,
|
|
63
|
-
})
|
|
70
|
+
});
|
|
64
71
|
}
|
|
65
|
-
pages.sort((a, b) => a.dir.localeCompare(b.dir))
|
|
66
|
-
return pages
|
|
72
|
+
pages.sort((a, b) => a.dir.localeCompare(b.dir));
|
|
73
|
+
return pages;
|
|
67
74
|
}
|
|
68
75
|
|
|
69
76
|
function formatPagesTable(pages) {
|
|
70
|
-
const lines = [
|
|
77
|
+
const lines = [
|
|
78
|
+
"| 页面目录 | index.vue | data.ts | index.scss | api.md | API_CONFIG |",
|
|
79
|
+
"|---|---:|---:|---:|---:|---:|",
|
|
80
|
+
];
|
|
71
81
|
for (const page of pages) {
|
|
72
82
|
lines.push(
|
|
73
|
-
`| ${page.dir} | ${page.hasIndexVue ?
|
|
74
|
-
)
|
|
83
|
+
`| ${page.dir} | ${page.hasIndexVue ? "✅" : "❌"} | ${page.hasDataTs ? "✅" : "❌"} | ${page.hasIndexScss ? "✅" : "❌"} | ${page.hasApiMd ? "✅" : "—"} | ${page.apiConfigCount} |`,
|
|
84
|
+
);
|
|
75
85
|
}
|
|
76
|
-
return lines.join(
|
|
86
|
+
return lines.join("\n");
|
|
77
87
|
}
|
|
78
88
|
|
|
79
89
|
async function handleCodeScan(args) {
|
|
80
|
-
const root = getProjectRoot()
|
|
81
|
-
const scanPath = args && args.path ? args.path :
|
|
82
|
-
const pages = findPageDirs(root, scanPath)
|
|
83
|
-
const missingData = pages.filter((p) => !p.hasDataTs).length
|
|
84
|
-
const missingScss = pages.filter((p) => !p.hasIndexScss).length
|
|
85
|
-
const missingApi = pages.filter((p) => !p.hasApiMd).length
|
|
86
|
-
const apiPages = pages.filter((p) => p.apiConfigCount > 0).length
|
|
90
|
+
const root = getProjectRoot();
|
|
91
|
+
const scanPath = args && args.path ? args.path : "src/views";
|
|
92
|
+
const pages = findPageDirs(root, scanPath);
|
|
93
|
+
const missingData = pages.filter((p) => !p.hasDataTs).length;
|
|
94
|
+
const missingScss = pages.filter((p) => !p.hasIndexScss).length;
|
|
95
|
+
const missingApi = pages.filter((p) => !p.hasApiMd).length;
|
|
96
|
+
const apiPages = pages.filter((p) => p.apiConfigCount > 0).length;
|
|
87
97
|
|
|
88
98
|
if (pages.length === 0) {
|
|
89
|
-
return `⚠️ 未在 ${scanPath} 下发现包含 index.vue
|
|
99
|
+
return `⚠️ 未在 ${scanPath} 下发现包含 index.vue 的页面目录`;
|
|
90
100
|
}
|
|
91
101
|
|
|
92
102
|
return [
|
|
93
103
|
`✅ 代码结构扫描完成:${scanPath}`,
|
|
94
|
-
|
|
104
|
+
"",
|
|
95
105
|
`- 页面目录:${pages.length}`,
|
|
96
106
|
`- 含 API_CONFIG:${apiPages}`,
|
|
97
107
|
`- 缺 data.ts:${missingData}`,
|
|
98
108
|
`- 缺 index.scss:${missingScss}`,
|
|
99
109
|
`- 缺 api.md:${missingApi}(需结合场景判断是否必须)`,
|
|
100
|
-
|
|
110
|
+
"",
|
|
101
111
|
formatPagesTable(pages),
|
|
102
|
-
].join(
|
|
112
|
+
].join("\n");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function readPackageDeps(root) {
|
|
116
|
+
const pkgPath = path.join(root, "package.json");
|
|
117
|
+
if (!fs.existsSync(pkgPath)) return {};
|
|
118
|
+
try {
|
|
119
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
120
|
+
return { ...pkg.dependencies, ...pkg.devDependencies };
|
|
121
|
+
} catch {
|
|
122
|
+
return {};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function findMockFiles(root) {
|
|
127
|
+
const mockDir = path.join(root, "mock");
|
|
128
|
+
if (!fs.existsSync(mockDir)) return [];
|
|
129
|
+
return walkFiles(mockDir, root).filter((rel) => /\.(ts|js)$/.test(rel));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function handleValidatePage(args) {
|
|
133
|
+
const root = getProjectRoot();
|
|
134
|
+
const scanPath = args && args.path ? args.path : "src/views";
|
|
135
|
+
const pages = findPageDirs(root, scanPath);
|
|
136
|
+
const mockFiles = findMockFiles(root);
|
|
137
|
+
const mockContent = mockFiles
|
|
138
|
+
.map((rel) => fs.readFileSync(path.join(root, rel), "utf8"))
|
|
139
|
+
.join("\n");
|
|
140
|
+
const issues = [];
|
|
141
|
+
|
|
142
|
+
for (const page of pages) {
|
|
143
|
+
const indexPath = path.join(root, page.dir, "index.vue");
|
|
144
|
+
const dataPath = path.join(root, page.dir, "data.ts");
|
|
145
|
+
const indexContent = fs.existsSync(indexPath)
|
|
146
|
+
? fs.readFileSync(indexPath, "utf8")
|
|
147
|
+
: "";
|
|
148
|
+
const dataContent = fs.existsSync(dataPath)
|
|
149
|
+
? fs.readFileSync(dataPath, "utf8")
|
|
150
|
+
: "";
|
|
151
|
+
const baseTableCount = (indexContent.match(/<BaseTable\b/g) || []).length;
|
|
152
|
+
const agGridCount = (
|
|
153
|
+
indexContent.match(/render-type=["']agGrid["']/g) || []
|
|
154
|
+
).length;
|
|
155
|
+
const cidCount = (indexContent.match(/\bcid=|:cid=/g) || []).length;
|
|
156
|
+
|
|
157
|
+
if (!page.hasDataTs) issues.push([page.dir, "warn", "缺 data.ts"]);
|
|
158
|
+
if (!page.hasIndexScss) issues.push([page.dir, "warn", "缺 index.scss"]);
|
|
159
|
+
if (page.apiConfigCount > 0 && !page.hasApiMd)
|
|
160
|
+
issues.push([page.dir, "warn", "检测到 API_CONFIG 但缺 api.md"]);
|
|
161
|
+
if (baseTableCount > 0 && agGridCount < baseTableCount)
|
|
162
|
+
issues.push([page.dir, "error", 'BaseTable 必须 render-type="agGrid"']);
|
|
163
|
+
if (baseTableCount > 0 && cidCount < baseTableCount)
|
|
164
|
+
issues.push([page.dir, "error", "BaseTable 必须配置 cid/:cid"]);
|
|
165
|
+
if (
|
|
166
|
+
baseTableCount > 0 &&
|
|
167
|
+
dataContent &&
|
|
168
|
+
!/defineColumns\s*\(/.test(dataContent)
|
|
169
|
+
)
|
|
170
|
+
issues.push([page.dir, "error", "列定义必须使用 defineColumns()"]);
|
|
171
|
+
if (/operations\s*:/.test(dataContent))
|
|
172
|
+
issues.push([
|
|
173
|
+
page.dir,
|
|
174
|
+
"error",
|
|
175
|
+
"禁止 operations 数组,必须使用 renderOps()",
|
|
176
|
+
]);
|
|
177
|
+
if (/onClick\s*:\s*\(\s*[^)]*\s*\)\s*=>\s*\{\s*\}/.test(dataContent))
|
|
178
|
+
issues.push([page.dir, "error", "存在空 onClick"]);
|
|
179
|
+
if (page.apiConfigCount > 0 && mockFiles.length === 0)
|
|
180
|
+
issues.push([page.dir, "warn", "检测到 API_CONFIG 但无 mock 文件"]);
|
|
181
|
+
const apiUrls = Array.from(
|
|
182
|
+
dataContent.matchAll(/:\s*["']([^"']+\/[^"']+)["']/g),
|
|
183
|
+
).map((m) => m[1]);
|
|
184
|
+
for (const url of apiUrls.filter((item) => item.startsWith("/"))) {
|
|
185
|
+
const mockUrl = `/dev-api${url}`;
|
|
186
|
+
if (mockContent && !mockContent.includes(mockUrl))
|
|
187
|
+
issues.push([page.dir, "warn", `mock 未发现端点 ${mockUrl}`]);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const errors = issues.filter((item) => item[1] === "error").length;
|
|
192
|
+
const lines = [
|
|
193
|
+
`✅ 页面校验完成:${scanPath}`,
|
|
194
|
+
"",
|
|
195
|
+
`- 页面目录:${pages.length}`,
|
|
196
|
+
`- error:${errors}`,
|
|
197
|
+
`- warn:${issues.length - errors}`,
|
|
198
|
+
"",
|
|
199
|
+
];
|
|
200
|
+
if (issues.length === 0) lines.push("✔ 未发现偏差");
|
|
201
|
+
else {
|
|
202
|
+
lines.push("| 页面目录 | 级别 | 问题 |", "|---|---|---|");
|
|
203
|
+
for (const [dir, level, text] of issues)
|
|
204
|
+
lines.push(`| ${dir} | ${level} | ${text} |`);
|
|
205
|
+
}
|
|
206
|
+
return lines.join("\n");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function handleDoctorUi() {
|
|
210
|
+
const root = getProjectRoot();
|
|
211
|
+
const deps = readPackageDeps(root);
|
|
212
|
+
const files = walkFiles(root, root).filter(
|
|
213
|
+
(rel) =>
|
|
214
|
+
/\.(ts|vue|scss|html)$/.test(rel) && !rel.startsWith("node_modules/"),
|
|
215
|
+
);
|
|
216
|
+
const content = files
|
|
217
|
+
.map((rel) => fs.readFileSync(path.join(root, rel), "utf8"))
|
|
218
|
+
.join("\n");
|
|
219
|
+
const checks = [
|
|
220
|
+
[
|
|
221
|
+
"@agile-team/wk-skills-ui",
|
|
222
|
+
Boolean(deps["@agile-team/wk-skills-ui"]),
|
|
223
|
+
deps["@agile-team/wk-skills-ui"] || "未安装",
|
|
224
|
+
],
|
|
225
|
+
[
|
|
226
|
+
"@element-plus/icons-vue",
|
|
227
|
+
Boolean(deps["@element-plus/icons-vue"]),
|
|
228
|
+
deps["@element-plus/icons-vue"] || "未安装",
|
|
229
|
+
],
|
|
230
|
+
[
|
|
231
|
+
"design tokens",
|
|
232
|
+
/@agile-team\/wk-skills-ui\/design\/tokens|dist\/tokens\.css/.test(
|
|
233
|
+
content,
|
|
234
|
+
),
|
|
235
|
+
"tokens 引入",
|
|
236
|
+
],
|
|
237
|
+
[
|
|
238
|
+
"styles preset",
|
|
239
|
+
/@agile-team\/wk-skills-ui\/styles/.test(content),
|
|
240
|
+
"styles/skin preset 引入",
|
|
241
|
+
],
|
|
242
|
+
[
|
|
243
|
+
"installCommonPreset",
|
|
244
|
+
/installCommonPreset\s*\(/.test(content),
|
|
245
|
+
"runtime preset 安装",
|
|
246
|
+
],
|
|
247
|
+
["defineColumns", /defineColumns\s*\(/.test(content), "列定义 runtime"],
|
|
248
|
+
["renderOps", /renderOps\s*\(/.test(content), "操作列 runtime"],
|
|
249
|
+
];
|
|
250
|
+
const lines = [
|
|
251
|
+
"✅ wk-skills-ui 接入检查",
|
|
252
|
+
"",
|
|
253
|
+
"| 检查项 | 状态 | 说明 |",
|
|
254
|
+
"|---|---:|---|",
|
|
255
|
+
];
|
|
256
|
+
for (const [name, ok, detail] of checks)
|
|
257
|
+
lines.push(`| ${name} | ${ok ? "✅" : "❌"} | ${detail} |`);
|
|
258
|
+
return lines.join("\n");
|
|
103
259
|
}
|
|
104
260
|
|
|
105
261
|
function findRouteFile(root, inputPath) {
|
|
106
262
|
if (inputPath) {
|
|
107
|
-
const full = safeResolve(root, inputPath)
|
|
108
|
-
return fs.existsSync(full) ? full : null
|
|
263
|
+
const full = safeResolve(root, inputPath);
|
|
264
|
+
return fs.existsSync(full) ? full : null;
|
|
109
265
|
}
|
|
110
266
|
const candidates = [
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
]
|
|
267
|
+
"vite/plugins/shared/pages.ts",
|
|
268
|
+
"src/router/pages.ts",
|
|
269
|
+
"src/router/routes.ts",
|
|
270
|
+
"src/router/index.ts",
|
|
271
|
+
];
|
|
116
272
|
for (const rel of candidates) {
|
|
117
|
-
const full = path.join(root, rel)
|
|
118
|
-
if (fs.existsSync(full)) return full
|
|
273
|
+
const full = path.join(root, rel);
|
|
274
|
+
if (fs.existsSync(full)) return full;
|
|
119
275
|
}
|
|
120
|
-
return null
|
|
276
|
+
return null;
|
|
121
277
|
}
|
|
122
278
|
|
|
123
279
|
async function handleRouteCheck(args) {
|
|
124
|
-
const root = getProjectRoot()
|
|
125
|
-
const scanPath = args && args.path ? args.path :
|
|
126
|
-
const routeFile = findRouteFile(root, args && args.routeFile)
|
|
280
|
+
const root = getProjectRoot();
|
|
281
|
+
const scanPath = args && args.path ? args.path : "src/views";
|
|
282
|
+
const routeFile = findRouteFile(root, args && args.routeFile);
|
|
127
283
|
if (!routeFile) {
|
|
128
|
-
return
|
|
284
|
+
return "⚠️ 未找到路由文件,默认检查路径:vite/plugins/shared/pages.ts / src/router/pages.ts / src/router/routes.ts / src/router/index.ts";
|
|
129
285
|
}
|
|
130
|
-
const routeContent = fs.readFileSync(routeFile,
|
|
131
|
-
const pages = findPageDirs(root, scanPath)
|
|
132
|
-
const rows = []
|
|
286
|
+
const routeContent = fs.readFileSync(routeFile, "utf8").replace(/\\/g, "/");
|
|
287
|
+
const pages = findPageDirs(root, scanPath);
|
|
288
|
+
const rows = [];
|
|
133
289
|
for (const page of pages) {
|
|
134
|
-
const viewRel = page.dir.replace(/^src\/views\//,
|
|
135
|
-
const lastSegment = viewRel.split(
|
|
136
|
-
const registered =
|
|
137
|
-
|
|
290
|
+
const viewRel = page.dir.replace(/^src\/views\//, "");
|
|
291
|
+
const lastSegment = viewRel.split("/").filter(Boolean).pop() || viewRel;
|
|
292
|
+
const registered =
|
|
293
|
+
routeContent.includes(viewRel) ||
|
|
294
|
+
routeContent.includes(page.dir) ||
|
|
295
|
+
routeContent.includes(lastSegment);
|
|
296
|
+
rows.push({ dir: page.dir, registered });
|
|
138
297
|
}
|
|
139
|
-
const miss = rows.filter((r) => !r.registered)
|
|
140
|
-
const relRoute = normalizePath(path.relative(root, routeFile))
|
|
141
|
-
const lines = [
|
|
142
|
-
|
|
143
|
-
|
|
298
|
+
const miss = rows.filter((r) => !r.registered);
|
|
299
|
+
const relRoute = normalizePath(path.relative(root, routeFile));
|
|
300
|
+
const lines = [
|
|
301
|
+
`✅ 路由检查完成:${relRoute}`,
|
|
302
|
+
"",
|
|
303
|
+
`- 页面目录:${rows.length}`,
|
|
304
|
+
`- 疑似未注册:${miss.length}`,
|
|
305
|
+
"",
|
|
306
|
+
"| 页面目录 | 路由文件中可发现 |",
|
|
307
|
+
"|---|---:|",
|
|
308
|
+
];
|
|
309
|
+
for (const row of rows)
|
|
310
|
+
lines.push(`| ${row.dir} | ${row.registered ? "✅" : "⚠️"} |`);
|
|
311
|
+
return lines.join("\n");
|
|
144
312
|
}
|
|
145
313
|
|
|
146
314
|
async function handleGitLogExtract(args) {
|
|
147
|
-
const root = getProjectRoot()
|
|
148
|
-
const n = Math.max(1, Math.min(Number((args && args.n) || 20), 100))
|
|
149
|
-
let output
|
|
315
|
+
const root = getProjectRoot();
|
|
316
|
+
const n = Math.max(1, Math.min(Number((args && args.n) || 20), 100));
|
|
317
|
+
let output;
|
|
150
318
|
try {
|
|
151
|
-
output = execFileSync(
|
|
319
|
+
output = execFileSync(
|
|
320
|
+
"git",
|
|
321
|
+
[
|
|
322
|
+
"log",
|
|
323
|
+
`-${n}`,
|
|
324
|
+
"--pretty=format:%h%x09%s%x09%an%x09%ad",
|
|
325
|
+
"--date=short",
|
|
326
|
+
],
|
|
327
|
+
{ cwd: root, encoding: "utf8" },
|
|
328
|
+
);
|
|
152
329
|
} catch (e) {
|
|
153
|
-
return `❌ git log 提取失败:${e.message}
|
|
330
|
+
return `❌ git log 提取失败:${e.message}`;
|
|
154
331
|
}
|
|
155
|
-
if (!output.trim()) return
|
|
156
|
-
const lines = [
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
332
|
+
if (!output.trim()) return "⚠️ 未提取到 git log";
|
|
333
|
+
const lines = [
|
|
334
|
+
"✅ 最近提交摘要",
|
|
335
|
+
"",
|
|
336
|
+
"| hash | message | author | date |",
|
|
337
|
+
"|---|---|---|---|",
|
|
338
|
+
];
|
|
339
|
+
for (const row of output.split("\n")) {
|
|
340
|
+
const [hash, message, author, date] = row.split("\t");
|
|
341
|
+
lines.push(
|
|
342
|
+
`| ${hash} | ${String(message || "").replace(/\|/g, "\\|")} | ${author || ""} | ${date || ""} |`,
|
|
343
|
+
);
|
|
160
344
|
}
|
|
161
|
-
return lines.join(
|
|
345
|
+
return lines.join("\n");
|
|
162
346
|
}
|
|
163
347
|
|
|
164
348
|
function readEnvLocal(root) {
|
|
165
|
-
const envPath = path.join(
|
|
166
|
-
|
|
349
|
+
const envPath = path.join(
|
|
350
|
+
root,
|
|
351
|
+
".github",
|
|
352
|
+
"skills",
|
|
353
|
+
"sync",
|
|
354
|
+
"env.local.json",
|
|
355
|
+
);
|
|
356
|
+
if (!fs.existsSync(envPath)) return null;
|
|
167
357
|
try {
|
|
168
|
-
return JSON.parse(fs.readFileSync(envPath,
|
|
358
|
+
return JSON.parse(fs.readFileSync(envPath, "utf8"));
|
|
169
359
|
} catch (e) {
|
|
170
|
-
return null
|
|
360
|
+
return null;
|
|
171
361
|
}
|
|
172
362
|
}
|
|
173
363
|
|
|
174
364
|
function findLatestAuditReport(root, inputPath) {
|
|
175
365
|
if (inputPath) {
|
|
176
|
-
const full = safeResolve(root, inputPath)
|
|
177
|
-
return fs.existsSync(full) ? full : null
|
|
366
|
+
const full = safeResolve(root, inputPath);
|
|
367
|
+
return fs.existsSync(full) ? full : null;
|
|
178
368
|
}
|
|
179
|
-
const reportDir = path.join(root,
|
|
180
|
-
if (!fs.existsSync(reportDir)) return null
|
|
369
|
+
const reportDir = path.join(root, ".github", "reports");
|
|
370
|
+
if (!fs.existsSync(reportDir)) return null;
|
|
181
371
|
const files = fs
|
|
182
372
|
.readdirSync(reportDir)
|
|
183
373
|
.filter((name) => /^AUDIT_.*\.md$|规范审查报告\.md$/.test(name))
|
|
184
374
|
.map((name) => path.join(reportDir, name))
|
|
185
|
-
.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs)
|
|
186
|
-
return files[0] || null
|
|
375
|
+
.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
|
|
376
|
+
return files[0] || null;
|
|
187
377
|
}
|
|
188
378
|
|
|
189
379
|
function postWebhook(webhook, payload) {
|
|
190
380
|
return new Promise((resolve) => {
|
|
191
|
-
const body = JSON.stringify(payload)
|
|
381
|
+
const body = JSON.stringify(payload);
|
|
192
382
|
const req = https.request(
|
|
193
383
|
webhook,
|
|
194
|
-
{
|
|
384
|
+
{
|
|
385
|
+
method: "POST",
|
|
386
|
+
headers: {
|
|
387
|
+
"Content-Type": "application/json",
|
|
388
|
+
"Content-Length": Buffer.byteLength(body),
|
|
389
|
+
},
|
|
390
|
+
},
|
|
195
391
|
(res) => {
|
|
196
|
-
const chunks = []
|
|
197
|
-
res.on(
|
|
198
|
-
res.on(
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
392
|
+
const chunks = [];
|
|
393
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
394
|
+
res.on("end", () =>
|
|
395
|
+
resolve({
|
|
396
|
+
ok: res.statusCode >= 200 && res.statusCode < 300,
|
|
397
|
+
statusCode: res.statusCode,
|
|
398
|
+
body: Buffer.concat(chunks).toString("utf8"),
|
|
399
|
+
}),
|
|
400
|
+
);
|
|
401
|
+
},
|
|
402
|
+
);
|
|
403
|
+
req.on("error", (e) => resolve({ ok: false, error: e.message }));
|
|
404
|
+
req.write(body);
|
|
405
|
+
req.end();
|
|
406
|
+
});
|
|
205
407
|
}
|
|
206
408
|
|
|
207
409
|
async function handleAuditReportPush(args) {
|
|
208
|
-
const root = getProjectRoot()
|
|
209
|
-
const env = readEnvLocal(root)
|
|
210
|
-
const webhook = env && env.feishu_webhook
|
|
211
|
-
if (
|
|
212
|
-
|
|
410
|
+
const root = getProjectRoot();
|
|
411
|
+
const env = readEnvLocal(root);
|
|
412
|
+
const webhook = env && env.feishu_webhook;
|
|
413
|
+
if (
|
|
414
|
+
!webhook ||
|
|
415
|
+
String(webhook).includes("你的") ||
|
|
416
|
+
String(webhook).includes("webhook")
|
|
417
|
+
) {
|
|
418
|
+
return "ℹ️ 未配置 env.local.json 的 feishu_webhook,已跳过审计报告推送";
|
|
213
419
|
}
|
|
214
|
-
const report = findLatestAuditReport(root, args && args.reportPath)
|
|
215
|
-
if (!report) return
|
|
216
|
-
const rel = normalizePath(path.relative(root, report))
|
|
217
|
-
const content = fs.readFileSync(report,
|
|
218
|
-
const result = await postWebhook(webhook, {
|
|
219
|
-
|
|
220
|
-
|
|
420
|
+
const report = findLatestAuditReport(root, args && args.reportPath);
|
|
421
|
+
if (!report) return "⚠️ 未找到可推送的审计报告";
|
|
422
|
+
const rel = normalizePath(path.relative(root, report));
|
|
423
|
+
const content = fs.readFileSync(report, "utf8").slice(0, 3500);
|
|
424
|
+
const result = await postWebhook(webhook, {
|
|
425
|
+
msg_type: "text",
|
|
426
|
+
content: { text: `wl-skills 审计报告:${rel}\n\n${content}` },
|
|
427
|
+
});
|
|
428
|
+
if (!result.ok)
|
|
429
|
+
return `❌ 飞书推送失败:${result.error || result.statusCode}`;
|
|
430
|
+
return `✅ 审计报告已推送:${rel}`;
|
|
221
431
|
}
|
|
222
432
|
|
|
223
433
|
module.exports = {
|
|
224
434
|
handleCodeScan,
|
|
435
|
+
handleValidatePage,
|
|
436
|
+
handleDoctorUi,
|
|
225
437
|
handleRouteCheck,
|
|
226
438
|
handleGitLogExtract,
|
|
227
439
|
handleAuditReportPush,
|
|
228
|
-
}
|
|
440
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agile-team/wl-skills-kit",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "AI Skill 模板包 v2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
|
+
"description": "AI Skill 模板包 v2.5.0 — 13 条编码规范 + 9 个 AI Skill + 17 个 MCP Tool,一条命令导入 Vue 3 项目",
|
|
5
5
|
"main": "./bin/wl-skills.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"wl-skills": "bin/wl-skills.js"
|
|
@@ -35,7 +35,36 @@
|
|
|
35
35
|
"engines": {
|
|
36
36
|
"node": ">=16.0.0"
|
|
37
37
|
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"standards:init": "npx @robot-admin/git-standards init",
|
|
40
|
+
"prepare": "husky",
|
|
41
|
+
"cz": "git-cz",
|
|
42
|
+
"lint": "eslint ."
|
|
43
|
+
},
|
|
38
44
|
"dependencies": {
|
|
39
45
|
"xlsx": "^0.18.5"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@commitlint/cli": "^20.5.3",
|
|
49
|
+
"@commitlint/config-conventional": "^20.5.3",
|
|
50
|
+
"@typescript-eslint/eslint-plugin": "^8.59.2",
|
|
51
|
+
"@typescript-eslint/parser": "^8.59.2",
|
|
52
|
+
"@vue/eslint-config-typescript": "^14.7.0",
|
|
53
|
+
"commitizen": "^4.3.1",
|
|
54
|
+
"cz-customizable": "^7.5.4",
|
|
55
|
+
"eslint": "^10.3.0",
|
|
56
|
+
"eslint-plugin-vue": "^10.9.0",
|
|
57
|
+
"husky": "^9.1.7",
|
|
58
|
+
"lint-staged": "^16.4.0"
|
|
59
|
+
},
|
|
60
|
+
"config": {
|
|
61
|
+
"commitizen": {
|
|
62
|
+
"path": "node_modules/cz-customizable"
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"lint-staged": {
|
|
66
|
+
"src/**/*.{js,jsx,ts,tsx,vue}": [
|
|
67
|
+
"eslint --fix --no-cache"
|
|
68
|
+
]
|
|
40
69
|
}
|
|
41
70
|
}
|