@agile-team/wl-skills-kit 2.7.3 → 2.8.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 CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.8.0] - 2026-05-16
4
+
5
+ ### Added
6
+
7
+ - **`files/docs/mock-architecture.md`**(新文档):Mock 架构规范,涵盖目录约定(按业务域分目录 `mock/[域]/[模块].ts`)、`_utils.ts` 共享工具、`ENV_MOCK` 开关机制、STORE 可变数组持久化模式、端点覆盖检查、一键清理流程。
8
+ - **`files/mock/_utils.ts`**(种子文件):`init`/`update` 自动写入目标项目 `mock/_utils.ts`,提供 `pageResult`/`ok`/`paginate`/`nowStr`/`pick` 五个共享工具,消除各 mock 模块重复定义。
9
+ - **CLI `mock-clean` 命令**:`--domain <name>` 按域清理(如 `mock-clean --domain mdata`)、`--all` 全量清理(保留 `_utils.ts`)、支持 `--dry-run` 预览。清理后提示修改 `ENV_MOCK` 并运行 `validate`。
10
+
11
+ ### Changed
12
+
13
+ - **`copilot-instructions.md`** 新增 Mock 架构节:目录结构约定、开关机制、URL 对齐、STORE 模式、模块自治原则、`mock-clean` CLI 用法。
14
+ - **`page-codegen/SKILL.md`** 规则修正:规则 9 → `mock/[业务域]/[模块].ts` 按域分目录 + 强制 `import ../_utils`;规则 20 → 自检 `mock/_utils.ts` 存在;规则 21 → 路径格式对齐;禁止 14 → 引用 `docs/mock-architecture.md`。
15
+ - **`validate` 增强**:新增 3 项 mock 架构质量检查 —— `_utils.ts` 存在性(warn)、mock 文件是否按域分目录(info)、是否引用共享工具(info)。输出新增 `ℹ` info 级别图标。
16
+ - **`_best-practices.md`** 场景 6 扩展:从一句话描述 → 完整 mock 架构速览 + 目录树 + 开关/清理说明。
17
+ - **`--domain` / `--all` 加入 `KNOWN_FLAGS`**;flag 校验兼容 `--domain=xxx` 等号格式。
18
+
19
+ ### Notes
20
+
21
+ - Mock 架构为前端最佳实践固化,已在 wl-mdata 项目验证。新项目 `init` 后即获得 `mock/_utils.ts`;已有项目 `update` 后自动补充。
22
+
3
23
  ## [2.7.3] - 2026-05-13
4
24
 
5
25
  ### Added
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @agile-team/wl-skills-kit
2
2
 
3
- **AI Skill 模板包 v2.7.3** — 一键将 13 条规范、10 个 AI Skill、17 个 MCP Tool、编辑器 MCP 配置、文档导入 Vue 3 项目。
3
+ **AI Skill 模板包 v2.8.0** — 一键将 13 条规范、10 个 AI Skill、17 个 MCP Tool、编辑器 MCP 配置、文档导入 Vue 3 项目。
4
4
 
5
5
  让 AI 编辑器(Copilot / Cursor / Windsurf / Claude Code / Cline / Kiro / Trae / Qoder / 通用 Agents)**真正理解项目规范**,从原型/详设到完整页面代码全流程自动化。
6
6
 
@@ -23,6 +23,15 @@ npm run standards:init # 本包维护/业务项目均可复用
23
23
 
24
24
  ## 版本亮点
25
25
 
26
+ **v2.8.0**:Mock 架构体系固化 + `mock-clean` CLI 命令。
27
+
28
+ - 新增 `docs/mock-architecture.md` — Mock 目录约定、开关机制、模块化规范、一键清理流程
29
+ - 新增 `mock/_utils.ts` 种子文件 — `init` 自动写入,提供 `pageResult`/`ok`/`paginate`/`nowStr`/`pick`
30
+ - 新增 CLI `mock-clean` 命令 — `--domain <name>` 按域清理、`--all` 全量清理(保留 `_utils.ts`)、`--dry-run` 预览
31
+ - `copilot-instructions.md` 新增 Mock 架构节(目录约定、开关、STORE 模式、URL 对齐)
32
+ - `page-codegen/SKILL.md` 规则 9/20/21 修正为按域分目录 + 强制引用 `_utils`
33
+ - `validate` 增强:检查 `_utils.ts` 存在、mock 文件是否按域分目录、是否引用共享工具
34
+
26
35
  **v2.7.3**:工程质量提升 — MCP tools 单测覆盖、lint-skills 扩展到 core Skill、.gitattributes 消除行尾符噪音、permission-sync SKILL.md 精简。
27
36
 
28
37
  - 新增 `tests/mcp-tools.test.js`(30 个测试)覆盖 menuSync/dictSync/permissionSync 核心纯函数与参数校验
@@ -53,7 +62,7 @@ npm run standards:init # 本包维护/业务项目均可复用
53
62
  - **单元测试**:registry / CLI / version-tools 共 18 项覆盖,上面三项防护都有连动验证
54
63
  - **单一数据源加固**:`SKILL_COUNT` 从常量改为从 `_registry.md` 动态计算;copilot-instructions 删除内嵌 Skill 表改为指针;dict-sync / code-fix 补 USAGE.md
55
64
 
56
- 当前 2.6.x 重点补齐业务理解闭环:原型/详设 → 业务文档 → 接口契约 → 页面代码 → 复扫。
65
+ 2.6.x 以来重点补齐业务理解闭环:原型/详设 → 业务文档 → 接口契约 → 页面代码 → 复扫。
57
66
 
58
67
  - **新增 `business-doc-extract` Skill**:语义级智能触发(不依赖固定关键词),在资料达模块/项目级完整度时建议生成业务文档:
59
68
  ```text
@@ -125,16 +134,17 @@ wl-skills-kit/ ← 你正看的这个仓库
125
134
  ├── package.json name: @agile-team/wl-skills-kit
126
135
 
127
136
  ├── bin/
128
- │ └── wl-skills.js CLI 实现(init / update / clean / check / diff / validate / validate-page / doctor-ui / export)
137
+ │ └── wl-skills.js CLI 实现(init / update / clean / check / diff / validate / validate-page / doctor-ui / export / mock-clean
129
138
 
130
139
  ├── files/ ★★★ 真正会被打包并复制到业务项目的内容 ★★★
131
- └── .github/
132
- ├── copilot-instructions.md 源 AI 主入口(编辑这里,不要编辑业务项目里的副本)
133
- ├── standards/ 13 条规范
134
- ├── skills/ Skill 目录(含 _compat/ 多编辑器适配源)
135
- ├── guides/ 人读指南
136
- └── reports/ 领域基线模板(菜单/字典/权限)
137
- │ ├── docs/ 组件 API 文档
140
+ ├── .github/
141
+ ├── copilot-instructions.md 源 AI 主入口(编辑这里,不要编辑业务项目里的副本)
142
+ ├── standards/ 13 条规范
143
+ ├── skills/ Skill 目录(含 _compat/ 多编辑器适配源)
144
+ ├── guides/ 人读指南
145
+ └── reports/ 领域基线模板(菜单/字典/权限)
146
+ │ ├── docs/ 组件 API 文档 + Mock 架构规范
147
+ │ ├── mock/ Mock 共享工具种子(_utils.ts)
138
148
  │ └── demo/ 领域样例
139
149
 
140
150
  ├── kit-internal/ ★★ 仅仓库可见,不会安装到业务项目 ★★
@@ -163,7 +173,7 @@ wl-skills-kit/ ← 你正看的这个仓库
163
173
  你的业务项目/
164
174
 
165
175
  ├── .github/ ← 来自本包 files/.github/
166
- │ ├── copilot-instructions.md Copilot 主入口(精简 ~320 行)
176
+ │ ├── copilot-instructions.md Copilot 主入口(精简 ~340 行)
167
177
  │ ├── standards/ 13 条模块化规范 + index.md 门控
168
178
  │ │ ├── 01-toolchain.md
169
179
  │ │ ├── 02-code-structure.md
@@ -204,7 +214,11 @@ wl-skills-kit/ ← 你正看的这个仓库
204
214
  ├── .trae/rules/conventions.md Trae(含 alwaysApply frontmatter)
205
215
  ├── .qoder/rules/conventions.md Qoder
206
216
 
207
- ├── docs/ 12 个组件 API 文档
217
+ ├── mock/ 来自本包 files/mock/(init 自动写入)
218
+ │ ├── _utils.ts 共享工具(pageResult / ok / paginate / nowStr / pick)
219
+ │ └── [业务域]/[模块].ts 按域分目录,page-codegen 自动生成
220
+
221
+ ├── docs/ 12 个组件 API 文档 + mock-architecture.md
208
222
  ├── demo/ 13 个领域样例
209
223
  └── src/
210
224
  ├── components/ 全局/局部/远程组件
@@ -254,6 +268,12 @@ npx @agile-team/wl-skills-kit clean
254
268
  # 清理但保留 reports/(菜单/字典/权限累积数据)
255
269
  npx @agile-team/wl-skills-kit clean --keep-reports
256
270
 
271
+ # 清理指定业务域的 mock 文件(保留 _utils.ts)
272
+ npx @agile-team/wl-skills-kit mock-clean --domain mdata
273
+
274
+ # 清理全部 mock(保留 _utils.ts)
275
+ npx @agile-team/wl-skills-kit mock-clean --all
276
+
257
277
  # 任何命令都可加 --dry-run 预览
258
278
  npx @agile-team/wl-skills-kit update --dry-run
259
279
  ```
