@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.
Files changed (101) hide show
  1. package/CHANGELOG.md +33 -22
  2. package/README.md +11 -103
  3. package/bin/wl-skills.js +2 -42
  4. package/files/.github/guides/README.md +13 -13
  5. package/files/.github/guides/architecture.md +555 -555
  6. package/files/.github/guides/usage.md +176 -173
  7. package/files/.github/reports/README.md +65 -65
  8. package/files/.github/reports/SYS_DICT_INFO.md +50 -50
  9. package/files/.github/reports/SYS_MENU_INFO.md +247 -247
  10. package/files/.github/reports/SYS_PERMISSION_INFO.md +20 -20
  11. 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
  12. 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
  13. package/files/.github/skills/_compat/README.md +108 -108
  14. package/files/.github/skills/_compat/headers/agents.txt +8 -8
  15. package/files/.github/skills/_compat/headers/claude-code.txt +7 -7
  16. package/files/.github/skills/_compat/headers/cline.txt +7 -7
  17. package/files/.github/skills/_compat/headers/cursor-mdc.txt +16 -16
  18. package/files/.github/skills/_compat/headers/cursor-rules.txt +7 -7
  19. package/files/.github/skills/_compat/headers/github-copilot.txt +1 -1
  20. package/files/.github/skills/_compat/headers/kiro.txt +10 -10
  21. package/files/.github/skills/_compat/headers/trae.txt +11 -11
  22. package/files/.github/skills/_compat/headers/windsurf.txt +7 -7
  23. package/files/.github/skills/_registry.md +81 -81
  24. package/files/.github/skills/core/api-contract/SKILL.md +344 -344
  25. package/files/.github/skills/core/api-contract/USAGE.md +110 -110
  26. package/files/.github/skills/core/convention-audit/SKILL.md +189 -189
  27. package/files/.github/skills/core/convention-audit/USAGE.md +99 -99
  28. package/files/.github/skills/core/page-codegen/SKILL.md +973 -973
  29. package/files/.github/skills/core/page-codegen/USAGE.md +102 -102
  30. package/files/.github/skills/core/page-codegen/templates/_index.md +46 -46
  31. package/files/.github/skills/core/page-codegen/templates/domains/_CONTRIBUTING.md +107 -107
  32. package/files/.github/skills/core/page-codegen/templates/domains/produce/TPL-OPERATION-STATION.md +442 -442
  33. package/files/.github/skills/core/page-codegen/templates/domains/sale/README.md +26 -26
  34. package/files/.github/skills/core/page-codegen/templates/universal/TPL-CHANGE-HISTORY.md +276 -276
  35. package/files/.github/skills/core/page-codegen/templates/universal/TPL-DETAIL-TABS.md +1145 -1145
  36. package/files/.github/skills/core/page-codegen/templates/universal/TPL-DRIVEN.md +124 -124
  37. package/files/.github/skills/core/page-codegen/templates/universal/TPL-FORM-ROUTE.md +436 -436
  38. package/files/.github/skills/core/page-codegen/templates/universal/TPL-LIST.md +191 -191
  39. package/files/.github/skills/core/page-codegen/templates/universal/TPL-MASTER-DETAIL.md +148 -148
  40. package/files/.github/skills/core/page-codegen/templates/universal/TPL-RECORD-FORM.md +376 -376
  41. package/files/.github/skills/core/page-codegen/templates/universal/TPL-TREE-LIST.md +186 -186
  42. package/files/.github/skills/core/prototype-scan/SKILL.md +498 -498
  43. package/files/.github/skills/core/prototype-scan/USAGE.md +95 -95
  44. package/files/.github/skills/core/template-extract/SKILL.md +139 -139
  45. package/files/.github/skills/core/template-extract/USAGE.md +93 -93
  46. package/files/.github/skills/domain/README.md +51 -51
  47. package/files/.github/skills/sync/env.local.json +2 -1
  48. package/files/.github/skills/sync/menu-sync/SKILL.md +263 -263
  49. package/files/.github/skills/sync/menu-sync/USAGE.md +104 -104
  50. package/files/.github/skills/sync/menu-sync/env/env.local.json +7 -7
  51. package/files/.github/skills/sync/menu-sync/env/guide.md +99 -99
  52. package/files/.github/skills/sync/permission-sync/SKILL.draft.md +91 -91
  53. package/files/.github/standards/01-toolchain.md +57 -57
  54. package/files/.github/standards/02-code-structure.md +111 -111
  55. package/files/.github/standards/03-comments.md +53 -53
  56. package/files/.github/standards/04-coding-basics.md +33 -33
  57. package/files/.github/standards/05-logging.md +38 -38
  58. package/files/.github/standards/06-security.md +44 -44
  59. package/files/.github/standards/07-config.md +52 -52
  60. package/files/.github/standards/08-git.md +60 -60
  61. package/files/.github/standards/09-typescript.md +71 -71
  62. package/files/.github/standards/10-pinia.md +57 -57
  63. package/files/.github/standards/11-form-validation.md +81 -81
  64. package/files/.github/standards/12-base-table.md +153 -153
  65. package/files/.github/standards/13-platform-components.md +123 -123
  66. package/files/.github/standards/index.md +89 -89
  67. package/files/demo/produce/aiflow/mmwr-customer-apply-add/api.md +1 -1
  68. package/files/demo/produce/aiflow/mmwr-customer-apply-change-history/data.ts +196 -196
  69. package/files/demo/produce/aiflow/mmwr-customer-apply-change-history/index.scss +150 -150
  70. package/files/demo/produce/aiflow/mmwr-customer-apply-change-history/index.vue +79 -79
  71. package/files/docs/jh-date-range.md +257 -257
  72. package/files/docs/jh-date.md +222 -222
  73. package/files/docs/jh-dept-picker.md +190 -190
  74. package/files/docs/jh-drag-row.md +590 -590
  75. package/files/docs/jh-file-upload.md +216 -216
  76. package/files/docs/jh-picker.md +218 -218
  77. package/files/docs/jh-select.md +148 -148
  78. package/files/docs/jh-text.md +248 -248
  79. package/files/docs/jh-user-picker.md +197 -197
  80. package/files/docs/request.md +24 -9
  81. package/files/src/components/global/C_RightToolbar/data.ts +228 -0
  82. package/files/src/components/global/C_RightToolbar/index.scss +44 -0
  83. package/files/src/components/global/C_RightToolbar/index.vue +34 -336
  84. package/files/src/components/global/C_Splitter/index.scss +61 -0
  85. package/files/src/components/global/C_Splitter/index.vue +2 -64
  86. package/files/src/components/global/C_SvgIcon/index.scss +15 -0
  87. package/files/src/components/global/C_SvgIcon/index.vue +20 -50
  88. package/files/src/components/global/C_TagStatus/index.scss +20 -0
  89. package/files/src/components/global/C_TagStatus/index.vue +1 -22
  90. package/files/src/components/global/C_Tree/data.ts +61 -0
  91. package/files/src/components/global/C_Tree/index.vue +12 -53
  92. package/files/src/components/local/c_listModal/index.scss +4 -0
  93. package/files/src/components/local/c_listModal/index.vue +1 -1
  94. package/mcp/api/client.js +76 -0
  95. package/mcp/api/dictApi.js +40 -0
  96. package/mcp/api/menuApi.js +32 -0
  97. package/mcp/config.js +47 -0
  98. package/mcp/server.js +210 -0
  99. package/mcp/tools/dictSync.js +173 -0
  100. package/mcp/tools/menuSync.js +96 -0
  101. 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 { ref, watch } from "vue";
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
- const activeTab = ref(props.defaultActiveTab);
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
- clearSearch: () => {
111
- searchKeyword.value = "";
112
- },
113
- switchTab: (tabName: string) => {
114
- activeTab.value = tabName;
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,4 @@
1
+ .dialog-footer {
2
+ display: flex;
3
+ justify-content: flex-end;
4
+ }
@@ -133,4 +133,4 @@ defineExpose({
133
133
  });
134
134
  </script>
135
135
 
136
- <style scoped lang="scss"></style>
136
+ <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
+ })