cuxml 2.1.1 → 2.1.3

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/README.md CHANGED
@@ -3,17 +3,17 @@
3
3
  > CU: `'Can Use'` or `'customUI'`, whatever……
4
4
 
5
5
  [![npm version](https://badge.fury.io/js/cuxml.svg)](https://www.npmjs.com/package/cuxml)
6
- [![License](https://img.shields.io/npm/l/cuxml.svg)](LICENSE)
7
6
 
8
7
  **CUXML** 是一个专为 WPS Office JS 插件开发者设计的工具,用于处理 RibbonUI XML 文件。它可以显著简化开发流程,让你更专注于业务逻辑。
9
8
 
10
9
  ## ✨ 特性
11
10
 
12
11
  - 🔄 **XML 到 JavaScript 转换** - 自动将 RibbonUI XML 的回调函数配置转换为 JavaScript 代码
13
- - 🔍 **XML 合规性检查** - 全面检查 XML 文件是否符合 CustomUI 规范
12
+ - 🔍 **XML 合规性检查** - 全面检查 XML 文件是否符合 CustomUI 规范,详细显示错误和警告
14
13
  - 👀 **文件监视** - 监视 XML 文件变化,自动重新转换
15
14
  - 📦 **模块化 API** - 提供完整的编程接口,支持同步和异步调用
16
15
  - 🔌 **MCP 支持** - 可作为 MCP 插件集成到 AI 工具链中
16
+ - ⚡ **转换前自动检查** - 转换前自动运行检查,提高开发效率
17
17
 
18
18
  ## 📦 安装
19
19
 
@@ -34,22 +34,54 @@ cuxml -i ./ribbon.xml # 输出到 ./ribbon_temp.js
34
34
  # 指定输出路径
35
35
  cuxml -i ./ribbon.xml -o ./ribbon.js
36
36
 
37
- # 监视模式(文件变化时自动转换)
38
- cuxml -i ./ribbon.xml -w # 输出到 ./ribbon_temp.js
37
+ # 监视模式(文件变化时自动转换,默认覆盖输出文件)
38
+ cuxml -i ./ribbon.xml -w
39
+
40
+ # 非 watch 模式下覆盖已存在的输出文件
41
+ cuxml -i ./ribbon.xml -o ./ribbon.js --overwrite
39
42
 
40
43
  # 使用默认输入路径(当前目录下的 ribbon.xml)
41
44
  cuxml # 需要在当前目录有 ribbon.xml 文件
45
+
46
+ # 注意:转换前会自动进行 XML 检查,有错误时会阻止转换
47
+ ```
48
+
49
+ **选项说明:**
50
+
51
+ | 选项 | 说明 |
52
+ |------|------|
53
+ | `-i, --input` | 输入的 XML 文件路径 |
54
+ | `-o, --output` | 输出的 JS 文件路径 |
55
+ | `-w, --watch` | 监视文件变化并自动转换(自动覆盖) |
56
+ | `--overwrite` | 覆盖输出文件(非 watch 模式使用) |
57
+
58
+ #### convert 子命令
59
+
60
+ ```bash
61
+ # 基本转换
62
+ cuxml convert ./ribbon.xml
63
+
64
+ # 指定输入输出
65
+ cuxml convert ./ribbon.xml ./ribbon.js
66
+
67
+ # 监视模式
68
+ cuxml convert ./ribbon.xml ./ribbon.js -w
69
+
70
+ # 非 watch 模式下覆盖文件
71
+ cuxml convert ./ribbon.xml ./ribbon.js --overwrite
42
72
  ```
43
73
 
44
74
  #### 检查 XML 文件
45
75
 
46
76
  ```bash
47
77
  cuxml check ./ribbon.xml
48
-
49
- # 显示详细信息
50
- cuxml check ./ribbon.xml -v
51
78
  ```
52
79
 
80
+ 检查命令会自动显示详细的错误和警告信息,包括:
81
+ - 错误类型和位置
82
+ - 警告详情和该元素支持的所有属性(以表格形式显示)
83
+ - 支持的属性会按 4 列格式化输出,便于快速查找
84
+
53
85
  #### 查看帮助
54
86
 
55
87
  ```bash
@@ -109,10 +141,6 @@ npm install @modelcontextprotocol/sdk
109
141
  }
110
142
  ```
111
143
 
112
- ## 📖 详细文档
113
-
114
- 完整的使用文档和技术细节请参考:[CUXML Agent 文档](./test/agent_new.md)
115
-
116
144
  ## 🔧 功能说明
117
145
 
118
146
  ### XML 到 JavaScript 转换
@@ -156,43 +184,9 @@ function handleClick(ctrl) {
156
184
  - ✅ 属性值类型验证
157
185
  - ✅ 元素不支持的属性检测
158
186
 
159
- ## 📁 项目结构
160
-
161
- ```
162
- cuxml/
163
- ├── src/
164
- │ ├── api.js # API 模块
165
- │ ├── checker.js # XML 检查器
166
- │ ├── cli.js # CLI 工具
167
- │ ├── converter.js # 转换器
168
- │ ├── mcp.js # MCP 插件
169
- │ ├── parser.js # XML 解析器
170
- │ └── templates.js # 模板生成器
171
- ├── lib/
172
- │ ├── simpleTypes.json # Simple Types 配置
173
- │ ├── clashAttributes.json # 冲突属性配置
174
- │ ├── StDelegate.json # 回调函数属性配置
175
- │ └── cbfTypes.json # Callback 函数类型配置
176
- ├── test/ # 测试文件和示例
177
- ├── index.js # 主入口
178
- └── package.json
179
- ```
180
-
181
- ## 🎯 最佳实践
182
-
183
- ### 1. 文件组织
184
-
185
- ```
186
- project/
187
- ├── src/
188
- │ └── ribbon.xml # RibbonUI 配置文件
189
- ├── gen/
190
- │ └── ribbon_callbacks.js # 自动生成的回调函数
191
- ├── main.js # 主逻辑文件
192
- └── package.json
193
- ```
187
+ 检查命令会以表格形式显示该元素支持的所有属性,便于快速查找正确的属性名。
194
188
 
195
- ### 2. 开发工作流
189
+ ### 开发工作流
196
190
 
197
191
  ```bash
198
192
  # 开发阶段:使用监视模式,实时更新
@@ -202,52 +196,19 @@ cuxml -i src/ribbon.xml -o gen/ribbon_callbacks.js -w
202
196
  cuxml check src/ribbon.xml && cuxml -i src/ribbon.xml -o gen/ribbon_callbacks.js
203
197
  ```
204
198
 
205
- ### 3. 错误处理
206
-
207
- ```javascript
208
- const cuxml = require('cuxml');
209
-
210
- try {
211
- cuxml.check('./ribbon.xml');
212
- cuxml.convert('./ribbon.xml', './ribbon.js');
213
- console.log('✅ 转换成功!');
214
- } catch (error) {
215
- console.error('❌ 转换失败:', error.message);
216
- // 处理错误
217
- }
218
- ```
219
-
220
199
  ## 🔗 相关资源
221
200
 
222
201
  - [Microsoft CustomUI 规范](https://learn.microsoft.com/en-us/openspecs/office_standards/ms-customui/574eeee8-7a03-406a-b95f-f9e51e53dd9d)
223
202
  - [Simple Types 参考](https://learn.microsoft.com/en-us/openspecs/office_standards/ms-customui/869c8c9a-45f8-4119-b068-f61e76d04322)
224
203
  - [WPS Office 开发文档](https://wps.cn/developer/)
225
- - [CUXML Agent 文档](./test/agent_new.md) - 完整的使用文档
226
-
227
- ## 🤝 贡献
228
-
229
- 欢迎贡献!请随时提交 Issue 或 Pull Request。
230
-
231
- ## 📄 许可证
232
-
233
- [ISC](LICENSE)
234
204
 
235
205
  ## 📊 版本历史
236
206
 
237
- ### v2.0.0 (2026-02-14)
238
-
239
- **重大更新:完全重构**
240
-
241
- - ✨ 全新的模块化架构
242
- - ✨ 完整的 API 模块(支持同步和异步)
243
- - ✨ 统一的 CLI 命令(`cuxml`)
244
- - ✨ MCP 协议支持
245
- - ✨ 完整的 JSDoc 文档
246
- - ✨ 改进的错误处理和日志输出
247
- - 🔧 优化项目结构
248
- - 🐛 修复多个已知问题
207
+ ### v2.1.3 (2026-04-20)
249
208
 
250
- ### v1.0.x
209
+ **功能增强**
251
210
 
252
- - 初始版本
253
- - 基本的 XML 转换和检查功能
211
+ - ✨ 改进检查命令输出 - 支持的属性以表格形式显示,提高可读性
212
+ - 转换前自动检查 - 转换前自动运行 XML 检查,有错误时阻止转换
213
+ - ✨ 详细的警告信息 - 未知属性和不支持的属性会显示该元素支持的所有属性
214
+ - 📝 更新 README 文档
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cuxml",
3
- "version": "2.1.1",
3
+ "version": "2.1.3",
4
4
  "description": "A tool to help use ribbon UI for your WPS Office Client JS add-in project.",
5
5
  "keywords": [
6
6
  "wps office",
package/src/checker.js CHANGED
@@ -228,7 +228,22 @@ class XMLChecker {
228
228
  .filter(r => r.warnings.length > 0)
229
229
  .forEach(r => {
230
230
  output += ` 路径: ${r.xmlpath}\n`;
231
- r.warnings.forEach(w => output += ` - ${w}\n`);
231
+ r.warnings.forEach(w => {
232
+ output += ` - ${w}\n`;
233
+ // 如果是未知属性或不支持的属性,列出该元素支持的所有属性
234
+ if ((w.startsWith('未知属性') || w.includes('不支持的属性')) && r.nodeElement && r.nodeElement.name) {
235
+ const elementName = r.nodeElement.name;
236
+ const supportedAttrs = elementsAttributes[elementName];
237
+ if (supportedAttrs && supportedAttrs.length > 0) {
238
+ // 先对数组进行排序
239
+ const sortedAttrs = [...supportedAttrs].sort();
240
+ output += ` 支持的属性:\n`;
241
+ sortedAttrs.forEach(attr => {
242
+ output += ` - ${attr}\n`;
243
+ });
244
+ }
245
+ }
246
+ });
232
247
  });
233
248
  }
234
249
 
package/src/cli.js CHANGED
@@ -33,15 +33,20 @@ program
33
33
  .option('-i, --input <file>', '输入的 XML 文件路径', './ribbon.xml')
34
34
  .option('-o, --output <file>', '输出的 JS 文件路径')
35
35
  .option('-w, --watch', '监视文件变化并自动转换')
36
+ .option('--overwrite', '覆盖输出文件(非 watch 模式)')
36
37
  .action((options) => {
37
38
  const outputPath = options.output || getDefaultOutputPath(options.input);
38
39
  try {
40
+ const watchOptions = options.watch ? { overwrite: true } : { overwrite: options.overwrite };
41
+
39
42
  if (options.watch) {
40
43
  console.log('🚀 启动监视模式...\n');
41
- CUXMLAPI.watch(options.input, outputPath);
44
+ CUXMLAPI.watch(options.input, outputPath, watchOptions);
42
45
  } else {
43
46
  console.log('🔄 开始转换...\n');
44
- const result = CUXMLAPI.convertSync(options.input, outputPath);
47
+ const result = CUXMLAPI.convertSync(options.input, outputPath, {
48
+ overwrite: options.overwrite
49
+ });
45
50
 
46
51
  if (result.success) {
47
52
  console.log(`✅ ${result.message}`);
@@ -62,29 +67,15 @@ program
62
67
  .command('check')
63
68
  .description('检查 XML 文件是否符合 CustomUI 规范')
64
69
  .argument('<xmlFile>', '要检查的 XML 文件路径')
65
- .option('-v, --verbose', '显示详细信息')
66
- .action((xmlFile, options) => {
70
+ .action((xmlFile) => {
67
71
  try {
68
72
  console.log('🔍 开始检查...\n');
69
-
73
+
70
74
  const result = XMLChecker.checkWithSummary(xmlFile);
71
-
72
- if (options.verbose) {
73
- console.log(XMLChecker.formatSummary(result));
74
- } else {
75
- console.log(`总节点数: ${result.totalNodes}`);
76
- console.log(`通过: ${result.passed}`);
77
- console.log(`错误: ${result.errors}`);
78
- console.log(`警告: ${result.warnings}`);
79
-
80
- if (result.errors === 0 && result.warnings === 0) {
81
- console.log('\n✅ 检查通过!');
82
- } else if (result.errors === 0) {
83
- console.log('\n⚠️ 检查通过,但有警告');
84
- } else {
85
- console.log('\n❌ 检查失败,发现错误');
86
- process.exit(1);
87
- }
75
+ console.log(XMLChecker.formatSummary(result));
76
+
77
+ if (result.errors > 0) {
78
+ process.exit(1);
88
79
  }
89
80
  } catch (error) {
90
81
  console.error(`❌ 错误: ${error.message}`);
@@ -99,12 +90,14 @@ program
99
90
  .argument('<inputFile>', '输入的 XML 文件路径')
100
91
  .argument('[outputFile]', '输出的 JS 文件路径', './ribbon_temp.js')
101
92
  .option('-w, --watch', '监视文件变化并自动转换')
102
- .option('--overwrite', '覆盖输出文件')
93
+ .option('--overwrite', '覆盖输出文件(非 watch 模式)')
103
94
  .action((inputFile, outputFile, options) => {
104
95
  try {
96
+ const watchOptions = options.watch ? { overwrite: true } : { overwrite: options.overwrite };
97
+
105
98
  if (options.watch) {
106
99
  console.log('🚀 启动监视模式...\n');
107
- CUXMLAPI.watch(inputFile, outputFile, { overwrite: options.overwrite });
100
+ CUXMLAPI.watch(inputFile, outputFile, watchOptions);
108
101
  } else {
109
102
  console.log('🔄 开始转换...\n');
110
103
  const result = CUXMLAPI.convertSync(inputFile, outputFile, {
package/src/converter.js CHANGED
@@ -2,6 +2,7 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const lodash = require('lodash');
4
4
  const XMLParser = require('./parser');
5
+ const XMLChecker = require('./checker');
5
6
  const { generateCallbackTemplate, processFunctionName } = require('./templates');
6
7
 
7
8
  /**
@@ -40,6 +41,19 @@ class XMLConverter {
40
41
  throw new Error(`输入文件不存在: ${inputPath}`);
41
42
  }
42
43
 
44
+ // 先进行 XML 检查
45
+ const checkSummary = XMLChecker.checkWithSummary(inputPath);
46
+
47
+ // 显示检查结果
48
+ if (!options.silent) {
49
+ if (checkSummary.errors > 0) {
50
+ console.log(XMLChecker.formatSummary(checkSummary));
51
+ throw new Error('XML 检查失败,存在错误');
52
+ } else if (checkSummary.warnings > 0) {
53
+ console.log(XMLChecker.formatSummary(checkSummary));
54
+ }
55
+ }
56
+
43
57
  // 加载配置
44
58
  const clashAttributes = require('../lib/clashAttributes.json').crashs;
45
59
  const stDelegate = require('../lib/StDelegate.json');
package/src/templates.js CHANGED
@@ -12,18 +12,11 @@ function generateCallbackTemplate(functionName, controls = []) {
12
12
  // 如果控件数量为 0 或 1,生成简化版本的函数
13
13
  if (controls.length <= 1) {
14
14
  return `/**
15
- * @param {RibbonUI} control 控件对象,代表一个菜单控件元素对象(如 button、group 等)
16
- * @note
17
- * - 这是控件的回调函数,不应该在其他 JS 文件中调用这个函数
18
- * - 尽量立即完成,避免写长耗时代码。
15
+ * @param {RibbonUI} control 控件对象
19
16
  *
20
- * 生成时间: ${new Date().toLocaleString()}
17
+ * ${new Date().toLocaleString()}
21
18
  */
22
19
  function ${functionName}(control) {
23
-
24
- // code here
25
-
26
- // 避免 UI 报错,总是返回 true
27
20
  return true;
28
21
  }
29
22
  `;
@@ -31,16 +24,16 @@ function ${functionName}(control) {
31
24
 
32
25
  // 多个控件使用时,生成 switch case 结构
33
26
  const cases = controls.map(control => {
34
- return ` // xmlPath: ${control.xmlPath}\n case "${control.id}":\n // TODO: 实现控件逻辑\n break;`;
27
+ return ` // xmlPath: ${control.xmlPath}\n case "${control.id}":\n\n break;`;
35
28
  });
36
29
 
37
30
  return `/**
38
- * @param {RibbonUI} control 控件对象,代表一个菜单控件元素对象(如 button、group 等)
31
+ * @param {RibbonUI} control 控件对象
39
32
  * @note
40
- * - 这是控件的回调函数,不应该在其他 JS 文件中调用这个函数
33
+ * - 这是控件的回调函数
41
34
  * - 尽量立即完成,避免写长耗时代码。
42
35
  *
43
- * 生成时间: ${new Date().toLocaleString()}
36
+ * ${new Date().toLocaleString()}
44
37
  */
45
38
  function ${functionName}(control) {
46
39
 
package/test/Agents.md DELETED
@@ -1,76 +0,0 @@
1
- # CUXML Agent
2
-
3
- ## 1. 简介
4
-
5
- CUXML 是一个处理 RibbonUI XML 文件的工具。
6
-
7
- > [ribbon](https://learn.microsoft.com/en-us/openspecs/office_standards/ms-customui/574eeee8-7a03-406a-b95f-f9e51e53dd9d)
8
-
9
- ## CUXML 的功能包括:
10
-
11
- - 将 RibbonUI 的 XML 文件转换为 callback function。
12
- - 检查 RibbonUI 的 XML 文件是否合法:
13
- - 检查 XML 文件是否符合规范。
14
- - 检查 XML 文件是否有重复的 ID。
15
- - 检查 XML 文件是否有冲突的属性。比如:label 和 getLabel. 参考:[冲突的属性](../clashAttributes.json)
16
- - 检查 XML tag 的嵌套合规性。比如:button 不能嵌套在 button 中。
17
- - 等等,可补充
18
- - 监视 RibbonUI 文件的变化,并自动转换为 callback function 到指定文件。
19
-
20
- ### callback function 的处理
21
-
22
- - 将 XML 文件转换为 callback function 的代码,放在一个单独的文件中,比如:`ribbon_temp.js`。
23
- - 如果callback function 的名称带有"."连接符,则截取"."之后的字符串作为callback function 的名称。比如:`onClick.button1` 转换为 `button1`,最终得到 `function button1(ctrol){}`。
24
-
25
- ### 参考的源文件
26
-
27
- - xml: [xml](./ribbon.xml)
28
- - js: [js](./ribbon.js)
29
- - 转换模版: [template](./template.js)
30
-
31
- #### 哪些属性可以被转换为 callback function
32
-
33
- 可以参考 [常见属性的callback function](../StDelegate.json)
34
-
35
- ## 2. 使用方法
36
-
37
- ### 命令行
38
-
39
- ```bash
40
- cuxml -h
41
- ```
42
- > -h 显示帮助信息
43
-
44
- ```bash
45
- cuxml -i ./ribbon.xml -o ./ribbon.js [-w]
46
- ```
47
- > -i 指定输入文件,默认是当前目录下的 ribbon.xml。
48
- >
49
- > -o 指定输出文件,默认是当前目录下的 ribbon_temp.js。
50
- >
51
- > -w 监视文件的变化,并自动转换.
52
-
53
- ```bash
54
- cuxml -c ./ribbon.xml
55
- ```
56
- > -c ,check, 检查 XML 文件是否合法。
57
-
58
- ### API
59
-
60
- ```js
61
- const cuxml = require('cuxml');
62
- // convert
63
- cuxml.convert('./ribbon.xml', './ribbon.js');
64
- // check
65
- cuxml.check(xmlString|xmlFile);
66
- ```
67
-
68
- ### MCP
69
-
70
- 将其开发为一个 MCP 插件,可供其他工具调用。
71
-
72
- MCP 提供的 API 包括:
73
-
74
- - `convert(inputFile, outputFile)`: 将 XML 文件转换为 JavaScript 文件。
75
- - `check(xmlString|xmlFile)`: 检查 XML 字符串是否合法。
76
-
package/test/agent_new.md DELETED
@@ -1,286 +0,0 @@
1
- # CUXML Agent
2
-
3
- ## 📌 简介
4
-
5
- CUXML 是一个专为 WPS Office JS 插件开发者设计的工具,用于处理 RibbonUI XML 文件。它可以显著简化开发流程,让你更专注于业务逻辑而不是重复的代码编写。
6
-
7
- > 📖 参考文档:[Microsoft CustomUI 规范](https://learn.microsoft.com/en-us/openspecs/office_standards/ms-customui/574eeee8-7a03-406a-b95f-f9e51e53dd9d)
8
-
9
- ---
10
-
11
- ## ✨ 核心功能
12
-
13
- ### 1. 🔄 XML 到 JavaScript 的转换
14
-
15
- 将 RibbonUI XML 文件中的回调函数配置自动转换为 JavaScript 函数代码,大幅减少手工编码工作。
16
-
17
- **特性:**
18
- - 智能解析 XML 中的回调函数属性
19
- - 生成标准化的 JavaScript 函数模板
20
- - 支持自动函数名处理(带 `.` 连接符的属性名会被截取)
21
- - 支持监视文件变化,自动重新转换
22
-
23
- ### 2. 🔍 XML 合规性检查
24
-
25
- 全面检查 XML 文件是否符合 CustomUI 规范,提前发现潜在问题。
26
-
27
- **检查项目:**
28
- - ✅ XML 语法规范性
29
- - ✅ ID 唯一性检查(防止重复 ID)
30
- - ✅ 属性冲突检测(如 `label` 和 `getLabel` 不能同时存在)
31
- - ✅ 标签嵌套合规性(如 `button` 不能嵌套在 `button` 中)
32
- - ✅ 属性值类型验证(基于 Simple Types 规范)
33
- - ✅ 其他自定义规则验证
34
-
35
- ---
36
-
37
- ## 🚀 快速开始
38
-
39
- ### 安装
40
-
41
- ```bash
42
- npm install -g cuxml
43
- ```
44
-
45
- ### 命令行使用
46
-
47
- #### 查看帮助信息
48
-
49
- ```bash
50
- cuxml -h
51
- ```
52
-
53
- #### 转换 XML 文件
54
-
55
- ```bash
56
- # 基本用法
57
- cuxml -i ./ribbon.xml -o ./ribbon.js
58
-
59
- # 使用默认路径(当前目录下的 ribbon.xml -> ribbon_temp.js)
60
- cuxml
61
-
62
- # 监视模式(文件变化时自动转换)
63
- cuxml -i ./ribbon.xml -o ./ribbon.js -w
64
- ```
65
-
66
- **参数说明:**
67
- - `-i, --input <file>`: 指定输入的 XML 文件路径,默认为 `./ribbon.xml`
68
- - `-o, --output <file>`: 指定输出的 JS 文件路径,默认为 `./ribbon_temp.js`
69
- - `-w, --watch`: 启用监视模式,自动检测文件变化并重新转换
70
- - `-h, --help`: 显示帮助信息
71
-
72
- #### 检查 XML 文件
73
-
74
- ```bash
75
- cuxml -c ./ribbon.xml
76
- ```
77
-
78
- **参数说明:**
79
- - `-c, --check <file>`: 检查指定的 XML 文件是否符合规范
80
-
81
- ### API 使用
82
-
83
- ```javascript
84
- const cuxml = require('cuxml');
85
-
86
- // 转换 XML 到 JavaScript
87
- cuxml.convert('./ribbon.xml', './ribbon.js');
88
-
89
- // 检查 XML 文件(支持文件路径或 XML 字符串)
90
- cuxml.check('./ribbon.xml');
91
- // 或
92
- cuxml.check('<customUI xmlns="...">...</customUI>');
93
- ```
94
-
95
- ---
96
-
97
- ## 📦 MCP 插件
98
-
99
- CUXML 还可以作为一个 MCP (Model Context Protocol) 插件使用,方便集成到 AI 工具链中。
100
-
101
- ### 可用的 MCP API
102
-
103
- ```javascript
104
- // 转换 XML 文件为 JavaScript
105
- mcp.convert(inputFile, outputFile);
106
-
107
- // 检查 XML 文件或字符串是否合法
108
- mcp.check(xmlString | xmlFile);
109
- ```
110
-
111
- **使用场景:**
112
- - AI 辅助生成 RibbonUI XML 后自动转换
113
- - 自动化构建流程中的代码生成
114
- - CI/CD 流程中的 XML 验证
115
-
116
- ---
117
-
118
- ## 🔧 技术细节
119
-
120
- ### Callback Function 转换规则
121
-
122
- 1. **函数名处理**
123
- - 如果属性名包含 `.` 分隔符,自动提取最后一部分作为函数名
124
- - 例如:`onClick.button1` → `function button1(control) {}`
125
-
126
- 2. **函数签名**
127
- - 所有生成的回调函数都遵循统一的签名
128
- - 函数接收一个 `control` 参数,表示触发事件的控件对象
129
-
130
- 3. **输出文件**
131
- - 生成的代码会写入到指定的 JS 文件中
132
- - 默认输出为当前目录的 `ribbon_temp.js`
133
-
134
- ### 支持的回调函数属性
135
-
136
- 所有支持转换为 callback function 的属性都定义在配置文件中,详细信息请参考:
137
-
138
- 📄 **参考文件:** [StDelegate.json](../StDelegate.json)
139
-
140
- 常见的回调函数属性包括:
141
- - `onAction` - 按钮点击事件
142
- - `getLabel` - 动态获取标签文本
143
- - `getImage` - 动态获取图标
144
- - `getEnabled` - 动态获取启用状态
145
- - `getVisible` - 动态获取可见性
146
- - 等等...
147
-
148
- ### XML 检查规则
149
-
150
- #### 属性冲突检测
151
-
152
- 参考配置文件中的冲突属性定义:
153
-
154
- 📄 **参考文件:** [clashAttributes.json](../clashAttributes.json)
155
-
156
- **常见冲突示例:**
157
- - `label` vs `getLabel`
158
- - `image` vs `getImage`
159
- - `screentip` vs `getScreentip`
160
- - `supertip` vs `getSupertip`
161
- - `enabled` vs `getEnabled`
162
- - `visible` vs `getVisible`
163
-
164
- #### 标签嵌套规则
165
-
166
- 严格遵循 CustomUI 的标签嵌套规范:
167
- - `button` 不能包含其他按钮
168
- - `tab` 必须包含在 `ribbon` 或 `ribbon.tabs` 中
169
- - `group` 必须包含在 `tab` 中
170
- - 等等...
171
-
172
- #### Simple Types 支持
173
-
174
- 支持 http://schemas.microsoft.com/office/2006/01/customui 命名空间下的所有 Simple Types。
175
-
176
- 📄 **参考文件:** [simpleTypes.json](../simpleTypes.json)
177
-
178
- 支持版本:
179
- - CustomUI 2007
180
- - CustomUI 2010
181
- - 其他兼容版本
182
-
183
- ---
184
-
185
- ## 📁 示例文件
186
-
187
- 项目提供了完整的示例文件,供参考和学习:
188
-
189
- ### 示例文件列表
190
-
191
- - **XML 示例:** [ribbon.xml](./ribbon.xml) - 完整的 RibbonUI XML 配置示例
192
- - **JS 输出示例:** [ribbon.js](./ribbon.js) - 转换后的 JavaScript 函数代码
193
- - **转换模板:** [template.js](./template.js) - 回调函数生成模板
194
-
195
- ### 快速体验
196
-
197
- 1. 查看 `ribbon.xml` 了解 RibbonUI 的 XML 结构
198
- 2. 运行 `cuxml -i ./test/ribbon.xml -o ./output.js` 进行转换
199
- 3. 对比 `ribbon.js` 和生成的 `output.js` 理解转换规则
200
- 4. 运行 `cuxml -c ./test/ribbon.xml` 进行 XML 检查
201
-
202
- ---
203
-
204
- ## 🎯 最佳实践
205
-
206
- ### 1. 文件组织
207
-
208
- ```
209
- project/
210
- ├── src/
211
- │ └── ribbon.xml # RibbonUI 配置文件
212
- ├── gen/
213
- │ └── ribbon_callbacks.js # 自动生成的回调函数
214
- ├── main.js # 主逻辑文件
215
- └── package.json
216
- ```
217
-
218
- ### 2. 开发工作流
219
-
220
- ```bash
221
- # 开发阶段:使用监视模式,实时更新
222
- cuxml -i src/ribbon.xml -o gen/ribbon_callbacks.js -w
223
-
224
- # 构建阶段:生成并验证
225
- cuxml -c src/ribbon.xml && cuxml -i src/ribbon.xml -o gen/ribbon_callbacks.js
226
- ```
227
-
228
- ### 3. 错误处理
229
-
230
- ```javascript
231
- const cuxml = require('cuxml');
232
-
233
- try {
234
- cuxml.check('./ribbon.xml');
235
- cuxml.convert('./ribbon.xml', './ribbon.js');
236
- console.log('✅ 转换成功!');
237
- } catch (error) {
238
- console.error('❌ 转换失败:', error.message);
239
- // 处理错误
240
- }
241
- ```
242
-
243
- ### 4. 自定义扩展
244
-
245
- 虽然 CUXML 提供了自动生成的模板,但复杂的业务逻辑仍需手动编写:
246
-
247
- ```javascript
248
- // 自动生成的基础模板
249
- function button1(control) {
250
- // TODO: 实现按钮逻辑
251
- }
252
-
253
- // 手动添加的复杂逻辑
254
- function button1(control) {
255
- const app = control.context;
256
- const selection = app.Selection;
257
-
258
- // 复杂的业务逻辑
259
- if (selection && selection.Type === 1) { // 文本选中
260
- // 处理文本...
261
- }
262
- }
263
- ```
264
-
265
- ---
266
-
267
- ## 🔗 相关资源
268
-
269
- - [Microsoft CustomUI 规范](https://learn.microsoft.com/en-us/openspecs/office_standards/ms-customui/574eeee8-7a03-406a-b95f-f9e51e53dd9d)
270
- - [Simple Types 参考](https://learn.microsoft.com/en-us/openspecs/office_standards/ms-customui/869c8c9a-45f8-4119-b068-f61e76d04322)
271
- - [WPS Office 开发文档](https://wps.cn/developer/)
272
-
273
- ---
274
-
275
- ## 🤝 贡献与反馈
276
-
277
- 如果您在使用过程中遇到问题或有改进建议,欢迎:
278
-
279
- - 提交 [Issue](https://github.com/YYago/cuxml/issues)
280
- - 发起 [Pull Request](https://github.com/YYago/cuxml/pulls)
281
-
282
- ---
283
-
284
- ## 📄 许可证
285
-
286
- ISC
package/test/ribbon.js DELETED
@@ -1,174 +0,0 @@
1
- import Util from './js/util.js'
2
- import SystemDemo from './js/systemdemo.js'
3
-
4
- //这个函数在整个wps加载项中是第一个执行的
5
- function OnAddinLoad(ribbonUI) {
6
- if (typeof window.Application.ribbonUI != 'object') {
7
- window.Application.ribbonUI = ribbonUI
8
- }
9
-
10
- if (typeof window.Application.Enum != 'object') {
11
- // 如果没有内置枚举值
12
- window.Application.Enum = Util.WPS_Enum
13
- }
14
-
15
- //这几个导出函数是给外部业务系统调用的
16
- window.openOfficeFileFromSystemDemo = SystemDemo.openOfficeFileFromSystemDemo
17
- window.InvokeFromSystemDemo = SystemDemo.InvokeFromSystemDemo
18
-
19
- window.Application.PluginStorage.setItem('EnableFlag', false) //往PluginStorage中设置一个标记,用于控制两个按钮的置灰
20
- window.Application.PluginStorage.setItem('ApiEventFlag', false) //往PluginStorage中设置一个标记,用于控制ApiEvent的按钮label
21
- return true
22
- }
23
-
24
- var WebNotifycount = 0
25
- function OnAction(control) {
26
- const eleId = control.Id
27
- switch (eleId) {
28
- case 'btnShowMsg':
29
- {
30
- const doc = window.Application.ActiveDocument
31
- if (!doc) {
32
- alert('当前没有打开任何文档')
33
- return
34
- }
35
- alert(doc.Name)
36
- }
37
- break
38
- case 'btnIsEnbable': {
39
- let bFlag = window.Application.PluginStorage.getItem('EnableFlag')
40
- window.Application.PluginStorage.setItem('EnableFlag', !bFlag)
41
-
42
- //通知wps刷新以下几个按饰的状态
43
- window.Application.ribbonUI.InvalidateControl('btnIsEnbable')
44
- window.Application.ribbonUI.InvalidateControl('btnShowDialog')
45
- window.Application.ribbonUI.InvalidateControl('btnShowTaskPane')
46
- //window.Application.ribbonUI.Invalidate(); 这行代码打开则是刷新所有的按钮状态
47
- break
48
- }
49
- case 'btnShowDialog': {
50
- window.Application.ShowDialog(
51
- Util.GetUrlPath() + Util.GetRouterHash() + '/dialog',
52
- '这是一个对话框网页',
53
- 400 * window.devicePixelRatio,
54
- 400 * window.devicePixelRatio,
55
- false
56
- )
57
- break
58
- }
59
- case 'btnShowTaskPane':
60
- {
61
- let tsId = window.Application.PluginStorage.getItem('taskpane_id')
62
- if (!tsId) {
63
- let tskpane = window.Application.CreateTaskPane(Util.GetUrlPath() + Util.GetRouterHash() + '/taskpane')
64
- let id = tskpane.ID
65
- window.Application.PluginStorage.setItem('taskpane_id', id)
66
- tskpane.Visible = true
67
- } else {
68
- let tskpane = window.Application.GetTaskPane(tsId)
69
- tskpane.Visible = !tskpane.Visible
70
- }
71
- }
72
- break
73
- case 'btnApiEvent':
74
- {
75
- let bFlag = window.Application.PluginStorage.getItem('ApiEventFlag')
76
- let bRegister = bFlag ? false : true
77
- window.Application.PluginStorage.setItem('ApiEventFlag', bRegister)
78
- if (bRegister) {
79
- window.Application.ApiEvent.AddApiEventListener('DocumentNew', 'ribbon.OnNewDocumentApiEvent')
80
- } else {
81
- window.Application.ApiEvent.RemoveApiEventListener('DocumentNew', 'ribbon.OnNewDocumentApiEvent')
82
- }
83
-
84
- window.Application.ribbonUI.InvalidateControl('btnApiEvent')
85
- }
86
- break
87
- case 'btnWebNotify':
88
- {
89
- let currentTime = new Date()
90
- let timeStr =
91
- currentTime.getHours() + ':' + currentTime.getMinutes() + ':' + currentTime.getSeconds()
92
- window.Application.OAAssist.WebNotify(
93
- '这行内容由wps加载项主动送达给业务系统,可以任意自定义, 比如时间值:' +
94
- timeStr +
95
- ',次数:' +
96
- ++WebNotifycount,
97
- true
98
- )
99
- }
100
- break
101
- default:
102
- break
103
- }
104
- return true
105
- }
106
-
107
- function GetImage(control) {
108
- const eleId = control.Id
109
- switch (eleId) {
110
- case 'btnShowMsg':
111
- return 'images/1.svg'
112
- case 'btnShowDialog':
113
- return 'images/2.svg'
114
- case 'btnShowTaskPane':
115
- return 'images/3.svg'
116
- default:
117
- }
118
- return 'images/newFromTemp.svg'
119
- }
120
-
121
- function OnGetEnabled(control) {
122
- const eleId = control.Id
123
- switch (eleId) {
124
- case 'btnShowMsg':
125
- return true
126
- case 'btnShowDialog': {
127
- let bFlag = window.Application.PluginStorage.getItem('EnableFlag')
128
- return bFlag
129
- }
130
- case 'btnShowTaskPane': {
131
- let bFlag = window.Application.PluginStorage.getItem('EnableFlag')
132
- return bFlag
133
- }
134
- default:
135
- break
136
- }
137
- return true
138
- }
139
-
140
- function OnGetVisible(control) {
141
- const eleId = control.Id
142
- console.log(eleId)
143
- return true
144
- }
145
-
146
- function OnGetLabel(control) {
147
- const eleId = control.Id
148
- switch (eleId) {
149
- case 'btnIsEnbable': {
150
- let bFlag = window.Application.PluginStorage.getItem('EnableFlag')
151
- return bFlag ? '按钮Disable' : '按钮Enable'
152
- }
153
- case 'btnApiEvent': {
154
- let bFlag = window.Application.PluginStorage.getItem('ApiEventFlag')
155
- return bFlag ? '清除新建文件事件' : '注册新建文件事件'
156
- }
157
- }
158
- return ''
159
- }
160
-
161
- function OnNewDocumentApiEvent(doc) {
162
- alert('新建文件事件响应,取文件名: ' + doc.Name)
163
- }
164
-
165
- //这些函数是给wps客户端调用的
166
- export default {
167
- OnAddinLoad,
168
- OnAction,
169
- GetImage,
170
- OnGetEnabled,
171
- OnGetVisible,
172
- OnGetLabel,
173
- OnNewDocumentApiEvent
174
- }
package/test/ribbon.xml DELETED
@@ -1,16 +0,0 @@
1
- <customUI xmlns="http://schemas.microsoft.com/office/2006/01/customui" onLoad="ribbon.OnAddinLoad">
2
- <ribbon startFromScratch="false">
3
- <tabs>
4
- <tab id="wpsAddinTab" label="wps加载项示例">
5
- <group id="btnDemoGroup" label="group1">
6
- <button id="btnShowMsg" label="弹出消息框" onAction="ribbon.OnAction" getEnabled="ribbon.OnGetEnabled" getImage="ribbon.GetImage" visible="true" size="large"/>
7
- <button id="btnIsEnbable" getLabel="ribbon.OnGetLabel" onAction="ribbon.OnAction" enabled="true" getImage="ribbon.GetImage" visible="true" size="large"/>
8
- <button id="btnShowDialog" label="弹对话框网页" onAction="ribbon.OnAction" getEnabled="ribbon.OnGetEnabled" getImage="ribbon.GetImage" getVisible="ribbon.OnGetVisible" size="large"/>
9
- <button id="btnShowTaskPane" label="弹任务窗格网页" onAction="ribbon.OnAction" getEnabled="ribbon.OnGetEnabled" getImage="ribbon.GetImage" getVisible="ribbon.OnGetVisible" size="large"/>
10
- <button id="btnApiEvent" getLabel="ribbon.OnGetLabel" onAction="ribbon.OnAction" getEnabled="ribbon.OnGetEnabled" getImage="ribbon.GetImage" getVisible="ribbon.OnGetVisible" size="large"/>
11
- <button id="btnWebNotify" label="给业务系统发通知" onAction="ribbon.OnAction" enabled="true" getImage="ribbon.GetImage" getVisible="ribbon.OnGetVisible" size="large"/>
12
- </group>
13
- </tab>
14
- </tabs>
15
- </ribbon>
16
- </customUI>
package/test/template.js DELETED
File without changes
package/test/test.js DELETED
@@ -1,325 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const cuxml = require('../index.js');
6
-
7
- // 测试结果目录
8
- const RESULT_DIR = path.join(__dirname, 'result');
9
-
10
- // 确保结果目录存在
11
- if (!fs.existsSync(RESULT_DIR)) {
12
- fs.mkdirSync(RESULT_DIR, { recursive: true });
13
- }
14
-
15
- // 测试结果收集
16
- const testResults = [];
17
-
18
- /**
19
- * 记录测试结果
20
- */
21
- function logTest(name, success, message, data = null) {
22
- const result = {
23
- name,
24
- success,
25
- message,
26
- data,
27
- timestamp: new Date().toISOString()
28
- };
29
- testResults.push(result);
30
-
31
- const icon = success ? '✅' : '❌';
32
- console.log(`${icon} ${name}: ${message}`);
33
- if (data) {
34
- console.log(` ${JSON.stringify(data)}`);
35
- }
36
- }
37
-
38
- /**
39
- * 清理测试文件
40
- */
41
- function cleanup() {
42
- const files = [
43
- path.join(RESULT_DIR, 'output1.js'),
44
- path.join(RESULT_DIR, 'output2.js'),
45
- path.join(RESULT_DIR, 'ribbon_temp.js')
46
- ];
47
- files.forEach(file => {
48
- if (fs.existsSync(file)) {
49
- fs.unlinkSync(file);
50
- }
51
- });
52
- }
53
-
54
- /**
55
- * 测试 1: 检查 XML 文件(同步)
56
- */
57
- async function test1_checkXMLSync() {
58
- try {
59
- const result = cuxml.checkSync(path.join(__dirname, 'ribbon.xml'));
60
- logTest(
61
- 'XML 检查(同步)',
62
- result.success,
63
- `检查完成: ${result.passed}/${result.totalNodes} 通过`,
64
- { errors: result.errors, warnings: result.warnings }
65
- );
66
- return result.success;
67
- } catch (error) {
68
- logTest('XML 检查(同步)', false, error.message);
69
- return false;
70
- }
71
- }
72
-
73
- /**
74
- * 测试 2: 检查 XML 文件(异步)
75
- */
76
- async function test2_checkXMLAsync() {
77
- try {
78
- const result = await cuxml.check(path.join(__dirname, 'ribbon.xml'));
79
- logTest(
80
- 'XML 检查(异步)',
81
- result.success,
82
- `检查完成: ${result.passed}/${result.totalNodes} 通过`,
83
- { errors: result.errors, warnings: result.warnings }
84
- );
85
- return result.success;
86
- } catch (error) {
87
- logTest('XML 检查(异步)', false, error.message);
88
- return false;
89
- }
90
- }
91
-
92
- /**
93
- * 测试 3: 转换 XML 到 JavaScript(同步)
94
- */
95
- async function test3_convertSync() {
96
- try {
97
- const inputFile = path.join(__dirname, 'ribbon.xml');
98
- const outputFile = path.join(RESULT_DIR, 'output1.js');
99
-
100
- const result = cuxml.convertSync(inputFile, outputFile);
101
-
102
- const fileExists = fs.existsSync(outputFile);
103
- logTest(
104
- 'XML 转换(同步)',
105
- result.success && fileExists,
106
- `生成 ${result.functionsGenerated} 个回调函数`,
107
- { outputPath: result.outputPath, fileExists }
108
- );
109
-
110
- return result.success && fileExists;
111
- } catch (error) {
112
- logTest('XML 转换(同步)', false, error.message);
113
- return false;
114
- }
115
- }
116
-
117
- /**
118
- * 测试 4: 转换 XML 到 JavaScript(异步)
119
- */
120
- async function test4_convertAsync() {
121
- try {
122
- const inputFile = path.join(__dirname, 'ribbon.xml');
123
- const outputFile = path.join(RESULT_DIR, 'output2.js');
124
-
125
- const result = await cuxml.convert(inputFile, outputFile);
126
-
127
- const fileExists = fs.existsSync(outputFile);
128
- logTest(
129
- 'XML 转换(异步)',
130
- result.success && fileExists,
131
- `生成 ${result.functionsGenerated} 个回调函数`,
132
- { outputPath: result.outputPath, fileExists }
133
- );
134
-
135
- return result.success && fileExists;
136
- } catch (error) {
137
- logTest('XML 转换(异步)', false, error.message);
138
- return false;
139
- }
140
- }
141
-
142
- /**
143
- * 测试 5: 检查 XML 字符串
144
- */
145
- async function test5_checkXMLString() {
146
- try {
147
- const xmlString = fs.readFileSync(path.join(__dirname, 'ribbon.xml'), 'utf-8');
148
- const result = cuxml.checkSync(xmlString);
149
-
150
- logTest(
151
- 'XML 字符串检查',
152
- result.success,
153
- `检查完成: ${result.passed}/${result.totalNodes} 通过`,
154
- { errors: result.errors, warnings: result.warnings }
155
- );
156
-
157
- return result.success;
158
- } catch (error) {
159
- logTest('XML 字符串检查', false, error.message);
160
- return false;
161
- }
162
- }
163
-
164
- /**
165
- * 测试 6: 格式化检查结果
166
- */
167
- async function test6_formatCheckResult() {
168
- try {
169
- const inputFile = path.join(__dirname, 'ribbon.xml');
170
- const result = cuxml.checkSync(inputFile, { detailed: true });
171
- const formatted = cuxml.formatCheckResult(result);
172
-
173
- const hasFormat = typeof formatted === 'string' && formatted.length > 0;
174
- logTest(
175
- '格式化检查结果',
176
- hasFormat,
177
- `格式化输出长度: ${formatted.length} 字符`,
178
- { outputLength: formatted.length }
179
- );
180
-
181
- return hasFormat;
182
- } catch (error) {
183
- logTest('格式化检查结果', false, error.message);
184
- return false;
185
- }
186
- }
187
-
188
- /**
189
- * 测试 7: 输出文件内容验证
190
- */
191
- async function test7_verifyOutputContent() {
192
- try {
193
- const outputFile = path.join(RESULT_DIR, 'output1.js');
194
-
195
- if (!fs.existsSync(outputFile)) {
196
- logTest('输出文件内容验证', false, '输出文件不存在');
197
- return false;
198
- }
199
-
200
- const content = fs.readFileSync(outputFile, 'utf-8');
201
-
202
- // 检查是否包含预期的函数
203
- const expectedFunctions = ['OnAddinLoad', 'OnGetLabel', 'OnGetEnabled', 'OnGetVisible'];
204
- const foundFunctions = expectedFunctions.filter(fn => content.includes(`function ${fn}`));
205
-
206
- const allFound = foundFunctions.length === expectedFunctions.length;
207
- logTest(
208
- '输出文件内容验证',
209
- allFound,
210
- `找到 ${foundFunctions.length}/${expectedFunctions.length} 个预期函数`,
211
- { foundFunctions, expectedFunctions }
212
- );
213
-
214
- return allFound;
215
- } catch (error) {
216
- logTest('输出文件内容验证', false, error.message);
217
- return false;
218
- }
219
- }
220
-
221
- /**
222
- * 保存测试结果
223
- */
224
- function saveResults() {
225
- const resultFile = path.join(RESULT_DIR, 'test_results.json');
226
- const successCount = testResults.filter(r => r.success).length;
227
- const totalCount = testResults.length;
228
-
229
- const summary = {
230
- totalTests: totalCount,
231
- successTests: successCount,
232
- failedTests: totalCount - successCount,
233
- successRate: `${((successCount / totalCount) * 100).toFixed(2)}%`,
234
- timestamp: new Date().toISOString(),
235
- tests: testResults
236
- };
237
-
238
- fs.writeFileSync(resultFile, JSON.stringify(summary, null, 2));
239
-
240
- // 保存 Markdown 格式的报告
241
- const mdReport = generateMarkdownReport(summary);
242
- const mdFile = path.join(RESULT_DIR, 'test_report.md');
243
- fs.writeFileSync(mdFile, mdReport);
244
-
245
- console.log('\n📊 测试报告已保存:');
246
- console.log(` JSON: ${resultFile}`);
247
- console.log(` Markdown: ${mdFile}`);
248
- }
249
-
250
- /**
251
- * 生成 Markdown 报告
252
- */
253
- function generateMarkdownReport(summary) {
254
- return `# CUXML v2.0.0 测试报告
255
-
256
- **测试时间**: ${summary.timestamp}
257
- **测试通过率**: ${summary.successRate} (${summary.successTests}/${summary.totalTests})
258
-
259
- ## 测试结果概览
260
-
261
- | 测试名称 | 状态 | 消息 |
262
- |---------|------|------|
263
- ${summary.tests.map(test =>
264
- `| ${test.name} | ${test.success ? '✅ 通过' : '❌ 失败'} | ${test.message} |`
265
- ).join('\n')}
266
-
267
- ## 详细测试结果
268
-
269
- ${summary.tests.map(test => `
270
- ### ${test.name}
271
-
272
- - **状态**: ${test.success ? '✅ 通过' : '❌ 失败'}
273
- - **消息**: ${test.message}
274
- - **时间**: ${test.timestamp}
275
- ${test.data ? `- **数据**: \`${JSON.stringify(test.data)}\`` : ''}
276
- `).join('\n')}
277
-
278
- ---
279
-
280
- ## 统计信息
281
-
282
- - 总测试数: ${summary.totalTests}
283
- - 成功: ${summary.successTests}
284
- - 失败: ${summary.failedTests}
285
- - 成功率: ${summary.successRate}
286
- `;
287
- }
288
-
289
- /**
290
- * 运行所有测试
291
- */
292
- async function runAllTests() {
293
- console.log('🧪 开始 CUXML v2.0.0 功能测试\n');
294
- console.log('='.repeat(60));
295
-
296
- // 清理之前的测试文件
297
- cleanup();
298
-
299
- // 运行测试
300
- const tests = [
301
- test1_checkXMLSync,
302
- test2_checkXMLAsync,
303
- test3_convertSync,
304
- test4_convertAsync,
305
- test5_checkXMLString,
306
- test6_formatCheckResult,
307
- test7_verifyOutputContent
308
- ];
309
-
310
- for (const test of tests) {
311
- await test();
312
- }
313
-
314
- console.log('\n' + '='.repeat(60));
315
- console.log(`✅ 测试完成: ${testResults.filter(r => r.success).length}/${testResults.length} 通过`);
316
-
317
- // 保存测试结果
318
- saveResults();
319
- }
320
-
321
- // 运行测试
322
- runAllTests().catch(error => {
323
- console.error('❌ 测试运行失败:', error);
324
- process.exit(1);
325
- });