cuxml 1.0.6 → 2.1.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.
@@ -0,0 +1,260 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const lodash = require('lodash');
4
+ const XMLParser = require('./parser');
5
+ const { generateCallbackTemplate, processFunctionName } = require('./templates');
6
+
7
+ /**
8
+ * 回调函数转换器
9
+ * 负责将 XML 中的回调函数配置转换为 JavaScript 代码
10
+ */
11
+ class XMLConverter {
12
+ /**
13
+ * 将 XML 文件转换为 JavaScript 回调函数文件
14
+ * @param {string} inputPath - 输入的 XML 文件路径
15
+ * @param {string} outputPath - 输出的 JS 文件路径
16
+ * @param {object} options - 转换选项
17
+ * @param {boolean} [options.overwrite=false] - 是否覆盖输出文件
18
+ * @returns {object} 转换结果
19
+ */
20
+ static convert(inputPath, outputPath, options = {}) {
21
+ // 验证输入参数
22
+ if (!inputPath || typeof inputPath !== 'string') {
23
+ throw new Error('输入文件路径必须是非空字符串');
24
+ }
25
+
26
+ if (!outputPath || typeof outputPath !== 'string') {
27
+ throw new Error('输出文件路径必须是非空字符串');
28
+ }
29
+
30
+ if (!inputPath.endsWith('.xml')) {
31
+ throw new Error('输入文件必须是 .xml 格式');
32
+ }
33
+
34
+ if (!outputPath.endsWith('.js')) {
35
+ throw new Error('输出文件必须是 .js 格式');
36
+ }
37
+
38
+ // 检查输入文件是否存在
39
+ if (!fs.existsSync(inputPath)) {
40
+ throw new Error(`输入文件不存在: ${inputPath}`);
41
+ }
42
+
43
+ // 加载配置
44
+ const clashAttributes = require('../lib/clashAttributes.json').crashs;
45
+ const stDelegate = require('../lib/StDelegate.json');
46
+
47
+ // 解析 XML
48
+ const flatElements = XMLParser.parseAndFlatten(inputPath);
49
+
50
+ // 提取有回调函数的元素
51
+ const callbackElements = this.extractCallbackElements(flatElements, stDelegate, clashAttributes);
52
+
53
+ if (callbackElements.length === 0) {
54
+ return {
55
+ success: true,
56
+ message: '没有找到需要转换的回调函数',
57
+ functionsGenerated: 0
58
+ };
59
+ }
60
+
61
+ // 按函数名分组
62
+ const functionGroups = this.groupByFunctionName(callbackElements);
63
+
64
+ // 生成回调函数代码
65
+ const generatedFunctions = this.generateFunctions(functionGroups);
66
+
67
+ // 写入输出文件
68
+ this.writeOutputFile(outputPath, generatedFunctions, options.overwrite);
69
+
70
+ return {
71
+ success: true,
72
+ message: '转换成功',
73
+ functionsGenerated: generatedFunctions.length,
74
+ outputPath: outputPath
75
+ };
76
+ }
77
+
78
+ /**
79
+ * 提取包含回调函数的元素
80
+ * @param {Array} elements - 扁平化的元素数组
81
+ * @param {object} stDelegate - 回调函数配置
82
+ * @param {Array} clashAttributes - 冲突属性配置
83
+ * @returns {Array} 包含回调函数的元素
84
+ */
85
+ static extractCallbackElements(elements, stDelegate, clashAttributes) {
86
+ const results = [];
87
+
88
+ elements.forEach(element => {
89
+ if (!element.attributes || Object.keys(element.attributes).length === 0) {
90
+ return;
91
+ }
92
+
93
+ // 获取该元素支持的回调函数属性
94
+ const elementCallbacks = stDelegate[element.name];
95
+ if (!elementCallbacks) {
96
+ return;
97
+ }
98
+
99
+ // 检查是否有回调函数
100
+ const attrKeys = Object.keys(element.attributes);
101
+ const hasCallback = elementCallbacks.some(cb => {
102
+ return attrKeys.includes(cb) && element.attributes[cb].trim() !== '';
103
+ });
104
+
105
+ if (!hasCallback) {
106
+ return;
107
+ }
108
+
109
+ // 检查是否有冲突属性
110
+ const hasClash = clashAttributes.some(clash => {
111
+ return lodash.intersection(attrKeys, clash).length > 1;
112
+ });
113
+
114
+ if (hasClash) {
115
+ return;
116
+ }
117
+
118
+ // 提取回调函数信息
119
+ elementCallbacks.forEach(cb => {
120
+ const callbackValue = element.attributes[cb];
121
+ if (callbackValue && callbackValue.trim()) {
122
+ results.push({
123
+ functionName: processFunctionName(callbackValue),
124
+ xmlPath: element.xmlpath,
125
+ id: element.attributes.id,
126
+ elementName: element.name,
127
+ attribute: cb
128
+ });
129
+ }
130
+ });
131
+ });
132
+
133
+ return results;
134
+ }
135
+
136
+ /**
137
+ * 按函数名分组
138
+ * @param {Array} callbackElements - 回调元素数组
139
+ * @returns {object} 分组后的对象
140
+ */
141
+ static groupByFunctionName(callbackElements) {
142
+ return lodash.groupBy(callbackElements, 'functionName');
143
+ }
144
+
145
+ /**
146
+ * 生成回调函数代码
147
+ * @param {object} functionGroups - 函数分组对象
148
+ * @returns {Array} 生成的函数信息数组
149
+ */
150
+ static generateFunctions(functionGroups) {
151
+ const functions = [];
152
+
153
+ Object.keys(functionGroups).forEach(functionName => {
154
+ const elements = functionGroups[functionName];
155
+ const controls = elements.map(el => ({
156
+ id: el.id,
157
+ xmlPath: el.xmlPath
158
+ }));
159
+
160
+ const functionCode = generateCallbackTemplate(functionName, controls);
161
+
162
+ functions.push({
163
+ name: functionName,
164
+ code: functionCode,
165
+ controls: controls.length
166
+ });
167
+ });
168
+
169
+ return functions;
170
+ }
171
+
172
+ /**
173
+ * 写入输出文件
174
+ * @param {string} outputPath - 输出文件路径
175
+ * @param {Array} functions - 函数信息数组
176
+ * @param {boolean} overwrite - 是否覆盖
177
+ */
178
+ static writeOutputFile(outputPath, functions, overwrite = false) {
179
+ const dir = path.dirname(outputPath);
180
+
181
+ // 确保目录存在
182
+ if (!fs.existsSync(dir)) {
183
+ fs.mkdirSync(dir, { recursive: true });
184
+ }
185
+
186
+ // 如果是覆盖模式,先清空文件
187
+ if (overwrite) {
188
+ fs.writeFileSync(outputPath, '', 'utf-8');
189
+ }
190
+
191
+ // 如果文件不存在,创建空文件
192
+ if (!fs.existsSync(outputPath)) {
193
+ fs.writeFileSync(outputPath, '', 'utf-8');
194
+ }
195
+
196
+ // 读取现有内容
197
+ const existingContent = fs.readFileSync(outputPath, 'utf-8');
198
+
199
+ // 追加新函数
200
+ functions.forEach(func => {
201
+ // 检查函数是否已存在
202
+ const functionRegex = new RegExp(`function\\s+${func.name}\\s*\\(`);
203
+
204
+ if (!existingContent.match(functionRegex)) {
205
+ fs.appendFileSync(outputPath, func.code + '\n', 'utf-8');
206
+ }
207
+ });
208
+
209
+ // 添加导出语句
210
+ const exportStatement = `
211
+ // 导出所有回调函数(如果不需要导出,可以注释掉以下语句)
212
+ module.exports = {
213
+ ${functions.map(func => ` ${func.name},`).join('\n')}
214
+ };
215
+ `;
216
+
217
+ // 检查是否已经有导出语句
218
+ if (!existingContent.includes('module.exports = {')) {
219
+ fs.appendFileSync(outputPath, exportStatement, 'utf-8');
220
+ }
221
+ }
222
+
223
+ /**
224
+ * 监视文件变化并自动转换
225
+ * @param {string} inputPath - 输入文件路径
226
+ * @param {string} outputPath - 输出文件路径
227
+ * @param {object} options - 转换选项
228
+ * @returns {object} watcher 对象
229
+ */
230
+ static watch(inputPath, outputPath, options = {}) {
231
+ const chokidar = require('chokidar');
232
+
233
+ console.log(`👀 开始监视文件: ${inputPath}`);
234
+
235
+ // 初始转换
236
+ this.convert(inputPath, outputPath, options);
237
+
238
+ const watcher = chokidar.watch(inputPath);
239
+
240
+ watcher.on('change', () => {
241
+ console.log(`📝 检测到文件变化,开始转换...`);
242
+ try {
243
+ const result = this.convert(inputPath, outputPath, options);
244
+ if (result.success) {
245
+ console.log(`✅ 转换完成,生成了 ${result.functionsGenerated} 个函数`);
246
+ }
247
+ } catch (error) {
248
+ console.error(`❌ 转换失败: ${error.message}`);
249
+ }
250
+ });
251
+
252
+ watcher.on('error', (error) => {
253
+ console.error(`❌ 监视错误: ${error.message}`);
254
+ });
255
+
256
+ return watcher;
257
+ }
258
+ }
259
+
260
+ module.exports = XMLConverter;
package/src/mcp.js ADDED
@@ -0,0 +1,289 @@
1
+ const CUXMLAPI = require('./api');
2
+
3
+ /**
4
+ * CUXML MCP (Model Context Protocol) 插件
5
+ * 提供 MCP 标准接口供 AI 工具链调用
6
+ */
7
+ class CUXMLMCP {
8
+ /**
9
+ * MCP 服务器信息
10
+ */
11
+ static getServerInfo() {
12
+ return {
13
+ name: 'cuxml',
14
+ version: require('../package.json').version,
15
+ description: 'CUXML - WPS Office RibbonUI XML 处理工具'
16
+ };
17
+ }
18
+
19
+ /**
20
+ * MCP 工具列表
21
+ */
22
+ static getTools() {
23
+ return [
24
+ {
25
+ name: 'convert',
26
+ description: '将 RibbonUI XML 文件转换为 JavaScript 回调函数文件',
27
+ inputSchema: {
28
+ type: 'object',
29
+ properties: {
30
+ inputFile: {
31
+ type: 'string',
32
+ description: '输入的 XML 文件路径'
33
+ },
34
+ outputFile: {
35
+ type: 'string',
36
+ description: '输出的 JS 文件路径'
37
+ },
38
+ overwrite: {
39
+ type: 'boolean',
40
+ description: '是否覆盖输出文件',
41
+ default: false
42
+ }
43
+ },
44
+ required: ['inputFile', 'outputFile']
45
+ }
46
+ },
47
+ {
48
+ name: 'check',
49
+ description: '检查 XML 文件或字符串是否符合 CustomUI 规范',
50
+ inputSchema: {
51
+ type: 'object',
52
+ properties: {
53
+ input: {
54
+ type: 'string',
55
+ description: 'XML 文件路径或 XML 字符串'
56
+ },
57
+ detailed: {
58
+ type: 'boolean',
59
+ description: '是否返回详细信息',
60
+ default: false
61
+ }
62
+ },
63
+ required: ['input']
64
+ }
65
+ }
66
+ ];
67
+ }
68
+
69
+ /**
70
+ * 执行 MCP 工具
71
+ * @param {string} toolName - 工具名称
72
+ * @param {object} args - 工具参数
73
+ * @returns {Promise<object>} 执行结果
74
+ */
75
+ static async executeTool(toolName, args) {
76
+ try {
77
+ switch (toolName) {
78
+ case 'convert':
79
+ return await this.convert(args);
80
+ case 'check':
81
+ return await this.check(args);
82
+ default:
83
+ throw new Error(`未知的工具: ${toolName}`);
84
+ }
85
+ } catch (error) {
86
+ return {
87
+ success: false,
88
+ error: error.message
89
+ };
90
+ }
91
+ }
92
+
93
+ /**
94
+ * 转换 XML 文件
95
+ * @param {object} args - 参数
96
+ * @param {string} args.inputFile - 输入文件
97
+ * @param {string} args.outputFile - 输出文件
98
+ * @param {boolean} args.overwrite - 是否覆盖
99
+ * @returns {Promise<object>} 转换结果
100
+ */
101
+ static async convert(args) {
102
+ const { inputFile, outputFile, overwrite = false } = args;
103
+
104
+ const result = await CUXMLAPI.convert(inputFile, outputFile, { overwrite });
105
+
106
+ return {
107
+ success: result.success,
108
+ message: result.message,
109
+ functionsGenerated: result.functionsGenerated,
110
+ outputPath: result.outputPath
111
+ };
112
+ }
113
+
114
+ /**
115
+ * 检查 XML
116
+ * @param {object} args - 参数
117
+ * @param {string} args.input - XML 文件或字符串
118
+ * @param {boolean} args.detailed - 是否详细
119
+ * @returns {Promise<object>} 检查结果
120
+ */
121
+ static async check(args) {
122
+ const { input, detailed = false } = args;
123
+
124
+ const result = await CUXMLAPI.check(input, { detailed });
125
+
126
+ if (detailed) {
127
+ return {
128
+ success: result.errors === 0,
129
+ ...result
130
+ };
131
+ } else {
132
+ return {
133
+ success: result.success,
134
+ totalNodes: result.totalNodes,
135
+ passed: result.passed,
136
+ errors: result.errors,
137
+ warnings: result.warnings
138
+ };
139
+ }
140
+ }
141
+
142
+ /**
143
+ * MCP 资源列表
144
+ */
145
+ static getResources() {
146
+ return [
147
+ {
148
+ uri: 'cuxml://config/simpleTypes',
149
+ name: 'Simple Types 配置',
150
+ description: 'CustomUI Simple Types 规范配置',
151
+ mimeType: 'application/json'
152
+ },
153
+ {
154
+ uri: 'cuxml://config/clashAttributes',
155
+ name: '冲突属性配置',
156
+ description: 'CustomUI 冲突属性配置',
157
+ mimeType: 'application/json'
158
+ },
159
+ {
160
+ uri: 'cuxml://config/stDelegate',
161
+ name: '回调函数属性配置',
162
+ description: 'CustomUI 回调函数属性配置',
163
+ mimeType: 'application/json'
164
+ }
165
+ ];
166
+ }
167
+
168
+ /**
169
+ * 读取 MCP 资源
170
+ * @param {string} uri - 资源 URI
171
+ * @returns {Promise<object>} 资源内容
172
+ */
173
+ static async readResource(uri) {
174
+ try {
175
+ switch (uri) {
176
+ case 'cuxml://config/simpleTypes':
177
+ return {
178
+ uri: uri,
179
+ mimeType: 'application/json',
180
+ content: JSON.stringify(require('../lib/simpleTypes.json'), null, 2)
181
+ };
182
+ case 'cuxml://config/clashAttributes':
183
+ return {
184
+ uri: uri,
185
+ mimeType: 'application/json',
186
+ content: JSON.stringify(require('../lib/clashAttributes.json'), null, 2)
187
+ };
188
+ case 'cuxml://config/stDelegate':
189
+ return {
190
+ uri: uri,
191
+ mimeType: 'application/json',
192
+ content: JSON.stringify(require('../lib/StDelegate.json'), null, 2)
193
+ };
194
+ default:
195
+ throw new Error(`未知的资源: ${uri}`);
196
+ }
197
+ } catch (error) {
198
+ return {
199
+ uri: uri,
200
+ error: error.message
201
+ };
202
+ }
203
+ }
204
+
205
+ /**
206
+ * MCP 提示模板
207
+ */
208
+ static getPrompts() {
209
+ return [
210
+ {
211
+ name: 'generate-ribbon-ui',
212
+ description: '生成 RibbonUI XML 配置',
213
+ arguments: [
214
+ {
215
+ name: 'requirements',
216
+ description: '功能需求描述',
217
+ required: true
218
+ }
219
+ ]
220
+ },
221
+ {
222
+ name: 'debug-xml',
223
+ description: '调试 XML 配置问题',
224
+ arguments: [
225
+ {
226
+ name: 'xmlContent',
227
+ description: 'XML 内容',
228
+ required: true
229
+ },
230
+ {
231
+ name: 'error',
232
+ description: '错误信息',
233
+ required: false
234
+ }
235
+ ]
236
+ }
237
+ ];
238
+ }
239
+ }
240
+
241
+ // 如果直接运行此文件,启动 MCP 服务器
242
+ if (require.main === module) {
243
+ const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
244
+ const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
245
+
246
+ const server = new Server(
247
+ CUXMLMCP.getServerInfo(),
248
+ {
249
+ capabilities: {
250
+ tools: {},
251
+ resources: {},
252
+ prompts: {}
253
+ }
254
+ }
255
+ );
256
+
257
+ // 注册工具
258
+ server.setRequestHandler('tools/list', async () => {
259
+ return { tools: CUXMLMCP.getTools() };
260
+ });
261
+
262
+ server.setRequestHandler('tools/call', async (request) => {
263
+ const { name, arguments: args } = request.params;
264
+ return await CUXMLMCP.executeTool(name, args);
265
+ });
266
+
267
+ // 注册资源
268
+ server.setRequestHandler('resources/list', async () => {
269
+ return { resources: CUXMLMCP.getResources() };
270
+ });
271
+
272
+ server.setRequestHandler('resources/read', async (request) => {
273
+ return await CUXMLMCP.readResource(request.params.uri);
274
+ });
275
+
276
+ // 注册提示
277
+ server.setRequestHandler('prompts/list', async () => {
278
+ return { prompts: CUXMLMCP.getPrompts() };
279
+ });
280
+
281
+ // 启动服务器
282
+ const transport = new StdioServerTransport();
283
+ server.connect(transport).catch(error => {
284
+ console.error('MCP 服务器启动失败:', error);
285
+ process.exit(1);
286
+ });
287
+ }
288
+
289
+ module.exports = CUXMLMCP;
package/src/parser.js ADDED
@@ -0,0 +1,117 @@
1
+ const xmljs = require('xml-js');
2
+ const fs = require('fs');
3
+ const lodash = require('lodash');
4
+
5
+ /**
6
+ * XML 解析器模块
7
+ * 负责解析 XML 文件并将其转换为扁平化的 JSON 结构
8
+ */
9
+ class XMLParser {
10
+ /**
11
+ * 解析 XML 文件
12
+ * @param {string} xmlPath - XML 文件路径
13
+ * @returns {object} 解析后的 JSON 对象
14
+ * @throws {Error} 如果文件不存在或解析失败
15
+ */
16
+ static parseFile(xmlPath) {
17
+ if (!xmlPath || typeof xmlPath !== 'string') {
18
+ throw new Error('XML 文件路径必须是非空字符串');
19
+ }
20
+
21
+ if (!fs.existsSync(xmlPath)) {
22
+ throw new Error(`XML 文件不存在: ${xmlPath}`);
23
+ }
24
+
25
+ const xmlContent = fs.readFileSync(xmlPath, 'utf-8');
26
+
27
+ if (!xmlContent.trim()) {
28
+ throw new Error('XML 文件为空');
29
+ }
30
+
31
+ return this.parseString(xmlContent);
32
+ }
33
+
34
+ /**
35
+ * 解析 XML 字符串
36
+ * @param {string} xmlString - XML 字符串
37
+ * @returns {object} 解析后的 JSON 对象
38
+ * @throws {Error} 如果解析失败
39
+ */
40
+ static parseString(xmlString) {
41
+ try {
42
+ return xmljs.xml2js(xmlString, { compact: false, spaces: 4 });
43
+ } catch (error) {
44
+ throw new Error(`XML 解析失败: ${error.message}`);
45
+ }
46
+ }
47
+
48
+ /**
49
+ * 将 XML JSON 对象扁平化为数组
50
+ * @param {object} obj - XML JSON 对象
51
+ * @returns {Array} 扁平化后的元素数组
52
+ */
53
+ static flatten(obj) {
54
+ const result = [];
55
+ const resNames = [];
56
+
57
+ function flattenInternal(currentObj, path = []) {
58
+ // 提取 type, name, attributes
59
+ const wanted = lodash.pick(currentObj, ['type', 'name', 'attributes']);
60
+
61
+ if (Object.keys(wanted).length > 0) {
62
+ Object.assign(wanted, { xmlpath: resNames.join('>') });
63
+ result.push(wanted);
64
+ }
65
+
66
+ if (currentObj && typeof currentObj === 'object') {
67
+ Object.keys(currentObj).forEach(key => {
68
+ const value = currentObj[key];
69
+ const newPath = path.concat(key);
70
+
71
+ // 处理数组索引
72
+ if (Number.isInteger(Number(key))) {
73
+ let eItem = `${value.name}[${key}]`;
74
+
75
+ if (Number(key) > 0) {
76
+ const beDeleted = `${value.name}[${key - 1}]`;
77
+ const index = resNames.lastIndexOf(beDeleted);
78
+ const resNamesCopy = resNames.slice(0, index);
79
+ resNames.splice(0, resNames.length, ...resNamesCopy, eItem);
80
+ } else {
81
+ resNames.push(eItem);
82
+ }
83
+ }
84
+
85
+ if (value && typeof value === 'object') {
86
+ flattenInternal(value, newPath);
87
+ }
88
+ });
89
+ }
90
+ }
91
+
92
+ flattenInternal(obj);
93
+ return result;
94
+ }
95
+
96
+ /**
97
+ * 解析并扁平化 XML 文件
98
+ * @param {string} xmlPath - XML 文件路径
99
+ * @returns {Array} 扁平化后的元素数组
100
+ */
101
+ static parseAndFlatten(xmlPath) {
102
+ const jsonObj = this.parseFile(xmlPath);
103
+ return this.flatten(jsonObj);
104
+ }
105
+
106
+ /**
107
+ * 解析字符串并扁平化
108
+ * @param {string} xmlString - XML 字符串
109
+ * @returns {Array} 扁平化后的元素数组
110
+ */
111
+ static parseStringAndFlatten(xmlString) {
112
+ const jsonObj = this.parseString(xmlString);
113
+ return this.flatten(jsonObj);
114
+ }
115
+ }
116
+
117
+ module.exports = XMLParser;