@szc-ft/mcp-szcd-client 0.28.1 → 0.29.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.
Files changed (36) hide show
  1. package/agents/szcd-component-expert.qoder.md +1 -0
  2. package/opencode-extension/skills/local-browser-test/SKILL.md +189 -53
  3. package/opencode-extension/skills/local-browser-test/lib/ant-adapter.js +735 -0
  4. package/opencode-extension/skills/local-browser-test/lib/browser-engine.js +2427 -0
  5. package/opencode-extension/skills/local-browser-test/lib/chrome-finder.js +125 -0
  6. package/opencode-extension/skills/local-browser-test/lib/expect.js +392 -0
  7. package/opencode-extension/skills/local-browser-test/lib/shared-deps.js +137 -0
  8. package/opencode-extension/skills/local-browser-test/lib/test-plan.js +48 -0
  9. package/opencode-extension/skills/local-browser-test/lib/visual-compare.js +192 -0
  10. package/opencode-extension/skills/local-browser-test/local-browser-executor.js +735 -0
  11. package/opencode-extension/skills/local-browser-test/package.json +1 -0
  12. package/opencode-extension/skills/szcd-upload-zip/SKILL.md +122 -0
  13. package/package.json +1 -1
  14. package/qwen-extension/qwen-extension.json +1 -1
  15. package/qwen-extension/skills/local-browser-test/SKILL.md +189 -53
  16. package/qwen-extension/skills/local-browser-test/lib/ant-adapter.js +735 -0
  17. package/qwen-extension/skills/local-browser-test/lib/browser-engine.js +2427 -0
  18. package/qwen-extension/skills/local-browser-test/lib/chrome-finder.js +125 -0
  19. package/qwen-extension/skills/local-browser-test/lib/expect.js +392 -0
  20. package/qwen-extension/skills/local-browser-test/lib/shared-deps.js +137 -0
  21. package/qwen-extension/skills/local-browser-test/lib/test-plan.js +48 -0
  22. package/qwen-extension/skills/local-browser-test/lib/visual-compare.js +192 -0
  23. package/qwen-extension/skills/local-browser-test/local-browser-executor.js +735 -0
  24. package/qwen-extension/skills/local-browser-test/package.json +1 -0
  25. package/qwen-extension/skills/szcd-upload-zip/SKILL.md +122 -0
  26. package/standard-skill/local-browser-test/SKILL.md +189 -53
  27. package/standard-skill/local-browser-test/lib/ant-adapter.js +735 -0
  28. package/standard-skill/local-browser-test/lib/browser-engine.js +2427 -0
  29. package/standard-skill/local-browser-test/lib/chrome-finder.js +125 -0
  30. package/standard-skill/local-browser-test/lib/expect.js +392 -0
  31. package/standard-skill/local-browser-test/lib/shared-deps.js +137 -0
  32. package/standard-skill/local-browser-test/lib/test-plan.js +48 -0
  33. package/standard-skill/local-browser-test/lib/visual-compare.js +192 -0
  34. package/standard-skill/local-browser-test/local-browser-executor.js +735 -0
  35. package/standard-skill/local-browser-test/package.json +1 -0
  36. package/standard-skill/szcd-upload-zip/SKILL.md +122 -0