@@ -313,6 +333,7 @@ npx @agile-team/wl-skills-kit update
313
333
  | `api-contract` | ✅ 启用 | `skills/core/api-contract/` | 生成 api.md 前后端契约 |
314
334
  | `page-codegen` | ✅ 启用 | `skills/core/page-codegen/` | 页面骨架生成 + 模板调度 |
315
335
  | `convention-audit` | ✅ 启用 | `skills/core/convention-audit/` | 13 条规范扫描 + 双报告 |
336
+ | `business-doc-extract` | ✅ 启用 | `skills/core/business-doc-extract/` | 语义触发,业务文档抽取与维护 |
316
337
  | `template-extract` | ✅ 启用 | `skills/core/template-extract/` | 现有页面 → 领域模板 |
317
338
  | `menu-sync` | ✅ 启用 | `skills/sync/menu-sync/` | 菜单基线 ↔ 后端接口 |
318
339
  | `dict-sync` | ✅ 启用 | `skills/sync/dict-sync/` | 字典基线 ↔ 后端接口 |
package/bin/wl-skills.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * wl-skills-kit CLI v2.7.3
4
+ * wl-skills-kit CLI v2.8.0
5
5
  *
6
6
  * 命令:
7
7
  * init 全量安装(默认,向后兼容)
@@ -13,6 +13,7 @@
13
13
  * validate-page validate 的别名,适用于单页/目录检查
14
14
  * doctor-ui 检查 @agile-team/wk-skills-ui 接入完整性
15
15
  * export 导出 SYS_MENU / SYS_DICT / SYS_PERMISSION 为 xlsx
16
+ * mock-clean 清理 mock 文件(按域或全部),保留 _utils.ts
16
17
  * --help 帮助
17
18
  * --dry-run 预览模式(所有命令均支持)
18
19
  */
@@ -39,6 +40,7 @@ const KNOWN_COMMANDS = new Set([
39
40
  "validate-page",
40
41
  "doctor-ui",
41
42
  "export",
43
+ "mock-clean",
42
44
  ]);
