@agile-team/wl-skills-kit 2.2.0 → 2.3.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,46 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.3.0] - 2026-04-27
4
+
5
+ ### 🤖 MCP Server 内置(菜单 & 字典自动同步)
6
+
7
+ #### MCP Server
8
+ - 新增 `mcp/` 目录(随包发布),内置 MCP stdio server(纯 Node.js,无三方依赖,兼容 Node 16+)
9
+ - 暴露 4 个 Tools:`wls_menu_query` / `wls_menu_upsert` / `wls_dict_query` / `wls_dict_upsert`
10
+ - `wls_dict_upsert` 内部自动处理 dict module `data=null` 问题(创建后自动 re-query 拿 id,再创建字典项)
11
+ - `mcp/config.js` 从 `.github/skills/sync/env.local.json` 读取配置,占位值校验友好提示
12
+
13
+ #### CLI 更新
14
+ - `wl-skills init` / `update` 新增 Step 2.5:自动生成 `.cursor/mcp.json` 和 `.claude/settings.json`(已存在则跳过)
15
+ - `package.json` 的 `files` 字段加入 `mcp/`,确保随包发布
16
+ - `--help` 保护路径说明补充 MCP 配置文件条目
17
+
18
+ #### 配置扩展
19
+ - `files/.github/skills/sync/env.local.json` 模板新增 `menu.domainId` 字段(MCP wls_menu_query 使用)
20
+
21
+ #### 文档同步
22
+ - `files/.github/guides/architecture.md`:第 8 节新增 MCP 模式说明;第 4.2 节 dict-sync 状态改为 ✅;bin 工作流补 Step 5;版本表更新至 v2.3.0
23
+ - `files/.github/guides/usage.md`:Skill 速览表 menu-sync / dict-sync 行补充"MCP 自动模式"说明;FAQ 新增 MCP 配置问题
24
+ - `README.md`:新增"MCP Server"章节(工具清单 + 配置说明 + 效率对比表)
25
+ - `docs/mcp建议.md`:新增第七节(已确认接口清单),更新阶段一目录结构为实际 .js 文件
26
+
27
+ ---
28
+
29
+ ## [2.2.0] - 2026-04-27
30
+
31
+ ### 🔄 sync-version.js 扩展 + 文档修复
32
+
33
+ #### sync-version.js 扩展
34
+ - 新增 `SKILL_COUNT` 常量(值 8),改一处自动同步到所有编辑器适配文件中的 Skill 数量描述
35
+ - 新增同步目标:`package.json description`、`files/.github/skills/_compat/headers/cursor-mdc.txt`、`kiro.txt`、`trae.txt`
36
+
37
+ #### 流程文档更新
38
+ - `files/.github/guides/usage.md`:完整流水线补充 step 5(dict-sync)和 step 7(code-fix)
39
+ - `files/.github/standards/12-base-table.md`:新增批量 cid 碰撞预防代码块
40
+ - `files/.github/skills/core/convention-audit/SKILL.md`:Rule 12 旧格式 cid 降级为 🟡 偏差(迁移豁免)
41
+
42
+ ---
43
+
3
44
  ## [2.1.5] - 2026-04-27
4
45
 
5
46
  ### 🔄 版本自动同步 + 发布流程优化