@@ -0,0 +1,48 @@
1
+ /**
2
+ * plan → testCase 兼容编译器
3
+ *
4
+ * 把现有线性 plan JSON 自动升级为 testCase 对象,无需用户迁移。
5
+ * 规则:
6
+ * - plan.steps[] → testCase.steps
7
+ * - plan.dataset 存在 → 参数化执行
8
+ * - 新 step type(antSelect/antInput/expect/formFill 等)原样保留
9
+ * - 旧 step type(click/type/checkElement 等)也原样保留
10
+ */
11
+
12
+ /**
13
+ * 编译 plan JSON 为 testCase 对象
14
+ * @param {object} plan - 现有 plan 格式
15
+ * @returns {object} testCase 对象
16
+ */
17
+ export function compilePlan(plan) {
18
+ return {
19
+ title: plan.description || plan.planId || `auto-${Date.now()}`,
20
+ labels: plan.labels || [],
21
+ setup: {
22
+ findPage: plan.findPage || undefined,
23
+ findFrame: plan.findFrame || undefined,
24
+ apiCapture: plan.apiCapture || undefined,
25
+ },
26
+ dataset: plan.dataset || [],
27
+ steps: plan.steps || [],
28
+ recovery: plan.recovery || {},
29
+ teardown: plan.teardown || undefined,
30
+ // 透传元数据,报告使用
31
+ _planId: plan.planId,
32
+ _options: plan.options,
33
+ };
34
+ }
35
+
36
+ /**
37
+ * 编译 plan 文件内容为 testCase 对象数组
38
+ * @param {object|object[]} fileContent - plan JSON 或 plan 数组
39
+ * @returns {object[]} testCase 数组
40
+ */
41
+ export function compilePlanFile(fileContent) {
42
+ if (Array.isArray(fileContent)) {
43
+ return fileContent.map(compilePlan);
44
+ }
45
+ return [compilePlan(fileContent)];
46
+ }
47
+
48
+ export default { compilePlan, compilePlanFile };
@@ -0,0 +1,192 @@
1
+ /**
2
+ * 视觉对比引擎 - pixelmatch + sharp
3
+ *
4
+ * 功能:
5
+ * - 全局像素对比,输出还原度评分
6
+ * - 分区域对比(按 CSS selector 裁剪后分别评分)
7
+ * - 三色 diff 图输出(红=真实差异,黄=抗锯齿,蓝=轻微色差)
8
+ * - 自动 resize 设计稿到截图尺寸
9
+ *
10
+ * 依赖从共享缓存加载(~/.szcd-mcp/deps/):
11
+ * - sharp: 图片处理和 resize
12
+ * - pixelmatch: 像素对比
13
+ * - pngjs: PNG 编解码
14
+ */
15
+
16
+ import fs from "node:fs";
17
+ import path from "node:path";
18
+ import { ensureVisualCompareDeps, loadFromShared } from "./shared-deps.js";
19
+
20
+ /**
21
+ * 全局视觉对比
22
+ * @param {string} screenshotPath - 截图文件路径
23
+ * @param {string} designImagePath - 设计稿图片路径
24
+ * @param {object} options
25
+ * @param {number} [options.threshold=0.1] - 颜色差异阈值 (0-1)
26
+ * @param {string} [options.outputDir] - diff 图输出目录
27
+ * @returns {Promise<object>} 对比结果
28
+ */
29
+ export async function compareDesign(screenshotPath, designImagePath, options = {}) {
30
+ // 视觉对比才安装 pixelmatch/pngjs/sharp,避免 observe 主链路被重依赖阻塞
31
+ await ensureVisualCompareDeps();
32
+ const sharp = loadFromShared("sharp");
33
+ const pixelmatch = loadFromShared("pixelmatch");
34
+ const { PNG } = loadFromShared("pngjs");
35
+
36
+ // 1. 读取图片元数据
37
+ const screenshotMeta = await sharp(screenshotPath).metadata();
38
+ const screenshotBuf = await sharp(screenshotPath).png().toBuffer();
39
+ const designBuf = await sharp(designImagePath).png().toBuffer();
40
+
41
+ // 2. 以截图尺寸为基准 resize 设计稿
42
+ const { width, height } = screenshotMeta;
43
+ const designResized = await sharp(designBuf)
44
+ .resize(width, height, { fit: "fill" })
45
+ .png()
46
+ .toBuffer();
47
+
48
+ // 3. 解码为像素数据
49
+ const img1 = PNG.sync.read(screenshotBuf);
50
+ const img2 = PNG.sync.read(designResized);
51
+ const diff = new PNG({ width, height });
52
+
53
+ // 4. pixelmatch 对比
54
+ const threshold = options.threshold ?? 0.1;
55
+ const diffPixels = pixelmatch(img1.data, img2.data, diff.data, width, height, {
56
+ threshold,
57
+ includeAA: false,
58
+ alpha: 0.1,
59
+ aaColor: [255, 255, 0], // 黄色 = 抗锯齿差异
60
+ diffColor: [255, 0, 0], // 红色 = 真实差异
61
+ diffColorAlt: [0, 100, 255], // 蓝色 = 轻微色差(低于阈值)
62
+ });
63
+
64
+ const totalPixels = width * height;
65
+ const fidelity = parseFloat(((totalPixels - diffPixels) / totalPixels * 100).toFixed(2));
66
+
67
+ // 5. 保存 diff 图
68
+ let diffImagePath = null;
69
+ if (options.outputDir) {
70
+ if (!fs.existsSync(options.outputDir)) {
71
+ fs.mkdirSync(options.outputDir, { recursive: true });
72
+ }
73
+ diffImagePath = path.join(options.outputDir, `diff-global-${Date.now()}.png`);
74
+ fs.writeFileSync(diffImagePath, PNG.sync.write(diff));
75
+ }
76
+
77
+ // 6. 区域对比(如果有)
78
+ const regions = [];
79
+ if (options.regions?.length && options.page) {
80
+ for (const region of options.regions) {
81
+ try {
82
+ const regionResult = await compareRegion(
83
+ screenshotPath, designImagePath, region, { ...options, sharp, PNG, pixelmatch }
84
+ );
85
+ regions.push(regionResult);
86
+ } catch (err) {
87
+ regions.push({
88
+ name: region.name,
89
+ selector: region.selector,
90
+ error: err.message,
91
+ fidelity: null,
92
+ });
93
+ }
94
+ }
95
+ }
96
+
97
+ return {
98
+ fidelity,
99
+ diffPixels,
100
+ totalPixels,
101
+ width,
102
+ height,
103
+ diffImagePath,
104
+ regions,
105
+ };
106
+ }
107
+
108
+ /**
109
+ * 区域对比 - 通过 Puppeteer 获取元素位置后裁剪对比
110
+ * @param {string} screenshotPath
111
+ * @param {string} designImagePath
112
+ * @param {object} region - { name, selector }
113
+ * @param {object} deps - { sharp, PNG, pixelmatch, page, outputDir, threshold }
114
+ * @returns {Promise<object>}
115
+ */
116
+ async function compareRegion(screenshotPath, designImagePath, region, deps) {
117
+ const { sharp, PNG, pixelmatch, page, outputDir } = deps;
118
+ const threshold = deps.threshold ?? 0.1;
119
+
120
+ // 获取元素边界框
121
+ const boundingBox = await page.evaluate((sel) => {
122
+ const el = document.querySelector(sel);
123
+ if (!el) return null;
124
+ const rect = el.getBoundingClientRect();
125
+ return {
126
+ x: Math.round(rect.x),
127
+ y: Math.round(rect.y),
128
+ width: Math.round(rect.width),
129
+ height: Math.round(rect.height),
130
+ };
131
+ }, region.selector);
132
+
133
+ if (!boundingBox) {
134
+ return {
135
+ name: region.name,
136
+ selector: region.selector,
137
+ error: `Element not found: ${region.selector}`,
138
+ fidelity: null,
139
+ };
140
+ }
141
+
142
+ // 用 sharp 裁剪两张图的对应区域
143
+ const screenshotRegion = await sharp(screenshotPath)
144
+ .extract(boundingBox)
145
+ .png()
146
+ .toBuffer();
147
+
148
+ const designRegion = await sharp(designImagePath)
149
+ .resize(boundingBox.width, boundingBox.height, { fit: "fill" })
150
+ .extract({
151
+ left: 0,
152
+ top: 0,
153
+ width: boundingBox.width,
154
+ height: boundingBox.height,
155
+ })
156
+ .png()
157
+ .toBuffer();
158
+
159
+ // 像素对比
160
+ const img1 = PNG.sync.read(screenshotRegion);
161
+ const img2 = PNG.sync.read(designRegion);
162
+ const diff = new PNG({ width: img1.width, height: img1.height });
163
+
164
+ const diffPixels = pixelmatch(img1.data, img2.data, diff.data, img1.width, img1.height, {
165
+ threshold,
166
+ includeAA: false,
167
+ alpha: 0.1,
168
+ });
169
+
170
+ const totalPixels = img1.width * img1.height;
171
+ const fidelity = parseFloat(((totalPixels - diffPixels) / totalPixels * 100).toFixed(2));
172
+
173
+ // 保存区域 diff 图
174
+ let diffImagePath = null;
175
+ if (outputDir) {
176
+ const safeName = region.name.replace(/[^a-zA-Z0-9\u4e00-\u9fff]/g, "_");
177
+ diffImagePath = path.join(outputDir, `diff-region-${safeName}-${Date.now()}.png`);
178
+ fs.writeFileSync(diffImagePath, PNG.sync.write(diff));
179
+ }
180
+
181
+ return {
182
+ name: region.name,
183
+ selector: region.selector,
184
+ boundingBox,
185
+ fidelity,
186
+ diffPixels,
187
+ totalPixels,
188
+ diffImagePath,
189
+ };
190
+ }
191
+
192
+ export default { compareDesign };