@agile-team/wl-skills-kit 2.2.0 → 2.3.1
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 +33 -22
- package/README.md +11 -103
- package/bin/wl-skills.js +2 -42
- package/files/.github/guides/README.md +13 -13
- package/files/.github/guides/architecture.md +555 -555
- package/files/.github/guides/usage.md +176 -173
- package/files/.github/reports/README.md +65 -65
- package/files/.github/reports/SYS_DICT_INFO.md +50 -50
- package/files/.github/reports/SYS_MENU_INFO.md +247 -247
- package/files/.github/reports/SYS_PERMISSION_INFO.md +20 -20
- package/files/.github/reports//347/273/204/344/273/266/346/217/220/345/217/226/345/273/272/350/256/256.md +33 -33
- package/files/.github/reports//350/247/204/350/214/203/345/256/241/346/237/245/346/212/245/345/221/212.md +44 -44
- package/files/.github/skills/_compat/README.md +108 -108
- package/files/.github/skills/_compat/headers/agents.txt +8 -8
- package/files/.github/skills/_compat/headers/claude-code.txt +7 -7
- package/files/.github/skills/_compat/headers/cline.txt +7 -7
- package/files/.github/skills/_compat/headers/cursor-mdc.txt +16 -16
- package/files/.github/skills/_compat/headers/cursor-rules.txt +7 -7
- package/files/.github/skills/_compat/headers/github-copilot.txt +1 -1
- package/files/.github/skills/_compat/headers/kiro.txt +10 -10
- package/files/.github/skills/_compat/headers/trae.txt +11 -11
- package/files/.github/skills/_compat/headers/windsurf.txt +7 -7
- package/files/.github/skills/_registry.md +81 -81
- package/files/.github/skills/core/api-contract/SKILL.md +344 -344
- package/files/.github/skills/core/api-contract/USAGE.md +110 -110
- package/files/.github/skills/core/convention-audit/SKILL.md +189 -189
- package/files/.github/skills/core/convention-audit/USAGE.md +99 -99
- package/files/.github/skills/core/page-codegen/SKILL.md +973 -973
- package/files/.github/skills/core/page-codegen/USAGE.md +102 -102
- package/files/.github/skills/core/page-codegen/templates/_index.md +46 -46
- package/files/.github/skills/core/page-codegen/templates/domains/_CONTRIBUTING.md +107 -107
- package/files/.github/skills/core/page-codegen/templates/domains/produce/TPL-OPERATION-STATION.md +442 -442
- package/files/.github/skills/core/page-codegen/templates/domains/sale/README.md +26 -26
- package/files/.github/skills/core/page-codegen/templates/universal/TPL-CHANGE-HISTORY.md +276 -276
- package/files/.github/skills/core/page-codegen/templates/universal/TPL-DETAIL-TABS.md +1145 -1145
- package/files/.github/skills/core/page-codegen/templates/universal/TPL-DRIVEN.md +124 -124
- package/files/.github/skills/core/page-codegen/templates/universal/TPL-FORM-ROUTE.md +436 -436
- package/files/.github/skills/core/page-codegen/templates/universal/TPL-LIST.md +191 -191
- package/files/.github/skills/core/page-codegen/templates/universal/TPL-MASTER-DETAIL.md +148 -148
- package/files/.github/skills/core/page-codegen/templates/universal/TPL-RECORD-FORM.md +376 -376
- package/files/.github/skills/core/page-codegen/templates/universal/TPL-TREE-LIST.md +186 -186
- package/files/.github/skills/core/prototype-scan/SKILL.md +498 -498
- package/files/.github/skills/core/prototype-scan/USAGE.md +95 -95
- package/files/.github/skills/core/template-extract/SKILL.md +139 -139
- package/files/.github/skills/core/template-extract/USAGE.md +93 -93
- package/files/.github/skills/domain/README.md +51 -51
- package/files/.github/skills/sync/env.local.json +2 -1
- package/files/.github/skills/sync/menu-sync/SKILL.md +263 -263
- package/files/.github/skills/sync/menu-sync/USAGE.md +104 -104
- package/files/.github/skills/sync/menu-sync/env/env.local.json +7 -7
- package/files/.github/skills/sync/menu-sync/env/guide.md +99 -99
- package/files/.github/skills/sync/permission-sync/SKILL.draft.md +91 -91
- package/files/.github/standards/01-toolchain.md +57 -57
- package/files/.github/standards/02-code-structure.md +111 -111
- package/files/.github/standards/03-comments.md +53 -53
- package/files/.github/standards/04-coding-basics.md +33 -33
- package/files/.github/standards/05-logging.md +38 -38
- package/files/.github/standards/06-security.md +44 -44
- package/files/.github/standards/07-config.md +52 -52
- package/files/.github/standards/08-git.md +60 -60
- package/files/.github/standards/09-typescript.md +71 -71
- package/files/.github/standards/10-pinia.md +57 -57
- package/files/.github/standards/11-form-validation.md +81 -81
- package/files/.github/standards/12-base-table.md +153 -153
- package/files/.github/standards/13-platform-components.md +123 -123
- package/files/.github/standards/index.md +89 -89
- package/files/demo/produce/aiflow/mmwr-customer-apply-add/api.md +1 -1
- package/files/demo/produce/aiflow/mmwr-customer-apply-change-history/data.ts +196 -196
- package/files/demo/produce/aiflow/mmwr-customer-apply-change-history/index.scss +150 -150
- package/files/demo/produce/aiflow/mmwr-customer-apply-change-history/index.vue +79 -79
- package/files/docs/jh-date-range.md +257 -257
- package/files/docs/jh-date.md +222 -222
- package/files/docs/jh-dept-picker.md +190 -190
- package/files/docs/jh-drag-row.md +590 -590
- package/files/docs/jh-file-upload.md +216 -216
- package/files/docs/jh-picker.md +218 -218
- package/files/docs/jh-select.md +148 -148
- package/files/docs/jh-text.md +248 -248
- package/files/docs/jh-user-picker.md +197 -197
- package/files/docs/request.md +24 -9
- package/files/src/components/global/C_RightToolbar/data.ts +228 -0
- package/files/src/components/global/C_RightToolbar/index.scss +44 -0
- package/files/src/components/global/C_RightToolbar/index.vue +34 -336
- package/files/src/components/global/C_Splitter/index.scss +61 -0
- package/files/src/components/global/C_Splitter/index.vue +2 -64
- package/files/src/components/global/C_SvgIcon/index.scss +15 -0
- package/files/src/components/global/C_SvgIcon/index.vue +20 -50
- package/files/src/components/global/C_TagStatus/index.scss +20 -0
- package/files/src/components/global/C_TagStatus/index.vue +1 -22
- package/files/src/components/global/C_Tree/data.ts +61 -0
- package/files/src/components/global/C_Tree/index.vue +12 -53
- package/files/src/components/local/c_listModal/index.scss +4 -0
- package/files/src/components/local/c_listModal/index.vue +1 -1
- package/mcp/api/client.js +76 -0
- package/mcp/api/dictApi.js +40 -0
- package/mcp/api/menuApi.js +32 -0
- package/mcp/config.js +47 -0
- package/mcp/server.js +210 -0
- package/mcp/tools/dictSync.js +173 -0
- package/mcp/tools/menuSync.js +96 -0
- package/package.json +6 -9
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { ref, watch } from "vue";
|
|
2
|
+
import { Search } from "@element-plus/icons-vue";
|
|
3
|
+
|
|
4
|
+
// ===== 类型定义 =====
|
|
5
|
+
export interface Props {
|
|
6
|
+
tabs?: Array<{ label: string; name: string }>;
|
|
7
|
+
treeData: any[];
|
|
8
|
+
treeProps?: Record<string, string>;
|
|
9
|
+
defaultActiveTab?: string;
|
|
10
|
+
showSearch?: boolean;
|
|
11
|
+
searchPlaceholder?: string;
|
|
12
|
+
defaultExpandAll?: boolean;
|
|
13
|
+
highlightCurrent?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ===== 组件逻辑 =====
|
|
17
|
+
export function createTree(
|
|
18
|
+
props: Props,
|
|
19
|
+
emit: (e: "node-click" | "tab-change", ...args: any[]) => void,
|
|
20
|
+
) {
|
|
21
|
+
const activeTab = ref(props.defaultActiveTab ?? "tab1");
|
|
22
|
+
const searchKeyword = ref("");
|
|
23
|
+
const treeRef = ref<any>(null);
|
|
24
|
+
|
|
25
|
+
const filterNode = (value: string, data: any) => {
|
|
26
|
+
if (!value) return true;
|
|
27
|
+
const label = data[props.treeProps?.label || "label"];
|
|
28
|
+
return label && label.includes(value);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const handleNodeClick = (data: any) => {
|
|
32
|
+
emit("node-click", data);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
watch(searchKeyword, (val) => {
|
|
36
|
+
treeRef.value?.filter(val);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
watch(activeTab, (val) => {
|
|
40
|
+
emit("tab-change", val);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const clearSearch = () => {
|
|
44
|
+
searchKeyword.value = "";
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const switchTab = (tabName: string) => {
|
|
48
|
+
activeTab.value = tabName;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
Search,
|
|
53
|
+
activeTab,
|
|
54
|
+
searchKeyword,
|
|
55
|
+
treeRef,
|
|
56
|
+
filterNode,
|
|
57
|
+
handleNodeClick,
|
|
58
|
+
clearSearch,
|
|
59
|
+
switchTab,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -45,20 +45,7 @@
|
|
|
45
45
|
</template>
|
|
46
46
|
|
|
47
47
|
<script setup lang="ts">
|
|
48
|
-
import {
|
|
49
|
-
import { Search } from "@element-plus/icons-vue";
|
|
50
|
-
|
|
51
|
-
// Props 定义
|
|
52
|
-
interface Props {
|
|
53
|
-
tabs?: Array<{ label: string; name: string }>;
|
|
54
|
-
treeData: any[];
|
|
55
|
-
treeProps?: Record<string, string>;
|
|
56
|
-
defaultActiveTab?: string;
|
|
57
|
-
showSearch?: boolean;
|
|
58
|
-
searchPlaceholder?: string;
|
|
59
|
-
defaultExpandAll?: boolean;
|
|
60
|
-
highlightCurrent?: boolean;
|
|
61
|
-
}
|
|
48
|
+
import { createTree, type Props } from "./data";
|
|
62
49
|
|
|
63
50
|
const props = withDefaults(defineProps<Props>(), {
|
|
64
51
|
treeProps: () => ({ children: "children", label: "label" }),
|
|
@@ -69,51 +56,23 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
69
56
|
highlightCurrent: true
|
|
70
57
|
});
|
|
71
58
|
|
|
72
|
-
// Emits 定义
|
|
73
59
|
const emit = defineEmits<{
|
|
74
60
|
(e: "node-click", data: any): void;
|
|
75
61
|
(e: "tab-change", tabName: string): void;
|
|
76
62
|
}>();
|
|
77
63
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const searchKeyword = ref("");
|
|
81
|
-
const treeRef = ref<any>(null);
|
|
82
|
-
|
|
83
|
-
// 过滤节点
|
|
84
|
-
const filterNode = (value: string, data: any) => {
|
|
85
|
-
if (!value) return true;
|
|
86
|
-
const label = data[props.treeProps.label || "label"];
|
|
87
|
-
return label && label.includes(value);
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
// 节点点击
|
|
91
|
-
const handleNodeClick = (data: any) => {
|
|
92
|
-
emit("node-click", data);
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
// 监听搜索关键词
|
|
96
|
-
watch(searchKeyword, (val) => {
|
|
97
|
-
treeRef.value?.filter(val);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
// 监听tab切换
|
|
101
|
-
watch(activeTab, (val) => {
|
|
102
|
-
emit("tab-change", val);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
// 暴露方法
|
|
106
|
-
defineExpose({
|
|
107
|
-
treeRef,
|
|
108
|
-
searchKeyword,
|
|
64
|
+
const {
|
|
65
|
+
Search,
|
|
109
66
|
activeTab,
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
});
|
|
67
|
+
searchKeyword,
|
|
68
|
+
treeRef,
|
|
69
|
+
filterNode,
|
|
70
|
+
handleNodeClick,
|
|
71
|
+
clearSearch,
|
|
72
|
+
switchTab
|
|
73
|
+
} = createTree(props, emit);
|
|
74
|
+
|
|
75
|
+
defineExpose({ treeRef, searchKeyword, activeTab, clearSearch, switchTab });
|
|
117
76
|
</script>
|
|
118
77
|
|
|
119
78
|
<style scoped lang="scss" src="./index.scss"></style>
|
|
@@ -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
|
+
})
|