package/README.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@agile-team/wl-skills-kit?label=wl-skills-kit&color=brightgreen)](https://www.npmjs.com/package/@agile-team/wl-skills-kit)
4
4
 
5
- **AI Skill 模板包 v2.2.0** — 一条命令尗13 条编码规范、8 个 AI Skill、组件文档、领域样例导入 Vue 3 项目。
5
+ **AI Skill 模板包 v2.3.0** — 一条命令导入 13 条编码规范、8 个 AI Skill、组件文档、领域样例到 Vue 3 项目。
6
6
 
7
- 让 AI 编辑器(Copilot / Cursor / Windsurf / Claude Code / Cline / Kiro / Trae / 通用 Agents)**真正理解项目规范**,从原型/详设到完整页面代码全流程自动化。
7
+ 让 AI 编辑器(Copilot / Cursor / Windsurf / Claude Code / Cline / Kiro / Trae / 通用 Agents)**真正理解项目规范**,从原型/详设到完整页面代码全流程自动化。内置 MCP Server 实现菜单和字典自动同步(对话轮数 ≤ 2)。
8
8
 
9
9
  ---
10
10
 
@@ -12,10 +12,11 @@
12
12
 
13
13
  ```bash
14
14
  npx @robot-admin/git-standards init # 工程化前置(必须)
15
- npx @agile-team/wl-skills-kit # 安装 AI 体系
15
+ npx @agile-team/wl-skills-kit # 安装 AI 体系(自动生成 MCP 配置)
16
+ # 填写 .github/skills/sync/env.local.json(token / domainId)
16
17
  # 在 AI 对话中:
17
- "扫描 docs/prototypes/ 下的原型生成页面清单"
18
- "基于上一步生成所有 api.md,再 codegen 出页面"
18
+ "扩展菜单"
19
+ "加字典"
19
20
  ```
20
21
 
21
22
  ---
@@ -187,6 +188,46 @@ npx @agile-team/wl-skills-kit update
187
188
 
188
189
  ---
189
190
 
191
+ ## MCP Server(菜单 & 字典自动同步)
192
+
193
+ `init` 安装时会自动生成 `.cursor/mcp.json` 和 `.claude/settings.json`,将 wl-skills MCP server 注册到 AI 编辑器。**无需手动配置**,启动 Cursor / Claude Code 后自动启用。
194
+
195
+ ### 提供的工具
196
+
197
+ | 工具 | 用途 |
198
+ |---|---|
199
+ | `wls_menu_query` | 查询当前应用完整菜单树(用于决策新增/更新) |
200
+ | `wls_menu_upsert` | 批量新增或更新菜单(有 id=更新,无 id=新增,自动返回服务端 id) |
201
+ | `wls_dict_query` | 查询所有字典模块及字典项 |
202
+ | `wls_dict_upsert` | 新增/更新字典模块+字典项(内部自动 re-query 获取 id) |
203
+
204
+ ### 配置(安装后填写一次)
205
+
206
+ 在 `.github/skills/sync/env.local.json` 中填写:
207
+
208
+ ```json
209
+ {
210
+ "gatewayPath": "http://你的网关地址:端口",
211
+ "token": "Bearer Token(不含 Bearer 前缀)",
212
+ "sysAppNo": "应用编码",
213
+ "menu": {
214
+ "domainId": "从 Network 面板 getMenuTreeByDomainId?domainId=xxx 中获取",
215
+ "parentMenuId": "新建菜单的父节点 ID"
216
+ }
217
+ }
218
+ ```
219
+
220
+ > `env.local.json` 已自动加入 `.gitignore`,不会入库。
221
+
222
+ ### 效率对比
223
+
224
+ | 方式 | 同步 10 条菜单 | 手动操作 | 耗时 |
225
+ |---|---|---|---|
226
+ | SKILL.md(prompt-based) | ~4000 token | 10 次 | ~20 分钟 |
227
+ | **MCP tools** | ~500 token | **0 次** | **~1 分钟** |
228
+
229
+ ---
230
+
190
231
  ## Skill 概览
191
232
 
192
233
  | Skill | 状态 | 路径 | 核心用途 |
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.2.0
4
+ * wl-skills-kit CLI v2.3.0
5
5
  *
6
6
  * 命令:
7
7
  * init 全量安装(默认,向后兼容)
@@ -52,7 +52,9 @@ if (showHelp) {
52
52
 
53
53
  保护路径(init / update 不覆盖已存在的):
54
54
  .github/reports/ AI 生成报告(团队累积数据,存在则跳过)
55
- .github/skills/sync/menu-sync/env/env.local.json 用户本地配置(token 等,已存在则跳过)
55
+ .github/skills/sync/env.local.json 用户本地配置(token 等,已存在则跳过)
56
+ .cursor/mcp.json MCP 配置(已存在则跳过,避免覆盖其他 server)
57
+ .claude/settings.json MCP 配置(已存在则跳过)
56
58
 
57
59
  清理保护路径(clean 不删除):
58
60
  src/components/ 通用组件(被业务页面 import,构建必需)
@@ -381,6 +383,62 @@ function runInstall(incremental) {
381
383
  }
382
384
  }
383
385
 
386
+ // ── Step 2.5: MCP 配置文件(.cursor/mcp.json + .claude/settings.json)──────
387
+
388
+ const MCP_SERVER_ARGS = ["node_modules/@agile-team/wl-skills-kit/mcp/server.js"];
389
+ const MCP_CONFIGS = [
390
+ {
391
+ relPath: ".cursor/mcp.json",
392
+ content: JSON.stringify(
393
+ {
394
+ mcpServers: {
395
+ "wl-skills": {
396
+ command: "node",
397
+ args: MCP_SERVER_ARGS,
398
+ env: { WL_PROJECT_ROOT: "${workspaceFolder}" },
399
+ },
400
+ },
401
+ },
402
+ null,
403
+ 2
404
+ ),
405
+ },
406
+ {
407
+ relPath: ".claude/settings.json",
408
+ content: JSON.stringify(
409
+ {
410
+ mcpServers: {
411
+ "wl-skills": {
412
+ command: "node",
413
+ args: MCP_SERVER_ARGS,
414
+ env: { WL_PROJECT_ROOT: "." },
415
+ },
416
+ },
417
+ },
418
+ null,
419
+ 2
420
+ ),
421
+ },
422
+ ];
423
+
424
+ if (dryRun) console.log("\n [Step 2.5] MCP 配置文件:\n");
425
+
426
+ for (const mc of MCP_CONFIGS) {
427
+ const mcDest = path.join(TARGET_DIR, mc.relPath);
428
+ if (fs.existsSync(mcDest)) {
429
+ preserved++;
430
+ if (dryRun) console.log(" 保留 " + mc.relPath + " (已存在,不覆盖)");
431
+ } else {
432
+ if (dryRun) {
433
+ console.log(" 新增 " + mc.relPath);
434
+ created++;
435
+ } else {
436
+ writeFile(mcDest, mc.content);
437
+ created++;
438
+ }
439
+ }
440
+ }
441
+
384
442
  // ── Step 3: 迁移清理(仅 update,清理旧版遗留文件)──────────────────
385
443
 
386
444
  if (incremental) {
@@ -2,7 +2,7 @@
2
2
 
3
3
  > **读者**:团队技术负责人 / wl-skills-kit 维护者 / 对体系设计感兴趣的团队成员
4
4
  > **更新方式**:重大架构变更后追加对应章节,旧章节原文保留(历史可溯)
5
- > **当前版本**:v2.2.0(2026-04-27)
5
+ > **当前版本**:v2.3.0(2026-04-27)
6
6
 
7
7
  ---
8
8
 
@@ -102,7 +102,10 @@ wl-skills.js init/update
102
102
  │ - reports/*.md 已存在则跳过(保护团队累积数据)
103
103
  │ - 其他文件:覆盖写入
104
104
 
105
- └─ 5. 写入/更新 .wl-skills-manifest.json(文件哈希快照)
105
+ ├─ 5. 生成 MCP 配置文件(.cursor/mcp.json + .claude/settings.json
106
+ │ - 已存在则跳过(不覆盖用户已有配置)
107
+
108
+ └─ 6. 写入/更新 .wl-skills-manifest.json(文件哈希快照)
106
109
  ```
107
110
 
108
111
  ---
@@ -213,7 +216,7 @@ skills/
213
216
 
214
217
  ├── sync/ 数据同步类(与后端系统联动)
215
218
  │ ├── menu-sync/ ✅ 启用:菜单基线 ↔ 后端菜单接口
216
- │ ├── dict-sync/ PLANNED
219
+ │ ├── dict-sync/ 启用:字典基线 ↔ 后端字典接口
217
220
  │ └── permission-sync/ ⏳ PLANNED
218
221
 
219
222
  ├── ops/ 运维/质量类
@@ -424,7 +427,24 @@ AI "假执行"——声称读了规范,实际按惯性输出。没有强制约
424
427
 
425
428
  ## 8. 菜单/字典/权限同步机制
426
429
 
427
- 三类 sync Skill 共用同一模式,以 `menu-sync` 为基准(已启用):
430
+ 三类 sync Skill 共用同一基线模式。**v2.3.0 起,menu-sync 和 dict-sync 同时提供 SKILL.md prompt 模式(手动中转)和 MCP Server 自动模式(零手动)**:
431
+
432
+ ### MCP 自动模式(推荐,v2.3.0+)
433
+
434
+ ```
435
+ 开发者说:"扩展菜单" / "加字典"
436
+
437
+ AI 自动 call MCP Tools(无需手动粘贴接口响应):
438
+ wls_menu_query → 拉取现有菜单树
439
+ wls_menu_upsert → 批量新增/更新
440
+ wls_dict_query → 拉取现有字典模块
441
+ wls_dict_upsert → 创建模块 + 字典项(内部自动 re-query 拿 id)
442
+ ```
443
+
444
+ `init` 时自动生成 `.cursor/mcp.json` + `.claude/settings.json`(已存在则跳过)。
445
+ MCP server 从 `.github/skills/sync/env.local.json` 读取 `gatewayPath / token / menu.domainId`。
446
+
447
+ ### SKILL.md Prompt 模式(兼容,无 MCP 环境可用)
428
448
 
429
449
  ```
430
450
  本地基线文件(reports/SYS_MENU_INFO.md)
@@ -437,7 +457,7 @@ AI "假执行"——声称读了规范,实际按惯性输出。没有强制约
437
457
  ③ 对比:基线 vs 线上 → 找出缺失条目
438
458
  ④ Pre-flight 输出操作预览(必须用户确认)
439
459
  ⑤ 调用后端接口写入
440
- 输出操作日志(含回滚 SQL)到 reports/MENU_SYNC_*.md
460
+ 输出操作日志到 reports/MENU_SYNC_*.md
441
461
  ```
442
462
 
443
463
  **关键设计约束**:
@@ -475,9 +495,10 @@ AI "假执行"——声称读了规范,实际按惯性输出。没有强制约
475
495
  | ---- | ----------------------------------------------------------------------------------------------------------------------------------------- | --------- |
476
496
  | v1.x | 5 个 Skill 平铺 + 9 个 TPL 平铺 + 单一超长 copilot-instructions | ✅ 已发布 |
477
497
  | v2.0 | 规范模块化(13 条)+ 模板分层(universal/domains)+ 报告分类 + Pre-flight + 工具链门控 | ✅ 已发布 |
478
- | v2.1 | Skill 分级目录(core/sync/ops/domain)+ 多 AI 适配解耦(editors.json)+ 各 Skill USAGE.md + api-contract 真实响应 + 3 个 PLANNED 草稿补全 | ✅ 当前 |
479
- | v2.2 | dict-sync / permission-sync / code-fix PLANNED 转正(视后端接口稳定情况) | 规划中 |
480
- | v2.3 | CI 流水线接入(convention-audit 报告注入 PR 评论)+ Skill 版本感知 | 规划中 |
498
+ | v2.1 | Skill 分级目录(core/sync/ops/domain)+ 多 AI 适配解耦(editors.json)+ 各 Skill USAGE.md + api-contract 真实响应 + 3 个 PLANNED 草稿补全 | ✅ 已发布 |
499
+ | v2.2 | sync-version.js(版本号自动同步)+ dict-sync / code-fix 正式激活 + 8 个 Skill 全部就绪 | 已发布 |
500
+ | v2.3 | MCP Server 内置(wls_menu_query/upsert + wls_dict_query/upsert)+ init 自动写 MCP 配置 + env.local.json 新增 menu.domainId | ✅ 当前 |
501
+ | v2.4 | permission-sync MCP 化(待后端接口确认) | ⏳ 规划中 |
481
502
 
482
503
  ---
483
504
 
@@ -55,8 +55,8 @@ AI 会自动识别意图,触发对应的 Skill。
55
55
  | `prototype-scan` | 扫描原型 / 解析原型 / 口述需求 | 原型 / 详设 → page-spec JSON |
56
56
  | `api-contract` | 接口约定 / api.md / 字段定义 | 生成接口约定文档 |
57
57
  | `page-codegen` | 生成页面 / 帮我生成 | 生成 4 文件 + 菜单注册 |
58
- | `menu-sync` | 创建菜单 / 同步菜单 | 菜单数据同步到后端 |
59
- | `dict-sync` | 同步字典 / 创建字典 / 字典审计 | 字典基线同步到后端 |
58
+ | `menu-sync` | 创建菜单 / 同步菜单 | 菜单数据同步到后端(MCP 自动 / prompt 手动两种模式) |
59
+ | `dict-sync` | 同步字典 / 创建字典 / 字典审计 | 字典基线同步到后端(MCP 自动 / prompt 手动两种模式) |
60
60
  | `convention-audit` | 规范审计 / 代码审计 | 13 条规范扫描 + 偏差报告 |
61
61
  | `template-extract` | 提取模板 / 抄取模板 | 从现有页面沉淠领域专属模板 |
62
62
  | `code-fix` | 自动修复 / 整改偏差 / 规范整改 | 受控自动修复审计报告中的偏差 |
@@ -119,6 +119,9 @@ A: 触发 `template-extract`,从现有标杆页面提取为领域专属模板
119
119
  **Q: 多个 AI 编辑器之间会冲突吗?**
120
120
  A: 不会。`bin/wl-skills.js` 已自动生成 8 种主流 AI 编辑器配置文件,所有内容来自同一份 `copilot-instructions.md`,保持一致。
121
121
 
122
+ **Q: MCP Server 是什么?需要额外配置吗?**
123
+ A: `wl-skills init` 会自动生成 `.cursor/mcp.json`(Cursor 用)和 `.claude/settings.json`(Claude Code 用)。只需在 `.github/skills/sync/env.local.json` 里填好 `token`、`gatewayPath`、`menu.domainId` 即可。填写后重启编辑器,对 AI 说「扩展菜单」或「加字典」,AI 会自动调用接口完成同步,无需手动粘贴接口响应。
124
+
122
125
  **Q: 部署到生产环境前如何清理 AI 文件?**
123
126
  A: 执行 `npx @agile-team/wl-skills-kit clean`。会移除所有 AI 辅助文件,保留 `src/components/` 和 `src/types/`。
124
127
 
@@ -7,7 +7,8 @@
7
7
 
8
8
  "menu": {
9
9
  "_comment": "menu-sync 专属配置",
10
- "parentMenuId": "父级菜单ID(从菜单管理后台获取)"
10
+ "parentMenuId": "父级菜单ID(从菜单管理后台获取,新建菜单时的父节点)",
11
+ "domainId": "应用域ID(从 Network 面板 getMenuTreeByDomainId?domainId=xxx 中获取,MCP wls_menu_query 使用)"
11
12
  },
12
13
 
13
14
  "dict": {
@@ -0,0 +1,76 @@
1
+ 'use strict'
2
+
3
+ const https = require('https')
4
+ const http = require('http')
5
+
6
+ /**
7
+ * 带鉴权的 HTTP 客户端(兼容 Node 16+,无额外依赖)
8
+ *
9
+ * @param {string} urlPath - 接口路径(以 / 开头),拼接到 config.gatewayPath 后
10
+ * @param {{ method?: string, body?: unknown }} options
11
+ * @param {{ gatewayPath: string, token: string }} config
12
+ * @returns {Promise<{ ok: boolean, data: any, error?: string, code?: number }>}
13
+ */
14
+ function wlsFetch(urlPath, options, config) {
15
+ const fullUrl = config.gatewayPath + urlPath
16
+ const isHttps = fullUrl.startsWith('https')
17
+ const lib = isHttps ? https : http
18
+ const bodyStr = options.body ? JSON.stringify(options.body) : null
19
+
20
+ let urlObj
21
+ try {
22
+ urlObj = new URL(fullUrl)
23
+ } catch (e) {
24
+ return Promise.reject(new Error(`无效的 URL: ${fullUrl}`))
25
+ }
26
+
27
+ const reqOptions = {
28
+ hostname: urlObj.hostname,
29
+ port: urlObj.port || (isHttps ? 443 : 80),
30
+ path: urlObj.pathname + urlObj.search,
31
+ method: options.method || 'GET',
32
+ headers: {
33
+ 'Content-Type': 'application/json',
34
+ 'Authorization': `Bearer ${config.token}`,
35
+ },
36
+ }
37
+
38
+ if (bodyStr) {
39
+ reqOptions.headers['Content-Length'] = Buffer.byteLength(bodyStr)
40
+ }
41
+
42
+ return new Promise((resolve, reject) => {
43
+ const req = lib.request(reqOptions, (res) => {
44
+ let data = ''
45
+ res.on('data', (chunk) => { data += chunk })
46
+ res.on('end', () => {
47
+ try {
48
+ const json = JSON.parse(data)
49
+ if (json.code === 2000) {
50
+ resolve({ ok: true, data: json.data, code: json.code })
51
+ } else {
52
+ resolve({
53
+ ok: false,
54
+ data: null,
55
+ error: json.message || `服务端返回 code=${json.code}`,
56
+ code: json.code,
57
+ })
58
+ }
59
+ } catch (e) {
60
+ reject(new Error(`响应解析失败: ${e.message},原始内容: ${data.slice(0, 200)}`))
61
+ }
62
+ })
63
+ })
64
+
65
+ req.on('error', (e) => reject(new Error(`请求失败: ${e.message}`)))
66
+ req.setTimeout(15000, () => {
67
+ req.destroy()
68
+ reject(new Error('请求超时(15s)'))
69
+ })
70
+
71
+ if (bodyStr) req.write(bodyStr)
72
+ req.end()
73
+ })
74
+ }
75
+
76
+ module.exports = { wlsFetch }
@@ -0,0 +1,40 @@
1
+ 'use strict'
2
+
3
+ const { wlsFetch } = require('./client')
4
+
5
+ /**
6
+ * 查询字典树(全量,含所有模块和字典项)
7
+ * GET /system/business/dict/getDictionaryTreeData
8
+ * 响应结构: { data: { dictionary: { children: DictModule[] } } }
9
+ *
10
+ * @param {{ gatewayPath: string, token: string }} config
11
+ */
12
+ function queryDictModules(config) {
13
+ return wlsFetch('/system/business/dict/getDictionaryTreeData', {}, config)
14
+ }
15
+
16
+ /**
17
+ * 新增或更新字典模块
18
+ * POST /system/dictModule/save
19
+ * ⚠️ 响应 data 为 null,创建后必须通过 queryDictModules + strSn 匹配才能拿到 id
20
+ *
21
+ * @param {object} body - DictModuleSaveBody
22
+ * @param {{ gatewayPath: string, token: string }} config
23
+ */
24
+ function saveDictModule(body, config) {
25
+ return wlsFetch('/system/dictModule/save', { method: 'POST', body }, config)
26
+ }
27
+
28
+ /**
29
+ * 新增字典值(词典项)
30
+ * POST /system/business/dict/save
31
+ * 响应 data 为 null(只需确认 code=2000 成功即可)
32
+ *
33
+ * @param {object} body - DictItemSaveBody
34
+ * @param {{ gatewayPath: string, token: string }} config
35
+ */
36
+ function saveDictItem(body, config) {
37
+ return wlsFetch('/system/business/dict/save', { method: 'POST', body }, config)
38
+ }
39
+
40
+ module.exports = { queryDictModules, saveDictModule, saveDictItem }
@@ -0,0 +1,32 @@
1
+ 'use strict'
2
+
3
+ const { wlsFetch } = require('./client')
4
+
5
+ /**
6
+ * 查询指定应用域的菜单树
7
+ * GET /system/menu/getMenuTreeByDomainId?domainId={domainId}
8
+ *
9
+ * @param {string} domainId
10
+ * @param {{ gatewayPath: string, token: string }} config
11
+ */
12
+ function queryMenuTree(domainId, config) {
13
+ return wlsFetch(
14
+ `/system/menu/getMenuTreeByDomainId?domainId=${encodeURIComponent(domainId)}`,
15
+ {},
16
+ config
17
+ )
18
+ }
19
+
20
+ /**
21
+ * 新增或更新菜单
22
+ * POST /system/menu/save
23
+ * 有 id → 更新;无 id → 新增(响应 data 含服务端生成的完整对象包括 id)
24
+ *
25
+ * @param {object} body - MenuSaveBody
26
+ * @param {{ gatewayPath: string, token: string }} config
27
+ */
28
+ function saveMenu(body, config) {
29
+ return wlsFetch('/system/menu/save', { method: 'POST', body }, config)
30
+ }
31
+
32
+ module.exports = { queryMenuTree, saveMenu }
package/mcp/config.js ADDED
@@ -0,0 +1,47 @@
1
+ 'use strict'
2
+
3
+ const fs = require('fs')
4
+ const path = require('path')
5
+
6
+ /**
7
+ * 从项目的 .github/skills/sync/env.local.json 加载 MCP 运行配置
8
+ * 项目根目录通过环境变量 WL_PROJECT_ROOT 传入(由 .cursor/mcp.json 注入)
9
+ */
10
+ function loadConfig() {
11
+ const projectRoot = process.env.WL_PROJECT_ROOT
12
+ ? path.resolve(process.env.WL_PROJECT_ROOT)
13
+ : process.cwd()
14
+
15
+ const configPath = path.join(projectRoot, '.github', 'skills', 'sync', 'env.local.json')
16
+
17
+ if (!fs.existsSync(configPath)) {
18
+ throw new Error(
19
+ `配置文件不存在: ${configPath}\n` +
20
+ `请先执行 npx @agile-team/wl-skills-kit init,然后填写 .github/skills/sync/env.local.json`
21
+ )
22
+ }
23
+
24
+ let raw
25
+ try {
26
+ raw = JSON.parse(fs.readFileSync(configPath, 'utf8'))
27
+ } catch (e) {
28
+ throw new Error(`配置文件解析失败: ${e.message}`)
29
+ }
30
+
31
+ if (!raw.gatewayPath || raw.gatewayPath.includes('你的网关')) {
32
+ throw new Error('请在 env.local.json 中填写真实的 gatewayPath(当前为占位值)')
33
+ }
34
+ if (!raw.token || raw.token.includes('Bearer Token')) {
35
+ throw new Error('请在 env.local.json 中填写真实的 token(当前为占位值)')
36
+ }
37
+
38
+ return {
39
+ gatewayPath: raw.gatewayPath.replace(/\/$/, ''), // 去掉尾部斜杠
40
+ token: raw.token,
41
+ sysAppNo: raw.sysAppNo || '',
42
+ menu: raw.menu || {},
43
+ dict: raw.dict || {},
44
+ }
45
+ }
46
+
47
+ module.exports = { loadConfig }
package/mcp/server.js ADDED
@@ -0,0 +1,210 @@
1
+ #!/usr/bin/env node
2
+ 'use strict'
3
+
4
+ /**
5
+ * wl-skills MCP Server
6
+ *
7
+ * 实现 MCP 协议(stdio transport,JSON-RPC 2.0)
8
+ * 暴露 4 个工具:
9
+ * wls_menu_query 查询菜单树
10
+ * wls_menu_upsert 批量新增/更新菜单
11
+ * wls_dict_query 查询字典模块
12
+ * wls_dict_upsert 新增/更新字典模块及字典项
13
+ *
14
+ * 启动方式(由 .cursor/mcp.json 自动注入):
15
+ * node node_modules/@agile-team/wl-skills-kit/mcp/server.js
16
+ */
17
+
18
+ const readline = require('readline')
19
+ const { loadConfig } = require('./config')
20
+ const { handleMenuQuery, handleMenuUpsert } = require('./tools/menuSync')
21
+ const { handleDictQuery, handleDictUpsert } = require('./tools/dictSync')
22
+
23
+ const PKG = require('../package.json')
24
+
25
+ // ─── Tool 注册表 ────────────────────────────────────────────────────────
26
+
27
+ const TOOLS = [
28
+ {
29
+ name: 'wls_menu_query',
30
+ description:
31
+ '查询当前应用的完整菜单树。自动从 .github/skills/sync/env.local.json 读取 domainId,' +
32
+ '无需传参。在 wls_menu_upsert 前调用,用于判断哪些菜单需要新增、哪些需要更新。',
33
+ inputSchema: {
34
+ type: 'object',
35
+ properties: {},
36
+ required: [],
37
+ },
38
+ },
39
+ {
40
+ name: 'wls_menu_upsert',
41
+ description:
42
+ '批量新增或更新菜单项。有 id 字段 → 更新;无 id 字段 → 新增。' +
43
+ '新增时响应自动包含服务端生成的 id,可链式用于创建子菜单。',
44
+ inputSchema: {
45
+ type: 'object',
46
+ properties: {
47
+ items: {
48
+ type: 'array',
49
+ description:
50
+ 'MenuSaveBody 数组。每项字段:' +
51
+ 'id(更新时传), sysAppNo, menuName, menuNameCode, parentId, ' +
52
+ 'type("M"=目录/"C"=菜单), path, icon, orderNum, ' +
53
+ 'useCache(1), common(2), hidden(false), editMode(false), ' +
54
+ 'component(type=C时传), permission(type=C时传)',
55
+ items: { type: 'object' },
56
+ },
57
+ },
58
+ required: ['items'],
59
+ },
60
+ },
61
+ {
62
+ name: 'wls_dict_query',
63
+ description:
64
+ '查询当前应用的所有字典模块及字典项。在 wls_dict_upsert 前调用,' +
65
+ '用于判断哪些模块/字典项已存在。',
66
+ inputSchema: {
67
+ type: 'object',
68
+ properties: {},
69
+ required: [],
70
+ },
71
+ },
72
+ {
73
+ name: 'wls_dict_upsert',
74
+ description:
75
+ '新增或更新字典模块及其字典项。内部自动处理:' +
76
+ '若模块不存在则创建(data=null 后自动 re-query 获取 id),' +
77
+ '若已存在则直接取 id;字典项自动跳过已存在的 strSn。',
78
+ inputSchema: {
79
+ type: 'object',
80
+ properties: {
81
+ module: {
82
+ type: 'object',
83
+ description: 'DictModuleSaveBody: strSn(必填), strName(必填), sortPriority("1"), strLevel(2)',
84
+ properties: {
85
+ strSn: { type: 'string', description: '模块标识符,如 "gender"' },
86
+ strName: { type: 'string', description: '模块显示名,如 "性别"' },
87
+ sortPriority: { type: 'string', description: '排序,字符串类型,如 "1"' },
88
+ strLevel: { type: 'number', description: '固定传 2' },
89
+ },
90
+ required: ['strSn', 'strName'],
91
+ },
92
+ items: {
93
+ type: 'array',
94
+ description:
95
+ 'DictItemSaveBody 数组(可选)。每项字段:' +
96
+ 'strSn(必填), strName(必填), strLevel(2), ' +
97
+ 'dtlValue(""), dtlValueRequired(false), dtlValue2Required(false), ' +
98
+ 'dtlValue3Required(false), dtlValue4Required(false)',
99
+ items: { type: 'object' },
100
+ },
101
+ },
102
+ required: ['module'],
103
+ },
104
+ },
105
+ ]
106
+
107
+ // ─── JSON-RPC 协议层 ────────────────────────────────────────────────────
108
+
109
+ function send(obj) {
110
+ process.stdout.write(JSON.stringify(obj) + '\n')
111
+ }
112
+
113
+ function sendResult(id, result) {
114
+ send({ jsonrpc: '2.0', id, result })
115
+ }
116
+
117
+ function sendError(id, code, message) {
118
+ send({ jsonrpc: '2.0', id, error: { code, message } })
119
+ }
120
+
121
+ // ─── Tool 调度 ───────────────────────────────────────────────────────────
122
+
123
+ async function dispatchTool(id, toolName, toolArgs) {
124
+ let config
125
+ try {
126
+ config = loadConfig()
127
+ } catch (e) {
128
+ // 配置加载失败:以文本形式返回给 AI(不是 JSON-RPC error,AI 能读)
129
+ sendResult(id, { content: [{ type: 'text', text: `❌ 配置加载失败: ${e.message}` }], isError: true })
130
+ return
131
+ }
132
+
133
+ try {
134
+ let text
135
+ switch (toolName) {
136
+ case 'wls_menu_query':
137
+ text = await handleMenuQuery(config)
138
+ break
139
+ case 'wls_menu_upsert':
140
+ text = await handleMenuUpsert(toolArgs, config)
141
+ break
142
+ case 'wls_dict_query':
143
+ text = await handleDictQuery(config)
144
+ break
145
+ case 'wls_dict_upsert':
146
+ text = await handleDictUpsert(toolArgs, config)
147
+ break
148
+ default:
149
+ sendError(id, -32601, `未知工具: ${toolName}`)
150
+ return
151
+ }
152
+ sendResult(id, { content: [{ type: 'text', text }] })
153
+ } catch (e) {
154
+ sendResult(id, {
155
+ content: [{ type: 'text', text: `❌ 工具执行异常: ${e.message}` }],
156
+ isError: true,
157
+ })
158
+ }
159
+ }
160
+
161
+ // ─── 消息循环 ────────────────────────────────────────────────────────────
162
+
163
+ const rl = readline.createInterface({ input: process.stdin, terminal: false })
164
+
165
+ rl.on('line', async (line) => {
166
+ const raw = line.trim()
167
+ if (!raw) return
168
+
169
+ let msg
170
+ try {
171
+ msg = JSON.parse(raw)
172
+ } catch (e) {
173
+ send({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } })
174
+ return
175
+ }
176
+
177
+ const { id, method, params = {} } = msg
178
+
179
+ // Notifications(无 id)不需要响应
180
+ if (id === undefined || id === null) return
181
+
182
+ switch (method) {
183
+ case 'initialize':
184
+ sendResult(id, {
185
+ protocolVersion: '2024-11-05',
186
+ capabilities: { tools: {} },
187
+ serverInfo: { name: 'wl-skills', version: PKG.version },
188
+ })
189
+ break
190
+
191
+ case 'tools/list':
192
+ sendResult(id, { tools: TOOLS })
193
+ break
194
+
195
+ case 'tools/call':
196
+ await dispatchTool(id, params.name, params.arguments || {})
197
+ break
198
+
199
+ case 'ping':
200
+ sendResult(id, {})
201
+ break
202
+
203
+ default:
204
+ sendError(id, -32601, `Method not found: ${method}`)
205
+ }
206
+ })
207
+
208
+ rl.on('close', () => {
209
+ process.exit(0)
210
+ })
@@ -0,0 +1,173 @@
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 = { handleDictQuery, handleDictUpsert }
@@ -0,0 +1,96 @@
1
+ 'use strict'
2
+
3
+ const { queryMenuTree, saveMenu } = require('../api/menuApi')
4
+
5
+ /**
6
+ * wls_menu_query 工具处理器
7
+ * 自动从 config.menu.domainId 读取应用域 ID,查询完整菜单树
8
+ *
9
+ * @param {{ menu: { domainId?: string } }} config
10
+ * @returns {Promise<string>} 返回给 AI 的文本内容
11
+ */
12
+ async function handleMenuQuery(config) {
13
+ const domainId = config.menu && config.menu.domainId
14
+
15
+ if (!domainId || domainId.toString().includes('domainId')) {
16
+ return [
17
+ '❌ 请在 env.local.json 的 menu.domainId 字段填写真实的应用域 ID',
18
+ '',
19
+ '获取方式:打开菜单管理后台,在 Network 面板找到类似',
20
+ ' getMenuTreeByDomainId?domainId=1777597797627056130',
21
+ '中的数字即为 domainId,填入 env.local.json:',
22
+ ' "menu": { "domainId": "1777597797627056130", ... }',
23
+ ].join('\n')
24
+ }
25
+
26
+ const result = await queryMenuTree(domainId, config)
27
+
28
+ if (!result.ok) {
29
+ return `❌ 查询菜单树失败: ${result.error} (code: ${result.code})`
30
+ }
31
+
32
+ const tree = result.data
33
+ const isEmpty = !tree || (Array.isArray(tree) && tree.length === 0)
34
+ if (isEmpty) {
35
+ return `✅ 菜单树查询成功,当前应用域(domainId=${domainId})暂无菜单数据`
36
+ }
37
+
38
+ return `✅ 菜单树查询成功(domainId=${domainId})\n\n${JSON.stringify(tree, null, 2)}`
39
+ }
40
+
41
+ /**
42
+ * wls_menu_upsert 工具处理器
43
+ * 批量新增(无 id)或更新(有 id)菜单项
44
+ * 新增时从响应 data 取服务端生成的 id,可用于链式创建子菜单
45
+ *
46
+ * @param {{ items: object[] }} args
47
+ * @param {object} config
48
+ * @returns {Promise<string>}
49
+ */
50
+ async function handleMenuUpsert(args, config) {
51
+ const { items } = args
52
+
53
+ if (!Array.isArray(items) || items.length === 0) {
54
+ return '❌ 参数错误:items 必须是非空数组'
55
+ }
56
+
57
+ const results = []
58
+
59
+ for (const item of items) {
60
+ const isUpdate = Boolean(item.id)
61
+ const action = isUpdate ? '更新' : '新增'
62
+
63
+ const result = await saveMenu(item, config)
64
+
65
+ if (result.ok) {
66
+ const saved = result.data
67
+ results.push({
68
+ action,
69
+ menuName: item.menuName || '(未命名)',
70
+ id: saved ? saved.id : item.id,
71
+ status: '✅ 成功',
72
+ })
73
+ } else {
74
+ results.push({
75
+ action,
76
+ menuName: item.menuName || '(未命名)',
77
+ id: item.id || '(新增)',
78
+ status: `❌ 失败: ${result.error}`,
79
+ })
80
+ }
81
+ }
82
+
83
+ const successCount = results.filter((r) => r.status.startsWith('✅')).length
84
+ const failCount = results.length - successCount
85
+
86
+ let output = `菜单操作完成:成功 ${successCount} 条,失败 ${failCount} 条\n\n`
87
+ output += '| 操作 | 菜单名 | ID | 状态 |\n'
88
+ output += '|---|---|---|---|\n'
89
+ for (const r of results) {
90
+ output += `| ${r.action} | ${r.menuName} | ${r.id} | ${r.status} |\n`
91
+ }
92
+
93
+ return output
94
+ }
95
+
96
+ module.exports = { handleMenuQuery, handleMenuUpsert }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@agile-team/wl-skills-kit",
3
- "version": "2.2.0",
4
- "description": "AI Skill 模板包 v2.2.0 — 13 条编码规范 + 8 个 AI Skill,一条命令导入 Vue 3 项目",
3
+ "version": "2.3.0",
4
+ "description": "AI Skill 模板包 v2.3.0 — 13 条编码规范 + 8 个 AI Skill,一条命令导入 Vue 3 项目",
5
5
  "main": "./bin/wl-skills.js",
6
6
  "bin": {
7
7
  "wl-skills": "bin/wl-skills.js"
@@ -9,6 +9,7 @@
9
9
  "files": [
10
10
  "bin/",
11
11
  "files/",
12
+ "mcp/",
12
13
  "README.md",
13
14
  "CHANGELOG.md"
14
15
  ],