43
45
  const KNOWN_FLAGS = new Set([
44
46
  "--dry-run",
@@ -46,6 +48,8 @@ const KNOWN_FLAGS = new Set([
46
48
  "--force",
47
49
  "--help",
48
50
  "-h",
51
+ "--domain",
52
+ "--all",
49
53
  ]);
50
54
 
51
55
  const dryRun = args.includes("--dry-run");
@@ -56,7 +60,10 @@ const force = args.includes("--force");
56
60
  // 校验所有 flag 是否已知(--help 优先,跳过校验直接显示帮助)
57
61
  if (!showHelp) {
58
62
  const unknownFlags = args.filter(
59
- (a) => a.startsWith("-") && !KNOWN_FLAGS.has(a),
63
+ (a) =>
64
+ a.startsWith("-") &&
65
+ !KNOWN_FLAGS.has(a) &&
66
+ !KNOWN_FLAGS.has(a.split("=")[0]),
60
67
  );
61
68
  if (unknownFlags.length > 0) {
62
69
  console.error("");
@@ -96,11 +103,14 @@ if (showHelp) {
96
103
  validate-page validate 的别名,适用于单页/目录检查
97
104
  doctor-ui 检查 @agile-team/wk-skills-ui 接入完整性
98
105
  export 导出 reports/SYS_* 数据为 xlsx
106
+ mock-clean 清理 mock 文件(按域或全部),保留 _utils.ts
99
107
 
100
108
  选项:
101
109
  --dry-run 预览模式,不实际写入/删除任何文件
102
110
  --keep-reports clean 命令保留 .github/reports/(默认一起删除)
103
111
  --force 强制执行,跳过同版本检测(忽略已安装状态)
112
+ --domain <name> mock-clean 指定要清理的业务域(如 sale、mdata)
113
+ --all mock-clean 清理全部 mock(保留 _utils.ts)
104
114
  --help 显示帮助
105
115
 
106
116
  示例:
@@ -116,6 +126,9 @@ if (showHelp) {
116
126
  npx @agile-team/wl-skills-kit clean 清理开发期文件
117
127
  npx @agile-team/wl-skills-kit clean --keep-reports 保留 reports/中的菜单/字典数据
118
128
  npx @agile-team/wl-skills-kit clean --dry-run 预览将要清理哪些文件
129
+ npx @agile-team/wl-skills-kit mock-clean --domain mdata 清理 mdata 域 mock
130
+ npx @agile-team/wl-skills-kit mock-clean --all 清理全部 mock
131
+ npx @agile-team/wl-skills-kit mock-clean --all --dry-run 预览将要清理的 mock 文件
119
132
 
120
133
  保护路径(init / update 不覆盖已存在的):
121
134
  .github/reports/ AI 生成报告(团队累积数据,存在则跳过)
@@ -853,6 +866,54 @@ function runValidate() {
853
866
  const mockContent = mockFiles
854
867
  .map((rel) => fs.readFileSync(path.join(TARGET_DIR, rel), "utf8"))
855
868
  .join("\n");
869
+
870
+ // ── Mock 架构质量检查 ──────────────────────────────────────────────
871
+ const mockDir = path.join(TARGET_DIR, "mock");
872
+ const hasMockDir = fs.existsSync(mockDir);
873
+ const hasUtilsTs =
874
+ hasMockDir &&
875
+ (fs.existsSync(path.join(mockDir, "_utils.ts")) ||
876
+ fs.existsSync(path.join(mockDir, "_utils.js")));
877
+ if (hasMockDir && mockFiles.length > 0 && !hasUtilsTs) {
878
+ issues.push({
879
+ level: "warn",
880
+ dir: "mock/",
881
+ text: "缺少 mock/_utils.ts 共享工具文件(建议 wl-skills init 补充)",
882
+ });
883
+ }
884
+ // 检查 mock 文件是否按域分目录(非 _utils 的 ts/js 文件不应直接放在 mock/ 根)
885
+ for (const rel of mockFiles) {
886
+ const parts = rel
887
+ .replace(/\\/g, "/")
888
+ .replace(/^mock\//, "")
889
+ .split("/");
890
+ const basename = parts[parts.length - 1];
891
+ if (parts.length === 1 && !basename.startsWith("_")) {
892
+ issues.push({
893
+ level: "info",
894
+ dir: "mock/",
895
+ text:
896
+ basename +
897
+ " 直接放在 mock/ 根目录,建议按业务域分子目录(如 mock/sale/" +
898
+ basename +
899
+ ")",
900
+ });
901
+ }
902
+ }
903
+ // 检查 mock 模块文件是否 import _utils
904
+ for (const rel of mockFiles) {
905
+ const basename = path.basename(rel);
906
+ if (basename.startsWith("_")) continue;
907
+ const content = fs.readFileSync(path.join(TARGET_DIR, rel), "utf8");
908
+ if (hasUtilsTs && !/_utils/.test(content)) {
909
+ issues.push({
910
+ level: "info",
911
+ dir: rel,
912
+ text: "未引用 mock/_utils 共享工具,建议统一使用 pageResult/ok/paginate",
913
+ });
914
+ }
915
+ }
916
+
856
917
  for (const page of pages) {
857
918
  if (!page.hasDataTs)
858
919
  issues.push({
@@ -933,7 +994,8 @@ function runValidate() {
933
994
  console.log("");
934
995
  const errors = issues.filter((issue) => issue.level === "error").length;
935
996
  for (const issue of issues) {
936
- const icon = issue.level === "error" ? "✖" : "⚠";
997
+ const icon =
998
+ issue.level === "error" ? "✖" : issue.level === "info" ? "ℹ" : "⚠";
937
999
  console.log(" " + icon + " " + issue.dir + " — " + issue.text);
938
1000
  }
939
1001
  if (issues.length === 0) console.log(" ✔ 页面文件完整性检查通过");
@@ -1106,6 +1168,85 @@ function runExport() {
1106
1168
  console.log("");
1107
1169
  }
1108
1170
 
1171
+ // ─── mock-clean ──────────────────────────────────────────────────────────
1172
+
1173
+ function runMockClean() {
1174
+ console.log("");
1175
+ console.log(" wl-skills-kit v" + PKG.version + " [mock-clean]");
1176
+ console.log("");
1177
+
1178
+ const mockDir = path.join(TARGET_DIR, "mock");
1179
+ if (!fs.existsSync(mockDir)) {
1180
+ console.log(" ⚠ mock/ 目录不存在,无需清理");
1181
+ console.log("");
1182
+ return;
1183
+ }
1184
+
1185
+ const domainArg = args.find((a) => a.startsWith("--domain"));
1186
+ const cleanAll = args.includes("--all");
1187
+ let domain = "";
1188
+ if (domainArg) {
1189
+ // 支持 --domain=xxx 和 --domain xxx
1190
+ if (domainArg.includes("=")) {
1191
+ domain = domainArg.split("=")[1];
1192
+ } else {
1193
+ const idx = args.indexOf(domainArg);
1194
+ domain = args[idx + 1] || "";
1195
+ }
1196
+ }
1197
+
1198
+ if (!domain && !cleanAll) {
1199
+ console.error(" ✖ 请指定 --domain <name> 或 --all");
1200
+ console.error(" 示例: wl-skills mock-clean --domain mdata");
1201
+ console.error(" wl-skills mock-clean --all");
1202
+ console.error("");
1203
+ process.exit(1);
1204
+ }
1205
+
1206
+ // 收集要删除的文件/目录
1207
+ const toRemove = [];
1208
+ if (cleanAll) {
1209
+ // 删除 mock/ 下除 _utils.ts/_utils.js 之外的所有文件和子目录
1210
+ const entries = fs.readdirSync(mockDir, { withFileTypes: true });
1211
+ for (const entry of entries) {
1212
+ if (entry.name.startsWith("_")) continue; // 保留 _utils.ts 等
1213
+ toRemove.push(path.join(mockDir, entry.name));
1214
+ }
1215
+ } else {
1216
+ // 删除指定域目录
1217
+ const domainDir = path.join(mockDir, domain);
1218
+ if (!fs.existsSync(domainDir)) {
1219
+ console.log(' ⚠ mock/' + domain + '/ 不存在');
1220
+ console.log("");
1221
+ return;
1222
+ }
1223
+ toRemove.push(domainDir);
1224
+ }
1225
+
1226
+ if (toRemove.length === 0) {
1227
+ console.log(" ✔ 无需清理(仅剩 _utils.ts)");
1228
+ console.log("");
1229
+ return;
1230
+ }
1231
+
1232
+ for (const target of toRemove) {
1233
+ const rel = path.relative(TARGET_DIR, target);
1234
+ if (dryRun) {
1235
+ console.log(" [dry-run] 将删除: " + rel);
1236
+ } else {
1237
+ fs.rmSync(target, { recursive: true, force: true });
1238
+ console.log(" ✔ 已删除: " + rel);
1239
+ }
1240
+ }
1241
+
1242
+ console.log("");
1243
+ if (!dryRun) {
1244
+ console.log(" 建议:将 .env.dev 中 ENV_MOCK 改为 false");
1245
+ console.log(" 然后运行 wl-skills validate 检查页面无 mock 依赖残留");
1246
+ console.log("");
1247
+ }
1248
+ }
1249
+
1109
1250
  // ─── 主路由 ─────────────────────────────────────────────────────────────
1110
1251
 
1111
1252
  switch (command) {
@@ -1134,6 +1275,9 @@ switch (command) {
1134
1275
  case "export":
1135
1276
  runExport();
1136
1277
  break;
1278
+ case "mock-clean":
1279
+ runMockClean();
1280
+ break;
1137
1281
  default:
1138
1282
  console.error(
1139
1283
  ' ✖ 未知命令: "' + command + '",请使用 --help 查看可用命令',
@@ -28,6 +28,24 @@ src/views/[域]/[模块]/[子模块]/[kebab-case目录]/
28
28
  - **通用弹窗**(新增/编辑表单,2+ 页面可复用)→ 提取到 `src/components/local/c_xxxModal/`
29
29
  - **极个性弹窗**(仅单页面使用,c_modal 无法满足)→ 放在页面 `components/xxxModal.vue`
30
30
 
31
+ ## Mock 架构(与页面完全解耦)
32
+
33
+ > 详细规范见 `docs/mock-architecture.md`
34
+
35
+ ```
36
+ mock/
37
+ ├── _utils.ts ← 共享工具(pageResult / ok / paginate / nowStr / pick)
38
+ └── [业务域]/ ← 镜像 src/views 第一级目录
39
+ └── [模块].ts ← 每个模块一个文件,export default MockMethod[]
40
+ ```
41
+
42
+ - **开关**:`.env.dev` 中 `ENV_MOCK=true/false`,`vite.config.ts` 中 `viteMockServe({ enable: command === "serve" && config.ENV_MOCK !== "false" })`
43
+ - **解耦**:mock 文件放在项目根 `mock/` 目录,不在 `src/views` 中 import 任何 mock
44
+ - **URL 对齐**:`API_CONFIG` 保持真实路径(如 `/mdata/mdataModel/list`),mock 端点带 `/dev-api` 前缀,关闭 mock 后无需改页面代码
45
+ - **STORE 模式**:`let STORE = Array.from({ length: N }, genRecord)` 可变数组,CRUD 直接修改内存,查询立即可见
46
+ - **按模块自治**:删某业务 mock 只删对应文件,不影响其他模块
47
+ - **一键清理**:`wl-skills mock-clean --domain [域]` 或 `--all`
48
+
31
49
  ## data.ts 核心模式
32
50
 
33
51
  > 配置化驱动,通过 `API_CONFIG` + `class extends AbstractPageQueryHook` 实现零 API 层开发。
@@ -2,7 +2,7 @@
2
2
 
3
3
  > **读者**:团队技术负责人 / wl-skills-kit 维护者 / 对体系设计感兴趣的团队成员
4
4
  > **更新方式**:重大架构变更后追加对应章节,旧章节原文保留(历史可溯)
5
- > **当前版本**:v2.7.3(2026-05-13
5
+ > **当前版本**:v2.8.0(2026-05-16
6
6
 
7
7
  ---
8
8
 
@@ -478,9 +478,11 @@ AI "假执行"——声称读了规范,实际按惯性输出。没有强制约
478
478
  | ---- | ----------------------------------------------------------------------------------------------------------------------------------------- | --------- |
479
479
  | v1.x | 5 个 Skill 平铺 + 9 个 TPL 平铺 + 单一超长 copilot-instructions | ✅ 已发布 |
480
480
  | v2.0 | 规范模块化(13 条)+ 模板分层(universal/domains)+ 报告分类 + Pre-flight + 工具链门控 | ✅ 已发布 |
481
- | v2.1 | Skill 分级目录(core/sync/ops/domain)+ 多 AI 适配解耦(editors.json)+ 各 Skill USAGE.md + api-contract 真实响应 + 3 个 PLANNED 草稿补全 | ✅ 当前 |
482
- | v2.2 | dict-sync / permission-sync / code-fix 从 PLANNED → 转正(视后端接口稳定情况) | 规划中 |
483
- | v2.3 | CI 流水线接入(convention-audit 报告注入 PR 评论)+ Skill 版本感知 | 规划中 |
481
+ | v2.1 | Skill 分级目录(core/sync/ops/domain)+ 多 AI 适配解耦(editors.json)+ 各 Skill USAGE.md + api-contract 真实响应 + 3 个 PLANNED 草稿补全 | ✅ 已发布 |
482
+ | v2.2–2.3 | dict-sync / permission-sync / code-fix 从 PLANNED → 转正 + lint-skills 静态护栏 + MCP 自愈闭环 | 已发布 |
483
+ | v2.6 | business-doc-extract 语义触发 + 业务文档体系 + AGGrid/cid/defineColumns/renderOps 最终标准 + doctor-ui / validate 增强 | 已发布 |
484
+ | v2.7 | JH 组件文档全面修正 + MCP tools 单测覆盖 + 场景索引路由 + _mcp-guardrail 自愈闭环 + .gitattributes + 版本一致性自检 | ✅ 已发布 |
485
+ | v2.8 | Mock 架构体系固化(mock-architecture.md + _utils.ts 种子 + mock-clean CLI + validate mock 质量检查 + 规则修正) | ✅ 当前 |
484
486
 
485
487
  ---
486
488
 
@@ -140,7 +140,17 @@ wls_code_scan ← 概览:页面目录、API_CONFIG、文件完整性
140
140
 
141
141
  **用户典型话术**:"先 mock 一下"、"假数据"、"后端没好先能跑"
142
142
 
143
- **推荐**:`page-codegen` 的 mock-first 规则,生成假数据 + 注释好真实接口对接位置。
143
+ **推荐**:`page-codegen` 的 mock-first 规则。Mock 架构详见 `docs/mock-architecture.md`。
144
+
145
+ ```
146
+ mock/
147
+ ├── _utils.ts ← 共享工具(kit init 自动写入)
148
+ └── [业务域]/[模块].ts ← 按 src/views 第一级域分目录
149
+ ```
150
+
151
+ - 生成页面自动生成 `mock/[业务域]/[模块].ts`,import `../_utils` 共享工具
152
+ - 开关:`.env.dev` 中 `ENV_MOCK=true/false`,零污染切换
153
+ - 清理:`wl-skills mock-clean --domain [域]` 按域清理,`--all` 全量清理(保留 `_utils.ts`)
144
154
 
145
155
  ---
146
156
 
@@ -92,7 +92,7 @@ src/views/[域]/[模块]/[子模块]/[kebab-case-目录名]/
92
92
 
93
93
  - `pages.ts` 注册片段
94
94
  - **`reports/SYS_MENU_INFO.md`** — 集中式菜单配置,**追加写入**(见下方 §SYS_MENU_INFO 生成规则)
95
- - `mock/[页面kebab-name].ts`(项目根目录 `mock/` 下,`vite-plugin-mock` 自动加载,与 api.md 的 URL 和字段完全一致)
95
+ - `mock/[业务域]/[模块].ts`(项目根目录 `mock/` 下按域分目录,`vite-plugin-mock` 自动加载,与 api.md 的 URL 和字段完全一致;详见 `docs/mock-architecture.md`)
96
96
 
97
97
  ---
98
98
 
@@ -108,7 +108,7 @@ src/views/[域]/[模块]/[子模块]/[kebab-case-目录名]/
108
108
  6. 字典字段用 `logicType: BusLogicDataType.dict, logicValue: "dictCode"`
109
109
  7. 同时生成 api.md(基于 api-contract Skill 模板)
110
110
  8. 提供 pages.ts 注册片段
111
- 9. 同时在 `mock/` 目录下生成对应的 mock 文件(`MockMethod[]` + mockjs,URL 和字段与 api.md 一致,URL 必须带 `/dev-api` 前缀)
111
+ 9. 同时在 `mock/[业务域]/` 目录下生成对应的 mock 文件(`MockMethod[]` + mockjs,URL 和字段与 api.md 一致,URL 必须带 `/dev-api` 前缀)。业务域取 `src/views/` 下第一级目录名(如 `sale`、`mdata`)。mock 文件必须 `import { paginate, ok, pick, nowStr } from "../_utils"` 复用共享工具,不可自行重复定义
112
112
  10. **查询字段顺序**:`queryDef()` 中字段顺序必须与 page-spec `query` 数组顺序严格一致(即原型从左到右、从上到下)
113
113
  11. **表格列顺序**:`columnsDef()` 中列顺序必须与 page-spec `columns` 数组顺序严格一致(`selection` + `index` 在最前,其余按原型表头从左到右)
114
114
  12. **按钮顺序与颜色**:`toolbarDef()` 中按钮顺序和 `name`(颜色)必须与 page-spec `toolbar` 数组严格一致(`primary`=蓝底, `danger`=红色, `warning`=橙色, `default`=灰色; `plain: true`=线框)。**"新增"类按钮永远排第一**(如"新增"、"新增申请"),这是产品通用规范
@@ -119,8 +119,8 @@ src/views/[域]/[模块]/[子模块]/[kebab-case-目录名]/
119
119
  17. **按钮颜色映射**:按钮的 `type` 属性决定颜色,须根据原型按钮颜色或按钮语义映射(见下方 §按钮颜色映射表)
120
120
  18. **按钮必须可交互**:所有按钮的 `onClick` 必须有真实处理逻辑,禁止空函数 `() => {}`。通用交互实现见下方 §按钮交互实现规则
121
121
  19. **未知交互兜底**:当原型未提供交互细节、且无法从通用模式推断时,`onClick` 中使用 `ElMessage.info("需业务确认交互逻辑")` 作为占位
122
- 20. **生成后依赖自检**:代码生成完成后,检查 `package.json` 是否已安装生成代码所需的依赖(`mockjs`、`vite-plugin-mock`、`lodash-es`、`xlsx` 等),若缺失则提示用户执行安装命令。同时检查 `vite.config.ts` 是否已注册 `viteMockServe`、`mock/` 目录是否存在
123
- 21. **默认 Mock First**:新生成页面默认必须走 `vite-plugin-mock`。必须生成 `mock/[页面kebab-name].ts`,并确保 `API_CONFIG` 中每个 URL 都有对应 mock 端点;只有当用户明确要求关闭 mock 或 `.env.dev` 中 `ENV_MOCK=false` 时,才允许直接联调真实接口。
122
+ 20. **生成后依赖自检**:代码生成完成后,检查 `package.json` 是否已安装生成代码所需的依赖(`mockjs`、`vite-plugin-mock`、`lodash-es`、`xlsx` 等),若缺失则提示用户执行安装命令。同时检查 `vite.config.ts` 是否已注册 `viteMockServe`、`mock/_utils.ts` 是否存在(若不存在则从 kit 种子文件补充)
123
+ 21. **默认 Mock First**:新生成页面默认必须走 `vite-plugin-mock`。必须生成 `mock/[业务域]/[模块].ts`(import `../_utils` 共享工具),并确保 `API_CONFIG` 中每个 URL 都有对应 mock 端点;只有当用户明确要求关闭 mock 或 `.env.dev` 中 `ENV_MOCK=false` 时,才允许直接联调真实接口。
124
124
  22. **Mock URL 必须匹配真实请求**:`API_CONFIG` 保持真实接口路径(如 `/mdata/mdataModel/list`),mock 文件端点必须带 Vite 代理前缀(如 `/dev-api/mdata/mdataModel/list`),这样关闭 mock 后无需修改业务代码。
125
125
  23. **页面初始数据必须由 mock 提供**:列表页 `onMounted(() => select())` 后必须能显示模拟数据,不允许生成空白页等待后端接口;`list` 端点返回 `{ code: 2000, data: { records, total, size, current } }`。
126
126
  24. **必须使用 wk-skills-ui runtime 风格**:当项目安装了 `@agile-team/wk-skills-ui` 时,列表列定义必须使用 `defineColumns()`,操作列必须使用 `renderOps()`,状态/字典列优先使用 runtime 渲染器或 `logicType=dict` 自动映射;不可退回默认纯文本/空函数风格。
@@ -145,7 +145,7 @@ src/views/[域]/[模块]/[子模块]/[kebab-case-目录名]/
145
145
  11. **❌ 禁止表单控件宽度不统一**:`jh-select`、`jh-date`、`el-input-number`、`jh-file-upload` 默认宽度可能与 `el-input` 不一致,必须在 scoped style 中用 `:deep()` 统一设为 `width: 100%`(详见 §表单页 UI 细节规范)
146
146
  12. **❌ 禁止表单页无滚动**:独立路由表单页内容超出视口时必须可滚动,`.app-page-container` 须设 `overflow-y: auto`(**不要加 `height: 100%`,全局已有 `height: calc(100vh - 100px)`,叠加会导致双滚动条**)
147
147
  13. **❌ 禁止内联 style 散落**:所有页面/组件样式统一写在 `index.scss` 中(便于复用和移动),不可在 template 中大量使用内联 `style="..."`
148
- 14. **❌ 禁止生成无 mock 的页面**:只写 `API_CONFIG` 但不写 `mock/*.ts` 属于生成失败。
148
+ 14. **❌ 禁止生成无 mock 的页面**:只写 `API_CONFIG` 但不写 `mock/[业务域]/*.ts` 属于生成失败。mock 文件必须按域分目录、import `_utils` 共享工具(详见 `docs/mock-architecture.md`)。
149
149
  15. **❌ 禁止生成空 onClick**:`onClick: () => {}` 属于生成失败;未知逻辑也必须用 `ElMessage.info(...)` 明示。
150
150
  16. **❌ 禁止忽略 wk-skills-ui**:项目已安装 `@agile-team/wk-skills-ui` 时,不使用 `defineColumns/renderOps` 属于生成失败。
151
151
  17. **❌ 禁止 BaseTable 非 AGGrid**:业务列表中 `<BaseTable>` 未写 `render-type="agGrid"` 或缺少 `cid/:cid` 属于生成失败。
@@ -0,0 +1,321 @@
1
+ # Mock 架构规范
2
+
3
+ > **适用范围**:所有基于 wl-skills-kit 的 Vue 3 业务子应用
4
+ > **技术依赖**:`vite-plugin-mock` + `mockjs`
5
+ > **源码参考**:wl-mdata 项目实践(v2.7.x 基线)
6
+
7
+ ---
8
+
9
+ ## 一、设计目标
10
+
11
+ | 目标 | 实现方式 |
12
+ |---|---|
13
+ | **与页面代码完全解耦** | Mock 文件独立放在 `mock/` 目录,不在 `src/views` 中 import 任何 mock |
14
+ | **开关零污染** | `ENV_MOCK=false` → vite-plugin-mock 整体不挂载,不拦截任何请求 |
15
+ | **按业务模块自治** | 每个模块一个 mock 文件,删除模块只需删除对应 mock 文件 |
16
+ | **共享工具复用** | `_utils.ts` 提供标准响应构造器,子模块 import 而非重复定义 |
17
+ | **会话内数据可见** | `let STORE = [...]` 可变数组,CRUD 操作直接修改内存,下次查询可见 |
18
+ | **跨会话重置** | dev server 重启时模块重新加载,STORE 恢复初始状态 |
19
+ | **真实接口无缝切换** | `API_CONFIG` 保持真实路径,mock 端点带 `/dev-api` 前缀,关闭 mock 后无需改代码 |
20
+
21
+ ---
22
+
23
+ ## 二、目录结构
24
+
25
+ ```
26
+ 项目根目录/
27
+ ├── .env.dev ← ENV_MOCK=true(Mock 总开关)
28
+ ├── vite.config.ts ← viteMockServe({ mockPath: "./mock", enable: ... })
29
+ ├── mock/
30
+ │ ├── _utils.ts ← 共享工具(pageResult / ok / paginate / nowStr / pick)
31
+ │ └── [业务域]/ ← 镜像 src/views 第一级目录
32
+ │ ├── [模块].ts ← 一个模块可覆盖多个相关页面
33
+ │ ├── [模块].ts
34
+ │ └── ...
35
+ └── src/
36
+ └── views/
37
+ └── [业务域]/
38
+ └── [模块]/
39
+ └── [页面]/
40
+ ├── index.vue
41
+ ├── data.ts ← API_CONFIG 保持真实路径
42
+ └── api.md
43
+ ```
44
+
45
+ **命名约定**:
46
+
47
+ | src/views 路径 | mock 文件路径 | 说明 |
48
+ |---|---|---|
49
+ | `src/views/mdata/model/` | `mock/mdata/model.ts` | 一个模块一个文件 |
50
+ | `src/views/mdata/management/` | `mock/mdata/master.ts` | 可按业务语义命名 |
51
+ | `src/views/sale/customer/` | `mock/sale/customer.ts` | 镜像第一级域 |
52
+ | `src/views/sale/contract/` | `mock/sale/contract.ts` | 同域不同模块 |
53
+
54
+ > **一个 mock 文件可覆盖同模块下多个页面的接口**,只要它们属于同一业务实体(如"客户管理"和"客户申请"可共用 `mock/sale/customer.ts`)。
55
+
56
+ ---
57
+
58
+ ## 三、`_utils.ts` 共享工具
59
+
60
+ > 该文件由 `wl-skills init` 自动写入 `mock/_utils.ts`。无 `export default`,vite-plugin-mock 会安全跳过。
61
+
62
+ ```typescript
63
+ import Mock from "mockjs";
64
+
65
+ export const Random = Mock.Random;
66
+ export const pick = <T>(arr: T[]): T => Random.pick(arr) as T;
67
+ export const bool = () => Random.boolean();
68
+
69
+ /** 标准分页响应 */
70
+ export function pageResult(records: any[], total: number) {
71
+ return { code: 2000, message: "操作成功", data: { records, total } };
72
+ }
73
+
74
+ /** 标准成功响应 */
75
+ export function ok(data: any = null) {
76
+ return { code: 2000, message: "操作成功", data };
77
+ }
78
+
79
+ /** 通用分页截取(兼容 pageNo/current、pageSize/size) */
80
+ export function paginate(pool: any[], query: any) {
81
+ const current = Number(query?.current || query?.pageNo) || 1;
82
+ const size = Number(query?.size || query?.pageSize) || 10;
83
+ const start = (current - 1) * size;
84
+ return pageResult(pool.slice(start, start + size), pool.length);
85
+ }
86
+
87
+ /** 当前时间字符串 */
88
+ export function nowStr() {
89
+ return new Date().toLocaleString("zh-CN", { hour12: false }).replace(/\//g, "-");
90
+ }
91
+ ```
92
+
93
+ 所有 mock 模块文件统一通过 `import { paginate, ok, pick, nowStr } from "../_utils"` 引入。
94
+
95
+ ---
96
+
97
+ ## 四、Mock 文件编写规范
98
+
99
+ ### 4.1 文件头注释
100
+
101
+ 每个 mock 文件顶部必须声明覆盖的业务模块和接口:
102
+
103
+ ```typescript
104
+ /**
105
+ * [业务模块名] Mock
106
+ * 覆盖接口:[接口前缀1] / [接口前缀2]
107
+ */
108
+ import type { MockMethod } from "vite-plugin-mock";
109
+ import Mock from "mockjs";
110
+ import { paginate, ok, pick, nowStr } from "../_utils";
111
+ ```
112
+
113
+ ### 4.2 STORE 模式
114
+
115
+ ```typescript
116
+ // ─── 数据生成器 ──────────────────────────────────────────────────
117
+ function genRecord() {
118
+ return {
119
+ id: Mock.Random.id(),
120
+ code: `MDM${Mock.Random.string("number", 8)}`,
121
+ name: Mock.Random.cword(4, 10),
122
+ status: pick(["ACTIVE", "DRAFT", "FROZEN"]),
123
+ createBy: Mock.Random.cname(),
124
+ createTime: Mock.Random.datetime("yyyy-MM-dd HH:mm:ss"),
125
+ };
126
+ }
127
+
128
+ // ─── 可变存储(dev server 会话内持久化)──────────────────────────
129
+ let STORE: any[] = Array.from({ length: 50 }, genRecord);
130
+ ```
131
+
132
+ **核心规则**:
133
+ - 用 `let`(非 `const`)声明 STORE,允许 splice/unshift/assign
134
+ - 生成器函数 `genRecord()` 封装单条数据的随机逻辑
135
+ - 初始数据量建议 20-100 条,支持分页验证
136
+
137
+ ### 4.3 端点编写
138
+
139
+ ```typescript
140
+ export default [
141
+ // ── 列表(分页)──
142
+ {
143
+ url: "/dev-api/[服务]/[资源]/list",
144
+ method: "get",
145
+ response: ({ query }: any) => paginate(STORE, query),
146
+ },
147
+
148
+ // ── 新增 ──
149
+ {
150
+ url: "/dev-api/[服务]/[资源]/save",
151
+ method: "post",
152
+ response: ({ body }: any) => {
153
+ STORE.unshift({ ...genRecord(), ...body, id: Mock.Random.id(), createTime: nowStr() });
154
+ return ok(null);
155
+ },
156
+ },
157
+
158
+ // ── 编辑 ──
159
+ {
160
+ url: "/dev-api/[服务]/[资源]/update",
161
+ method: "post",
162
+ response: ({ body }: any) => {
163
+ const idx = STORE.findIndex((r) => r.id === body?.id);
164
+ if (idx >= 0) Object.assign(STORE[idx], body, { updateTime: nowStr() });
165
+ return ok(null);
166
+ },
167
+ },
168
+
169
+ // ── 删除 ──
170
+ {
171
+ url: "/dev-api/[服务]/[资源]/remove",
172
+ method: "post",
173
+ response: ({ body }: any) => {
174
+ const ids = Array.isArray(body?.ids) ? body.ids : [body?.id];
175
+ ids.forEach((id: string) => {
176
+ const idx = STORE.findIndex((r) => r.id === id);
177
+ if (idx >= 0) STORE.splice(idx, 1);
178
+ });
179
+ return ok(null);
180
+ },
181
+ },
182
+
183
+ // ── 详情 ──
184
+ {
185
+ url: "/dev-api/[服务]/[资源]/getById",
186
+ method: "get",
187
+ response: ({ query }: any) => ok(STORE.find((r) => r.id === query?.id) || null),
188
+ },
189
+ ] as MockMethod[];
190
+ ```
191
+
192
+ ### 4.4 端点覆盖检查
193
+
194
+ **每个 `API_CONFIG` 的 key 都必须在 mock 文件中有对应端点**,零遗漏。
195
+
196
+ | 操作 | STORE 修改方式 |
197
+ |---|---|
198
+ | 删除 | `STORE.splice(idx, 1)` |
199
+ | 新增 | `STORE.unshift({ ...genRecord(), ...body, id })` |
200
+ | 编辑 | `Object.assign(STORE[idx], body)` |
201
+ | 启用/停用 | 修改 `item.status` |
202
+ | 提交/审批 | 修改 `item.approvalStatus` |
203
+ | 作废 | `STORE.splice(idx, 1)` 或修改状态 |
204
+
205
+ > ❌ 禁止端点只返回 `{ code: 2000 }` 不修改 STORE — 否则 `select()` 刷新后看不到变化。
206
+
207
+ ---
208
+
209
+ ## 五、开关机制
210
+
211
+ ### 5.1 环境变量
212
+
213
+ ```bash
214
+ # .env.dev
215
+ ENV_MOCK=true # true = 启用 mock,false = 关闭 mock 走真实接口
216
+ ```
217
+
218
+ ### 5.2 Vite 配置
219
+
220
+ ```typescript
221
+ // vite.config.ts
222
+ import { viteMockServe } from "vite-plugin-mock";
223
+
224
+ plugins: [
225
+ viteMockServe({
226
+ mockPath: "./mock",
227
+ enable: command === "serve" && config.ENV_MOCK !== "false",
228
+ logger: true,
229
+ }),
230
+ ]
231
+ ```
232
+
233
+ **关键**:`enable` 判断只在 `serve` 模式且 `ENV_MOCK` 不为 `"false"` 时挂载。生产构建永远不包含 mock。
234
+
235
+ ### 5.3 切换流程
236
+
237
+ ```
238
+ 开发阶段(Mock):ENV_MOCK=true → 所有接口走 mock,Network 可见
239
+ 联调阶段(真实):ENV_MOCK=false → mock 插件不加载,接口走 proxy → 后端
240
+ 部分联调: ENV_MOCK=false + 仅保留需要 mock 的端点(手动注释其余)
241
+ ```
242
+
243
+ > 切换时**无需修改任何页面代码**。`API_CONFIG` 始终保持真实路径,mock 端点的 `/dev-api` 前缀由 Vite proxy 统一处理。
244
+
245
+ ---
246
+
247
+ ## 六、一键清理
248
+
249
+ CLI 提供 `mock-clean` 命令,支持按模块清理或全量清理:
250
+
251
+ ```bash
252
+ # 清理指定业务域的 mock
253
+ npx @agile-team/wl-skills-kit mock-clean --domain mdata
254
+
255
+ # 清理全部 mock(保留 _utils.ts)
256
+ npx @agile-team/wl-skills-kit mock-clean --all
257
+
258
+ # 预览将要删除的文件(dry-run)
259
+ npx @agile-team/wl-skills-kit mock-clean --all --dry-run
260
+ ```
261
+
262
+ 清理后建议:
263
+ 1. 将 `.env.dev` 中 `ENV_MOCK` 改为 `false`
264
+ 2. 确认 `vite.config.ts` 中 proxy 已配置正确的后端地址
265
+ 3. 运行 `wl-skills validate` 确认页面无 mock 依赖残留
266
+
267
+ ---
268
+
269
+ ## 七、validate 检查项
270
+
271
+ `wl-skills validate` 对 mock 的检查(自动执行):
272
+
273
+ | 检查项 | 级别 | 说明 |
274
+ |---|---|---|
275
+ | `mock/_utils.ts` 是否存在 | warn | 缺失则提示 `wl-skills init` 补充 |
276
+ | API_CONFIG URL 是否有对应 mock 端点 | warn | 每个 URL 在 mock 文件中必须有 `/dev-api` 前缀的端点 |
277
+ | mock 文件是否 import `_utils` | info | 未使用共享工具则提示 |
278
+ | mock 文件是否按域分目录 | info | 扁平放在 `mock/` 根目录则提示迁移 |
279
+
280
+ ---
281
+
282
+ ## 八、常见问题
283
+
284
+ ### Q: mock 和真实接口返回格式不一致怎么办?
285
+
286
+ 确保 `_utils.ts` 中 `pageResult` / `ok` 的返回格式与后端统一:
287
+ ```typescript
288
+ // 如果后端用 code: 200
289
+ export function ok(data: any = null) {
290
+ return { code: 200, message: "操作成功", data };
291
+ }
292
+
293
+ // 如果后端用 code: 2000(平台默认)
294
+ export function ok(data: any = null) {
295
+ return { code: 2000, message: "操作成功", data };
296
+ }
297
+ ```
298
+
299
+ > 在 `_utils.ts` 中统一修改一处即可,无需改每个 mock 文件。
300
+
301
+ ### Q: 多个页面共享同一套数据怎么办?
302
+
303
+ 同一个 mock 模块文件中声明一个 STORE,多个端点共享。例如"客户列表"和"客户申请"可共享 `CUSTOMER_STORE`。
304
+
305
+ ### Q: 某个接口需要走真实后端,其余走 mock?
306
+
307
+ 方案一:删除该接口在 mock 文件中的端点,vite-plugin-mock 找不到匹配就会放行到 proxy。
308
+ 方案二:保持 `ENV_MOCK=true`,在 vite proxy 中对特定路径单独配置 target。
309
+
310
+ ---
311
+
312
+ ## 九、架构要点速查
313
+
314
+ | 能力 | 实现方式 |
315
+ |---|---|
316
+ | **新增数据可见** | 每个模块维护 `let STORE = [...]` 可变数组,save 用 `unshift` 置顶,下次 list 查询即可看到 |
317
+ | **关闭 mock 零污染** | `ENV_MOCK=false` → vite-plugin-mock 整体不挂载,不会有任何 mock handler 拦截真实请求 |
318
+ | **按模块独立** | 每个文件自治,未来删某业务模块 mock 只删对应文件 |
319
+ | **共享工具复用** | `_utils.ts` 提供 `pageResult` / `ok` / `paginate` / `nowStr` / `pick`,子模块 import 而非重复定义 |
320
+ | **跨会话重置** | dev server 重启时内存清空,mock 数据恢复初始状态(符合预期) |
321
+ | **URL 零修改切换** | `API_CONFIG` 保持真实路径,mock URL 带 `/dev-api` 前缀,关闭 mock 后业务代码无需任何改动 |
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Mock 公共工具函数
3
+ * 由 wl-skills-kit 提供,所有 mock 模块文件统一 import 此文件。
4
+ * 注意:本文件无 export default,vite-plugin-mock 会安全跳过,不会报错。
5
+ */
6
+ import Mock from "mockjs";
7
+
8
+ export const Random = Mock.Random;
9
+ export const pick = <T>(arr: T[]): T => Random.pick(arr) as T;
10
+ export const bool = () => Random.boolean();
11
+
12
+ /** 标准分页响应 */
13
+ export function pageResult(records: any[], total: number) {
14
+ return { code: 2000, message: "操作成功", data: { records, total } };
15
+ }
16
+
17
+ /** 标准成功响应 */
18
+ export function ok(data: any = null) {
19
+ return { code: 2000, message: "操作成功", data };
20
+ }
21
+
22
+ /** 通用分页截取(兼容 pageNo/current、pageSize/size 两种参数名) */
23
+ export function paginate(pool: any[], query: any) {
24
+ const current = Number(query?.current || query?.pageNo) || 1;
25
+ const size = Number(query?.size || query?.pageSize) || 10;
26
+ const start = (current - 1) * size;
27
+ return pageResult(pool.slice(start, start + size), pool.length);
28
+ }
29
+
30
+ /** 当前时间字符串 */
31
+ export function nowStr() {
32
+ return new Date()
33
+ .toLocaleString("zh-CN", { hour12: false })
34
+ .replace(/\//g, "-");
35
+ }
@@ -1,178 +1,178 @@
1
- 'use strict'
2
-
3
- const { queryDictModules, saveDictModule, saveDictItem } = require('../api/dictApi')
4
-
5
- /**
6
- * 从字典树查询响应中提取模块列表
7
- * 响应结构: { dictionary: { children: DictModule[] } }
8
- *
9
- * @param {any} data - queryDictModules 返回的 result.data
10
- * @returns {object[]}
11
- */
12
- function extractModules(data) {
13
- if (!data) return []
14
- if (data.dictionary && Array.isArray(data.dictionary.children)) {
15
- return data.dictionary.children
16
- }
17
- if (Array.isArray(data)) return data
18
- return []
19
- }
20
-
21
- /**
22
- * wls_dict_query 工具处理器
23
- * 查询当前应用的所有字典模块及字典项
24
- *
25
- * @param {object} config
26
- * @returns {Promise<string>}
27
- */
28
- async function handleDictQuery(config) {
29
- const result = await queryDictModules(config)
30
-
31
- if (!result.ok) {
32
- return `❌ 查询字典失败: ${result.error} (code: ${result.code})`
33
- }
34
-
35
- const modules = extractModules(result.data)
36
-
37
- if (modules.length === 0) {
38
- return '✅ 字典查询成功,当前应用暂无字典数据'
39
- }
40
-
41
- return `✅ 字典查询成功,共 ${modules.length} 个模块\n\n${JSON.stringify(modules, null, 2)}`
42
- }
43
-
44
- /**
45
- * wls_dict_upsert 工具处理器
46
- * 新增或更新字典模块及字典项(完整幂等流程)
47
- *
48
- * 流程:
49
- * ① 查询现有模块,检查 strSn 是否已存在
50
- * ② 若不存在:创建模块(data=null)→ 重新查询 → 用 strSn 定位拿 id
51
- * ③ 若已存在:跳过模块创建,直接取已有 id
52
- * ④ 遍历 items,跳过 strSn 已存在的,其余逐条创建
53
- *
54
- * @param {{ module: object, items?: object[] }} args
55
- * @param {object} config
56
- * @returns {Promise<string>}
57
- */
58
- async function handleDictUpsert(args, config) {
59
- const { module: moduleBody, items = [] } = args
60
-
61
- if (!moduleBody || !moduleBody.strSn) {
62
- return '❌ 参数错误:module.strSn 必填'
63
- }
64
- if (!moduleBody.strName) {
65
- return '❌ 参数错误:module.strName 必填'
66
- }
67
-
68
- // ① 查询现有模块
69
- const queryResult = await queryDictModules(config)
70
- if (!queryResult.ok) {
71
- return `❌ 查询字典失败: ${queryResult.error}`
72
- }
73
-
74
- const existingModules = extractModules(queryResult.data)
75
- let targetModule = existingModules.find((m) => m.strSn === moduleBody.strSn)
76
- let moduleAction
77
-
78
- // ② 模块不存在时创建
79
- if (!targetModule) {
80
- const saveBody = {
81
- strSn: moduleBody.strSn,
82
- strName: moduleBody.strName,
83
- sortPriority: moduleBody.sortPriority != null ? String(moduleBody.sortPriority) : '1',
84
- strLevel: 2,
85
- }
86
-
87
- const createResult = await saveDictModule(saveBody, config)
88
- if (!createResult.ok) {
89
- return `❌ 创建字典模块失败: ${createResult.error}`
90
- }
91
-
92
- // ③ 重新查询(data=null,只能靠 re-query 拿 id)
93
- const reQueryResult = await queryDictModules(config)
94
- if (!reQueryResult.ok) {
95
- return `❌ 重新查询字典失败: ${reQueryResult.error}`
96
- }
97
-
98
- const freshModules = extractModules(reQueryResult.data)
99
- targetModule = freshModules.find((m) => m.strSn === moduleBody.strSn)
100
-
101
- if (!targetModule) {
102
- return `❌ 字典模块创建后未能找到(strSn="${moduleBody.strSn}"),请在字典管理后台确认`
103
- }
104
-
105
- moduleAction = '✅ 已创建'
106
- } else {
107
- moduleAction = '⏭ 已存在(跳过创建)'
108
- }
109
-
110
- const moduleId = targetModule.id
111
-
112
- // ④ 处理字典项
113
- const existingItems = Array.isArray(targetModule.dictionaries)
114
- ? targetModule.dictionaries
115
- : []
116
- const existingSns = new Set(existingItems.map((i) => i.strSn))
117
-
118
- const itemResults = []
119
-
120
- for (const item of items) {
121
- if (existingSns.has(item.strSn)) {
122
- itemResults.push({
123
- strSn: item.strSn,
124
- strName: item.strName || '',
125
- status: '⏭ 已存在(跳过)',
126
- })
127
- continue
128
- }
129
-
130
- const itemBody = {
131
- moduleId,
132
- strSn: item.strSn,
133
- strName: item.strName,
134
- strLevel: 2,
135
- dtlValue: item.dtlValue != null ? item.dtlValue : '',
136
- dtlValueRequired: item.dtlValueRequired || false,
137
- dtlValue2Required: item.dtlValue2Required || false,
138
- dtlValue3Required: item.dtlValue3Required || false,
139
- dtlValue4Required: item.dtlValue4Required || false,
140
- }
141
-
142
- const createResult = await saveDictItem(itemBody, config)
143
- if (createResult.ok) {
144
- itemResults.push({ strSn: item.strSn, strName: item.strName || '', status: '✅ 已创建' })
145
- } else {
146
- itemResults.push({
147
- strSn: item.strSn,
148
- strName: item.strName || '',
149
- status: `❌ 失败: ${createResult.error}`,
150
- })
151
- }
152
- }
153
-
154
- const createdCount = itemResults.filter((r) => r.status.startsWith('✅')).length
155
- const skippedCount = itemResults.filter((r) => r.status.startsWith('⏭')).length
156
- const failedCount = itemResults.filter((r) => r.status.startsWith('❌')).length
157
-
158
- let output = `字典模块 "${moduleBody.strSn}" (${moduleBody.strName}):${moduleAction}\n`
159
- output += `模块 ID:${moduleId}\n`
160
-
161
- if (items.length > 0) {
162
- output += `字典项:创建 ${createdCount},跳过 ${skippedCount},失败 ${failedCount}\n\n`
163
- output += '| strSn | strName | 状态 |\n'
164
- output += '|---|---|---|\n'
165
- for (const r of itemResults) {
166
- output += `| ${r.strSn} | ${r.strName} | ${r.status} |\n`
167
- }
168
- }
169
-
170
- return output
171
- }
172
-
173
- module.exports = {
174
- handleDictQuery,
175
- handleDictUpsert,
176
- // 导出纯工具函数供单测覆盖
177
- _internal: { extractModules },
178
- }
1
+ 'use strict'
2
+
3
+ const { queryDictModules, saveDictModule, saveDictItem } = require('../api/dictApi')
4
+
5
+ /**
6
+ * 从字典树查询响应中提取模块列表
7
+ * 响应结构: { dictionary: { children: DictModule[] } }
8
+ *
9
+ * @param {any} data - queryDictModules 返回的 result.data
10
+ * @returns {object[]}
11
+ */
12
+ function extractModules(data) {
13
+ if (!data) return []
14
+ if (data.dictionary && Array.isArray(data.dictionary.children)) {
15
+ return data.dictionary.children
16
+ }
17
+ if (Array.isArray(data)) return data
18
+ return []
19
+ }
20
+
21
+ /**
22
+ * wls_dict_query 工具处理器
23
+ * 查询当前应用的所有字典模块及字典项
24
+ *
25
+ * @param {object} config
26
+ * @returns {Promise<string>}
27
+ */
28
+ async function handleDictQuery(config) {
29
+ const result = await queryDictModules(config)
30
+
31
+ if (!result.ok) {
32
+ return `❌ 查询字典失败: ${result.error} (code: ${result.code})`
33
+ }
34
+
35
+ const modules = extractModules(result.data)
36
+
37
+ if (modules.length === 0) {
38
+ return '✅ 字典查询成功,当前应用暂无字典数据'
39
+ }
40
+
41
+ return `✅ 字典查询成功,共 ${modules.length} 个模块\n\n${JSON.stringify(modules, null, 2)}`
42
+ }
43
+
44
+ /**
45
+ * wls_dict_upsert 工具处理器
46
+ * 新增或更新字典模块及字典项(完整幂等流程)
47
+ *
48
+ * 流程:
49
+ * ① 查询现有模块,检查 strSn 是否已存在
50
+ * ② 若不存在:创建模块(data=null)→ 重新查询 → 用 strSn 定位拿 id
51
+ * ③ 若已存在:跳过模块创建,直接取已有 id
52
+ * ④ 遍历 items,跳过 strSn 已存在的,其余逐条创建
53
+ *
54
+ * @param {{ module: object, items?: object[] }} args
55
+ * @param {object} config
56
+ * @returns {Promise<string>}
57
+ */
58
+ async function handleDictUpsert(args, config) {
59
+ const { module: moduleBody, items = [] } = args
60
+
61
+ if (!moduleBody || !moduleBody.strSn) {
62
+ return '❌ 参数错误:module.strSn 必填'
63
+ }
64
+ if (!moduleBody.strName) {
65
+ return '❌ 参数错误:module.strName 必填'
66
+ }
67
+
68
+ // ① 查询现有模块
69
+ const queryResult = await queryDictModules(config)
70
+ if (!queryResult.ok) {
71
+ return `❌ 查询字典失败: ${queryResult.error}`
72
+ }
73
+
74
+ const existingModules = extractModules(queryResult.data)
75
+ let targetModule = existingModules.find((m) => m.strSn === moduleBody.strSn)
76
+ let moduleAction
77
+
78
+ // ② 模块不存在时创建
79
+ if (!targetModule) {
80
+ const saveBody = {
81
+ strSn: moduleBody.strSn,
82
+ strName: moduleBody.strName,
83
+ sortPriority: moduleBody.sortPriority != null ? String(moduleBody.sortPriority) : '1',
84
+ strLevel: 2,
85
+ }
86
+
87
+ const createResult = await saveDictModule(saveBody, config)
88
+ if (!createResult.ok) {
89
+ return `❌ 创建字典模块失败: ${createResult.error}`
90
+ }
91
+
92
+ // ③ 重新查询(data=null,只能靠 re-query 拿 id)
93
+ const reQueryResult = await queryDictModules(config)
94
+ if (!reQueryResult.ok) {
95
+ return `❌ 重新查询字典失败: ${reQueryResult.error}`
96
+ }
97
+
98
+ const freshModules = extractModules(reQueryResult.data)
99
+ targetModule = freshModules.find((m) => m.strSn === moduleBody.strSn)
100
+
101
+ if (!targetModule) {
102
+ return `❌ 字典模块创建后未能找到(strSn="${moduleBody.strSn}"),请在字典管理后台确认`
103
+ }
104
+
105
+ moduleAction = '✅ 已创建'
106
+ } else {
107
+ moduleAction = '⏭ 已存在(跳过创建)'
108
+ }
109
+
110
+ const moduleId = targetModule.id
111
+
112
+ // ④ 处理字典项
113
+ const existingItems = Array.isArray(targetModule.dictionaries)
114
+ ? targetModule.dictionaries
115
+ : []
116
+ const existingSns = new Set(existingItems.map((i) => i.strSn))
117
+
118
+ const itemResults = []
119
+
120
+ for (const item of items) {
121
+ if (existingSns.has(item.strSn)) {
122
+ itemResults.push({
123
+ strSn: item.strSn,
124
+ strName: item.strName || '',
125
+ status: '⏭ 已存在(跳过)',
126
+ })
127
+ continue
128
+ }
129
+
130
+ const itemBody = {
131
+ moduleId,
132
+ strSn: item.strSn,
133
+ strName: item.strName,
134
+ strLevel: 2,
135
+ dtlValue: item.dtlValue != null ? item.dtlValue : '',
136
+ dtlValueRequired: item.dtlValueRequired || false,
137
+ dtlValue2Required: item.dtlValue2Required || false,
138
+ dtlValue3Required: item.dtlValue3Required || false,
139
+ dtlValue4Required: item.dtlValue4Required || false,
140
+ }
141
+
142
+ const createResult = await saveDictItem(itemBody, config)
143
+ if (createResult.ok) {
144
+ itemResults.push({ strSn: item.strSn, strName: item.strName || '', status: '✅ 已创建' })
145
+ } else {
146
+ itemResults.push({
147
+ strSn: item.strSn,
148
+ strName: item.strName || '',
149
+ status: `❌ 失败: ${createResult.error}`,
150
+ })
151
+ }
152
+ }
153
+
154
+ const createdCount = itemResults.filter((r) => r.status.startsWith('✅')).length
155
+ const skippedCount = itemResults.filter((r) => r.status.startsWith('⏭')).length
156
+ const failedCount = itemResults.filter((r) => r.status.startsWith('❌')).length
157
+
158
+ let output = `字典模块 "${moduleBody.strSn}" (${moduleBody.strName}):${moduleAction}\n`
159
+ output += `模块 ID:${moduleId}\n`
160
+
161
+ if (items.length > 0) {
162
+ output += `字典项:创建 ${createdCount},跳过 ${skippedCount},失败 ${failedCount}\n\n`
163
+ output += '| strSn | strName | 状态 |\n'
164
+ output += '|---|---|---|\n'
165
+ for (const r of itemResults) {
166
+ output += `| ${r.strSn} | ${r.strName} | ${r.status} |\n`
167
+ }
168
+ }
169
+
170
+ return output
171
+ }
172
+
173
+ module.exports = {
174
+ handleDictQuery,
175
+ handleDictUpsert,
176
+ // 导出纯工具函数供单测覆盖
177
+ _internal: { extractModules },
178
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@agile-team/wl-skills-kit",
3
- "version": "2.7.3",
4
- "description": "AI Skill 模板包 v2.7.3 — 13 条编码规范 + 10 个 AI Skill + 17 个 MCP Tool,一条命令导入 Vue 3 项目",
3
+ "version": "2.8.0",
4
+ "description": "AI Skill 模板包 v2.8.0 — 13 条编码规范 + 10 个 AI Skill + 17 个 MCP Tool + Mock 架构体系,一条命令导入 Vue 3 项目",
5
5
  "main": "./bin/wl-skills.js",
6
6
  "bin": {
7
7
  "wl-skills": "bin/wl-skills.js"
@@ -75,4 +75,4 @@
75
75
  "eslint --fix --no-cache"
76
76
  ]
77
77
  }
78
- }
78
+ }