mta-mcp 3.15.6 → 3.16.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.
@@ -36,11 +36,25 @@
36
36
  5. 只有当 compact 无法解释局部布局/样式时,再补用 measure/style
37
37
  ```
38
38
 
39
+ ### 图标资产强制规则(v4.8.0 更新)
40
+
41
+ - 默认流程改为:**先在 `assets/icons/` 查找用户手工导出的图标 PNG**,文件名优先匹配设计稿图层名对应的 `*_icon@4x.png`,找到即直接复用。
42
+ - 先看测量结果里的图层类型:`icon/svg` 才允许落地为 SVG;如果 Sketch 返回的是 `Image`,禁止伪造 SVG。
43
+ - `Image` 类型的 icon/button/arrow/help/customer-service 图层,优先做 **精确文件名查找**,不要先做语义联想或 canonical 合并。
44
+ - `Help Button`、`Customer Service Button`、`Customer Service Icon` 不能只因为位置相近就合并成同一个语义;先按 Sketch 图层名区分资产,再决定是否复用。
45
+ - 如果 `restorationContract.blockOrder` 中的顶部入口测得是 `Image`,回复里必须明确写出:测得的真实块名、最终采用的 assetPath、以及它来自 `bitmapLookupCandidates` 还是 canonical 副本;未确认前禁止继续猜图标。
46
+ - 只有当 `assets/icons/` 中不存在对应的手工导出资源时,才允许提示用户补导;不要再优先产出 page-local PNG 副本。
47
+ - 手工 `*_icon@4x.png` 约定下,默认不再强制要求补齐 `2.0x/3.0x`;只有旧位图流程才继续要求多倍率变体。
48
+ - 已存在但内容损坏的 SVG 视为无效资产,必须修复或替换;禁止为了绕过损坏文件再平行创建第二个语义相同的新图标。
49
+ - 设计源是 `Image` 时,回复里必须明确说明“来源优先是 assets/icons 下的手工导出 PNG,不是设计稿原生 SVG”,避免误报为高清 SVG。
50
+ - 若 `bitmapLookupCandidates` 与代码实际资产名都未命中,必须停止并说明“设计资产未确认”,而不是回退到 help/customer-service 等语义相近资源。
51
+
39
52
  **注意:**
40
53
  - 不要手动读取 artboard-measure.js(已废弃,合并进 sketch-tools.js)
41
54
  - 不要手动拼接 _SKETCH_CMD(由 sketch_measure skill 自动处理)
42
55
  - 测量结果中所有颜色均包含 `flutterColor: "Color(0xAARRGGBB)"` 格式,可直接使用
43
56
  - 设计稿还原时,restorationContract 优先于现有业务组件、路由标题、i18n 文案和常见 CTA 习惯
57
+ - 图标资产落地前,必须先做一次 `assets/icons/` 精确搜索;先看设计稿图层名对应的 `*_icon@4x.png`,再考虑其他复用路径
44
58
 
45
59
  ### 测量数据关键字段(v4.6.0 新增带★标记)
46
60
 
package/dist/index.js CHANGED
@@ -6406,7 +6406,7 @@ var OUTPUT_GUIDE = {
6406
6406
  "- **rp**: [left,top,right,bottom] \u76F8\u5BF9\u7236\u5BB9\u5668\u8FB9\u8DDD",
6407
6407
  "- **vf**: [x,y,w,h] \u89C6\u89C9\u8FB9\u754C\uFF1B\u5F53\u80CC\u666F\u5C42/\u9634\u5F71\u8D85\u51FA\u7236 Group frame \u65F6\uFF0C\u5BB9\u5668\u5C3A\u5BF8\u548C\u89C6\u89C9\u5E95\u8FB9\u8DDD\u4F18\u5148\u770B vf",
6408
6408
  "- **icon/svg/cs/xs**: \u56FE\u6807\u6807\u8BC6/\u771F\u5B9E SvgPicture.string(...) \u4EE3\u7801/containerSize/contentSize",
6409
- "- **image/img/bitmapPath/bitmapScales**: \u4F4D\u56FE\u56FE\u5C42\u6807\u8BC6/Image.asset \u4EE3\u7801/\u5EFA\u8BAE\u843D\u76D8\u8DEF\u5F84/1x-3x \u5BFC\u51FA\u8981\u6C42",
6409
+ "- **image/img/bitmapPath/bitmapScales/bitmapLookupCandidates/bitmapReuseMode**: \u4F4D\u56FE\u56FE\u5C42\u6807\u8BC6/Image.asset \u4EE3\u7801/\u5EFA\u8BAE\u843D\u76D8\u8DEF\u5F84/\u5BFC\u51FA\u7EA6\u5B9A/\u4F18\u5148\u641C\u7D22\u7684\u624B\u5DE5\u56FE\u6807\u6587\u4EF6\u540D/\u662F\u5426 shared-canonical",
6410
6410
  "- **svgMode/svgReason/svgFile**: preserve|tint / \u989C\u8272\u7B56\u7565\u539F\u56E0 / \u5EFA\u8BAE\u843D\u76D8\u6587\u4EF6\u540D",
6411
6411
  "- **text/style/family/textHeight**: \u6587\u672C\u5185\u5BB9/\u5B8C\u6574 TextStyle/\u5B57\u4F53\u65CF/\u884C\u9AD8\u6BD4",
6412
6412
  "- **lh/strut**: strutHeight / \u5B8C\u6574 StrutStyle(...)",
@@ -6423,7 +6423,8 @@ var OUTPUT_GUIDE = {
6423
6423
  "- \u56FE\u6807\u662F\u5426\u8BEF\u7528\u65E7\u9875\u9762\u8D44\u4EA7\uFF1A\u540C\u540D\u7BAD\u5934/\u56FE\u6807\u9ED8\u8BA4\u4E0D\u53EF\u4FE1\uFF0C\u5148\u6838\u5BF9\u6765\u6E90\u56FE\u5C42",
6424
6424
  "- SVG \u662F\u5426\u8BEF\u52A0 ColorFilter\uFF1A\u4FDD\u7559\u539F\u8272\u7684 SVG \u4E0D\u5F97\u4E8C\u6B21\u67D3\u8272",
6425
6425
  "- \u56FE\u6807\u662F\u5426\u7528\u9519\u5C3A\u5BF8\uFF1A16x16 \u69FD\u4F4D\u4E0D\u7B49\u4E8E 16x16 \u56FE\u5F62\uFF0C\u6309 xs/cs \u5206\u79BB\u5360\u4F4D\u4E0E\u6E32\u67D3",
6426
- "- Bitmap \u662F\u5426\u8BEF\u5F53 SVG\uFF1ASketch Image \u56FE\u5C42\u5FC5\u987B\u5BFC\u51FA PNG + Image.asset\uFF0C\u4E14\u8865\u9F50 2.0x/3.0x \u53D8\u4F53",
6426
+ "- Bitmap \u662F\u5426\u8BEF\u5F53 SVG\uFF1ASketch Image \u56FE\u5C42\u5FC5\u987B\u4F18\u5148\u5728 assets/icons/ \u4E2D\u67E5\u627E\u8BBE\u8BA1\u539F\u540D\u6216 bitmapLookupCandidates \u6307\u5411\u7684\u4F4D\u56FE\uFF0C\u547D\u4E2D\u540E\u76F4\u63A5\u590D\u7528\uFF0C\u4E0D\u518D\u4F2A\u9020 SVG",
6427
+ "- \u9876\u90E8 Help/Customer Service/Support Entry \u82E5\u5728 blockOrder \u4E2D\u662F Image\uFF0C\u5FC5\u987B\u5148\u6309\u771F\u5B9E\u5757\u540D\u548C bitmapLookupCandidates \u590D\u7528\uFF0C\u4E0D\u5F97\u7528\u8BED\u4E49\u76F8\u8FD1\u6309\u94AE\u66FF\u4EE3",
6427
6428
  "- \u5168\u5C4F app \u9875\u9762\u5728\u5BBD\u5C4F\u5E94\u8DDF\u968F\u5BBF\u4E3B Scaffold/SafeArea/AppBar \u548C\u9875\u9762 padding\uFF0C\u7981\u6B62\u628A\u6574\u9875\u9501\u6210 390 \u753B\u677F\u58F3\u5B50",
6428
6429
  "- Sketch \u9876\u90E8\u65F6\u95F4/\u4FE1\u53F7/WiFi/\u7535\u6C60\u5C5E\u4E8E\u8BBE\u5907\u7CFB\u7EDF chrome\uFF0C\u4E0D\u5C5E\u4E8E\u9875\u9762\u4E1A\u52A1 UI\uFF0C\u7981\u6B62\u76F4\u63A5\u8FD8\u539F\u5230 Flutter \u9875\u9762",
6429
6430
  "- \u82E5\u8282\u70B9\u540C\u65F6\u7ED9\u51FA f \u4E0E vf\uFF0C\u8BF4\u660E\u80CC\u666F\u5C42/\u9634\u5F71\u8D85\u51FA\u903B\u8F91 frame\uFF1B\u5BB9\u5668\u5C3A\u5BF8\u548C\u5E95\u90E8\u89C6\u89C9\u95F4\u8DDD\u4F18\u5148\u4F7F\u7528 vf",
@@ -6493,11 +6494,20 @@ async function sketchMeasure(args) {
6493
6494
  const scriptVersion = vm ? vm[1] : "1.0.0";
6494
6495
  const source = `plugin:${scriptFile}`;
6495
6496
  let fullScript = scriptContent;
6497
+ const measureOutputPath = cmd !== "svg" ? args.outputPath || path19.join(os.tmpdir(), `mta_sketch_${cmd}_output.json`) : null;
6496
6498
  if (cmd === "svg" && args.outputPath) {
6497
- fullScript = `var _SVG_OUTPUT_PATH = '${args.outputPath.replace(/'/g, "\\'")}';\\n` + scriptContent;
6499
+ fullScript = `var _SVG_OUTPUT_PATH = '${args.outputPath.replace(/'/g, "\\'")}';
6500
+ ` + scriptContent;
6498
6501
  } else if (cmd === "compact") {
6502
+ if (measureOutputPath) {
6503
+ fullScript = `var _MEASURE_OUTPUT_PATH = '${measureOutputPath.replace(/'/g, "\\'")}';
6504
+ ` + fullScript;
6505
+ }
6499
6506
  fullScript = `var _FLUTTER_COMPACT = true;
6500
- ` + scriptContent;
6507
+ ` + fullScript;
6508
+ } else if (measureOutputPath) {
6509
+ fullScript = `var _MEASURE_OUTPUT_PATH = '${measureOutputPath.replace(/'/g, "\\'")}';
6510
+ ` + fullScript;
6501
6511
  }
6502
6512
  const tempScriptPath = path19.join(os.tmpdir(), "mta_sketch_script.js");
6503
6513
  fs19.writeFileSync(tempScriptPath, fullScript, "utf-8");
@@ -6518,13 +6528,15 @@ async function sketchMeasure(args) {
6518
6528
  scriptPath: tempScriptPath,
6519
6529
  scriptLength: fullScript.length,
6520
6530
  pluginInstall: installStatus,
6521
- instruction: "\u5C06 loaderScript \u4F20\u5165 mcp_sketch_run_code({ code: loaderScript }) \u6267\u884C\u3002\u8BBE\u8BA1\u7A3F\u8FD8\u539F\u9996\u8F6E\u4F18\u5148\u4F7F\u7528 cmd=compact\uFF1B\u53EA\u6709\u5F53 compact contract \u4E0D\u8DB3\u4EE5\u89E3\u91CA\u5C40\u90E8\u5E03\u5C40\u6216\u6837\u5F0F\u65F6\uFF0C\u518D\u8865\u7528 measure/style\u3002\u82E5\u504F\u5DEE\u96C6\u4E2D\u5728\u5355\u4E2A icon/svg/bitmap\uFF0C\u8BF7\u91CD\u65B0\u9009\u4E2D\u8BE5\u56FE\u5C42\u6267\u884C svg \u6216\u5C40\u90E8\u5BFC\u51FA\uFF0C\u5E76\u590D\u67E5 colorStrategy\u3001viewBox\u3001containerSize/contentSize\u3001vf \u4E0E bitmapScales\uFF0C\u907F\u514D\u590D\u7528\u65E7\u9875\u9762\u8D44\u4EA7\u3002\u5168\u5C4F app \u9875\u9762\u5728\u5BBD\u5C4F\u5E94\u8DDF\u968F\u5BBF\u4E3B Scaffold/SafeArea/AppBar \u548C\u9875\u9762 padding \u9002\u914D\uFF1BSketch \u9876\u90E8\u65F6\u95F4/\u4FE1\u53F7/WiFi/\u7535\u6C60\u5C5E\u4E8E\u8BBE\u5907\u7CFB\u7EDF chrome\uFF0C\u7981\u6B62\u76F4\u63A5\u8FD8\u539F\u5230 Flutter \u9875\u9762\u3002compact \u82E5\u68C0\u6D4B\u5230\u4F60\u9009\u4E2D\u7684\u662F\u5185\u5C42 glyph\uFF0C\u4F1A\u81EA\u52A8\u63D0\u5347\u5230\u6240\u5C5E\u753B\u677F\u5E76\u901A\u8FC7 selectionContext \u56DE\u62A5\u3002\u8BF7\u5148\u5728 Sketch \u4E2D\u9009\u4E2D\u76EE\u6807\u753B\u677F/\u56FE\u5C42\u3002",
6531
+ instruction: "\u5C06 loaderScript \u4F20\u5165 mcp_sketch_run_code({ code: loaderScript }) \u6267\u884C\u3002\u8BBE\u8BA1\u7A3F\u8FD8\u539F\u9996\u8F6E\u4F18\u5148\u4F7F\u7528 cmd=compact\uFF1B\u53EA\u6709\u5F53 compact contract \u4E0D\u8DB3\u4EE5\u89E3\u91CA\u5C40\u90E8\u5E03\u5C40\u6216\u6837\u5F0F\u65F6\uFF0C\u518D\u8865\u7528 measure/style\u3002\u82E5\u504F\u5DEE\u96C6\u4E2D\u5728\u5355\u4E2A icon/svg/bitmap\uFF0C\u8BF7\u5148\u5728 assets/icons/ \u4E2D\u6309\u8BBE\u8BA1\u7A3F\u56FE\u5C42\u540D\u3001bitmapLookupCandidates \u548C bitmapReuseMode \u67E5\u627E\u771F\u5B9E\u4F4D\u56FE\u6216\u89C4\u8303\u5316\u526F\u672C\uFF0C\u907F\u514D\u7EE7\u7EED\u63A8\u65AD\u65E7\u9875\u9762\u8D44\u4EA7\u6216\u4F2A\u9020 SVG\u3002\u9876\u90E8 Help/Customer Service/Support Entry \u82E5 blockOrder \u6807\u8BB0\u4E3A Image\uFF0C\u5FC5\u987B\u5148\u590D\u7528\u5BF9\u5E94\u4F4D\u56FE\uFF0C\u672A\u547D\u4E2D\u65F6\u5E94\u505C\u6B62\u731C\u6D4B\u3002\u5168\u5C4F app \u9875\u9762\u5728\u5BBD\u5C4F\u5E94\u8DDF\u968F\u5BBF\u4E3B Scaffold/SafeArea/AppBar \u548C\u9875\u9762 padding \u9002\u914D\uFF1BSketch \u9876\u90E8\u65F6\u95F4/\u4FE1\u53F7/WiFi/\u7535\u6C60\u5C5E\u4E8E\u8BBE\u5907\u7CFB\u7EDF chrome\uFF0C\u7981\u6B62\u76F4\u63A5\u8FD8\u539F\u5230 Flutter \u9875\u9762\u3002compact \u82E5\u68C0\u6D4B\u5230\u4F60\u9009\u4E2D\u7684\u662F\u5185\u5C42 glyph\uFF0C\u4F1A\u81EA\u52A8\u63D0\u5347\u5230\u6240\u5C5E\u753B\u677F\u5E76\u901A\u8FC7 selectionContext \u56DE\u62A5\u3002\u8BF7\u5148\u5728 Sketch \u4E2D\u9009\u4E2D\u76EE\u6807\u753B\u677F/\u56FE\u5C42\u3002",
6532
+ measureOutputPath,
6522
6533
  restoreChecklist: [
6523
6534
  "\u5148\u6309 restorationContract \u9501\u5B9A\u9875\u9762\u9AA8\u67B6\uFF0C\u518D\u5904\u7406\u5C40\u90E8\u6837\u5F0F",
6524
- "\u5355\u4E2A\u56FE\u6807\u504F\u5DEE\u4F18\u5148\u5C40\u90E8\u91CD\u5BFC\u51FA\uFF0C\u4E0D\u590D\u7528\u5176\u5B83\u9875\u9762\u65E7\u8D44\u4EA7",
6535
+ "\u5355\u4E2A\u56FE\u6807\u504F\u5DEE\u5148\u67E5 assets/icons/ \u4E2D\u8BBE\u8BA1\u7A3F\u540C\u540D\u3001bitmapLookupCandidates \u5217\u51FA\u7684\u771F\u5B9E\u4F4D\u56FE\uFF0C\u518D\u51B3\u5B9A\u662F\u5426\u8865\u5BFC",
6525
6536
  "preserve \u6A21\u5F0F SVG \u7981\u6B62\u8FFD\u52A0 ColorFilter",
6526
6537
  "16x16 \u69FD\u4F4D\u4E0E 6x8/12x12 \u56FE\u5F62\u8981\u5206\u5F00\u5904\u7406",
6527
- "Sketch Image \u56FE\u5C42\u5BFC\u51FA PNG\uFF0C\u4E0D\u8981\u4F2A\u88C5\u6210 SVG \u4F7F\u7528\uFF0C\u5E76\u8865\u9F50 2.0x/3.0x \u53D8\u4F53",
6538
+ "Sketch Image \u56FE\u5C42\u4F18\u5148\u590D\u7528 assets/icons \u4E2D\u6309\u56FE\u5C42\u539F\u540D\u5BFC\u51FA\u7684\u4F4D\u56FE\u6216 bitmapLookupCandidates \u547D\u4E2D\u7684\u8D44\u6E90\uFF0C\u4E0D\u8981\u4F2A\u88C5\u6210 SVG\uFF1B\u53EA\u6709\u65E7\u4F4D\u56FE\u6D41\u7A0B\u624D\u8865 2.0x/3.0x",
6539
+ "\u9876\u90E8 Help/Customer Service/Support Entry \u82E5\u6D4B\u5F97\u4E3A Image\uFF0C\u7981\u6B62\u7528\u8BED\u4E49\u76F8\u8FD1\u7684\u5176\u4ED6\u6309\u94AE\u4F4D\u56FE\u6216 help glyph \u66FF\u4EE3",
6528
6540
  "\u5168\u5C4F app \u9875\u9762\u5728\u5BBD\u5C4F\u5E94\u8DDF\u968F\u5BBF\u4E3B Scaffold/SafeArea/AppBar \u548C\u9875\u9762 padding\uFF0C\u53EA\u6709\u660E\u786E\u6D4B\u5F97\u7684\u5C40\u90E8\u6D6E\u5C42/\u5361\u7247\u5BB9\u5668\u624D\u505A clamp",
6529
6541
  "Sketch \u9876\u90E8\u65F6\u95F4/\u4FE1\u53F7/WiFi/\u7535\u6C60\u5C5E\u4E8E\u8BBE\u5907\u7CFB\u7EDF chrome\uFF0C\u4E0D\u5C5E\u4E8E\u9875\u9762\u4E1A\u52A1 UI\uFF0C\u7981\u6B62\u76F4\u63A5\u8FD8\u539F\u5230 Flutter \u9875\u9762",
6530
6542
  "\u82E5\u8282\u70B9\u540C\u65F6\u51FA\u73B0 f \u4E0E vf\uFF0C\u5BB9\u5668\u5C3A\u5BF8\u548C\u5E95\u90E8\u89C6\u89C9\u95F4\u8DDD\u4F18\u5148\u4F7F\u7528 vf\uFF0C\u4E0D\u8981\u53EA\u6284\u903B\u8F91 frame",
@@ -6536,7 +6548,7 @@ async function sketchMeasure(args) {
6536
6548
  required: true,
6537
6549
  checks: ["contract-validator", "asset-source-validator", "route-review-validator"],
6538
6550
  exampleArgs: {
6539
- measurePath: "/path/to/sketch-compact-output.json",
6551
+ measurePath: measureOutputPath || "/path/to/sketch-compact-output.json",
6540
6552
  codeFilePath: "/path/to/generated_page.dart",
6541
6553
  routeFilePath: "/path/to/app_pages.dart",
6542
6554
  routeName: "AppRoutes.xxx",
@@ -6557,30 +6569,57 @@ function isRecord2(value) {
6557
6569
  return typeof value === "object" && value !== null && !Array.isArray(value);
6558
6570
  }
6559
6571
  function parseNestedJson(raw) {
6560
- let candidate = raw.trim();
6561
- for (let attempt = 0; attempt < 4; attempt += 1) {
6572
+ var _a;
6573
+ const queue = [raw.trim()];
6574
+ const seen = /* @__PURE__ */ new Set();
6575
+ let lastError = null;
6576
+ while (queue.length > 0) {
6577
+ const candidate = (_a = queue.shift()) == null ? void 0 : _a.trim();
6578
+ if (!candidate || seen.has(candidate)) {
6579
+ continue;
6580
+ }
6581
+ seen.add(candidate);
6562
6582
  try {
6563
6583
  const parsed = JSON.parse(candidate);
6564
6584
  if (typeof parsed === "string") {
6565
- candidate = parsed.trim();
6585
+ queue.push(parsed.trim());
6566
6586
  continue;
6567
6587
  }
6568
6588
  if (isRecord2(parsed) && Array.isArray(parsed.content) && parsed.content[0] && isRecord2(parsed.content[0])) {
6569
6589
  const text = parsed.content[0].text;
6570
6590
  if (typeof text === "string") {
6571
- candidate = text.trim();
6591
+ queue.push(text.trim());
6572
6592
  continue;
6573
6593
  }
6574
6594
  }
6575
6595
  return parsed;
6576
- } catch {
6577
- if (candidate.startsWith("'") && candidate.endsWith("'") || candidate.startsWith('"') && candidate.endsWith('"')) {
6578
- candidate = candidate.slice(1, -1);
6596
+ } catch (error) {
6597
+ lastError = error;
6598
+ }
6599
+ if (candidate.startsWith("'") && candidate.endsWith("'") || candidate.startsWith('"') && candidate.endsWith('"')) {
6600
+ queue.push(candidate.slice(1, -1));
6601
+ }
6602
+ const decoded = candidate.replace(/\\r/g, "\r").replace(/\\n/g, "\n").replace(/\\t/g, " ").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
6603
+ if (decoded !== candidate) {
6604
+ queue.push(decoded);
6605
+ }
6606
+ const firstBrace = candidate.indexOf("{");
6607
+ const lastBrace = candidate.lastIndexOf("}");
6608
+ if (firstBrace !== -1 && lastBrace > firstBrace) {
6609
+ const sliced = candidate.slice(firstBrace, lastBrace + 1);
6610
+ if (sliced !== candidate) {
6611
+ queue.push(sliced);
6612
+ }
6613
+ const decodedSliced = sliced.replace(/\\r/g, "\r").replace(/\\n/g, "\n").replace(/\\t/g, " ").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
6614
+ if (decodedSliced !== sliced) {
6615
+ queue.push(decodedSliced);
6579
6616
  }
6580
- candidate = candidate.replace(/\\n/g, "\n").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
6581
6617
  }
6582
6618
  }
6583
- return JSON.parse(candidate);
6619
+ if (lastError instanceof Error) {
6620
+ throw lastError;
6621
+ }
6622
+ throw new Error("measure payload \u89E3\u6790\u5931\u8D25");
6584
6623
  }
6585
6624
  function loadMeasurePayload(args) {
6586
6625
  const raw = args.measureContent || (args.measurePath ? fs20.readFileSync(args.measurePath, "utf-8") : "");
@@ -6593,6 +6632,9 @@ function loadMeasurePayload(args) {
6593
6632
  }
6594
6633
  return parsed;
6595
6634
  }
6635
+ function normalizeSemanticKey(value) {
6636
+ return value.replace(/[\s_-]+/g, "").toLowerCase();
6637
+ }
6596
6638
  function hasAnyPattern(source, patterns) {
6597
6639
  return patterns.some((pattern) => pattern.test(source));
6598
6640
  }
@@ -6615,7 +6657,10 @@ function collectAssetContracts(node, bucket = []) {
6615
6657
  sketchName: node.n || "Image",
6616
6658
  fileName: typeof node.bitmapFile === "string" ? path20.posix.basename(node.bitmapFile) : typeof node.bitmapPath === "string" ? path20.posix.basename(node.bitmapPath) : void 0,
6617
6659
  sourceType: "bitmap",
6618
- scaleVariants: node.bitmapScales
6660
+ scaleVariants: node.bitmapScales,
6661
+ lookupCandidates: node.bitmapLookupCandidates,
6662
+ reuseMode: node.bitmapReuseMode,
6663
+ reuseKey: node.bitmapReuseKey
6619
6664
  });
6620
6665
  }
6621
6666
  for (const child of node.ch || []) {
@@ -6624,9 +6669,43 @@ function collectAssetContracts(node, bucket = []) {
6624
6669
  return bucket;
6625
6670
  }
6626
6671
  function extractCodeAssets(codeContent) {
6627
- const matches = codeContent.match(/assets\/[A-Za-z0-9_./-]+\.(svg|png|jpg|jpeg|webp)/g);
6672
+ const matches = codeContent.match(/assets\/[^'")]+\.(svg|png|jpg|jpeg|webp)/g);
6628
6673
  return matches ? [...new Set(matches)] : [];
6629
6674
  }
6675
+ function isManualExport4xBitmapAsset(assetPath) {
6676
+ return /_icon@4x\.(png|jpg|jpeg|webp)$/i.test(path20.posix.basename(assetPath));
6677
+ }
6678
+ function isCriticalBitmapEntryName(name) {
6679
+ return /help[_ -]?button|customer[_ -]?service(?:[_ -]?(button|icon))?|support(?:[_ -]?entry)?|帮助|客服/i.test(name);
6680
+ }
6681
+ function buildBitmapCandidateSet(contract) {
6682
+ const candidates = /* @__PURE__ */ new Set();
6683
+ if (contract.fileName) {
6684
+ candidates.add(path20.posix.basename(contract.fileName).toLowerCase());
6685
+ }
6686
+ for (const candidate of contract.lookupCandidates || []) {
6687
+ candidates.add(path20.posix.basename(candidate).toLowerCase());
6688
+ }
6689
+ return candidates;
6690
+ }
6691
+ function collectCriticalBitmapContracts(payload, assetContracts) {
6692
+ var _a;
6693
+ const criticalBitmapKeys = new Set(
6694
+ (((_a = payload.restorationContract) == null ? void 0 : _a.blockOrder) || []).flatMap((block) => {
6695
+ const blockIdentity = `${block.name || ""} ${block.label || ""}`;
6696
+ if (block.type !== "Image" || !isCriticalBitmapEntryName(blockIdentity)) {
6697
+ return [];
6698
+ }
6699
+ return [block.name, block.label].filter((value) => typeof value === "string" && value.trim().length > 0).map((value) => normalizeSemanticKey(value));
6700
+ })
6701
+ );
6702
+ if (criticalBitmapKeys.size === 0) {
6703
+ return [];
6704
+ }
6705
+ return assetContracts.filter(
6706
+ (contract) => contract.sourceType === "bitmap" && criticalBitmapKeys.has(normalizeSemanticKey(contract.sketchName))
6707
+ );
6708
+ }
6630
6709
  function hasCodePropertyLiteral(codeContent, property, value) {
6631
6710
  const escapedValue = String(value).replace(".", "\\.");
6632
6711
  return new RegExp(`${property}\\s*:\\s*${escapedValue}(?:\\.0+)?\\b`).test(codeContent);
@@ -6747,17 +6826,17 @@ function buildContractValidator(payload, codeContent) {
6747
6826
  });
6748
6827
  }
6749
6828
  }
6750
- const needsSupportButton = blockOrder.some((block) => /customer service|support|客服/i.test(`${block.name || ""} ${block.label || ""}`));
6829
+ const needsSupportButton = blockOrder.some((block) => /customer service|support|客服|help[_ -]?button|帮助/i.test(`${block.name || ""} ${block.label || ""}`));
6751
6830
  if (needsSupportButton) {
6752
6831
  requiredBlocks += 1;
6753
- if (hasAnyPattern(codeContent, [/customer[_ ]service/i, /support/i, /recipient_customer_service_button/i, /客服/])) {
6832
+ if (hasAnyPattern(codeContent, [/customer[_ -]?service/i, /support/i, /help[_ -]?button/i, /recipient_customer_service_button/i, /客服/])) {
6754
6833
  matchedBlocks += 1;
6755
6834
  } else {
6756
6835
  findings.push({
6757
6836
  id: "missing_customer_service_block",
6758
6837
  severity: "error",
6759
- message: "\u8BBE\u8BA1\u7A3F\u5B58\u5728 Customer Service Button\uFF0C\u4F46\u4EE3\u7801\u4E2D\u672A\u68C0\u6D4B\u5230\u5BF9\u5E94\u5165\u53E3\u3002",
6760
- suggestion: "\u8865\u9F50\u5BA2\u670D\u6309\u94AE\uFF0C\u4E0D\u8981\u5728\u751F\u6210\u9636\u6BB5\u76F4\u63A5\u7701\u7565\u53F3\u4E0A\u89D2\u5165\u53E3\u3002"
6838
+ message: "\u8BBE\u8BA1\u7A3F\u5B58\u5728 Help/Customer Service Button\uFF0C\u4F46\u4EE3\u7801\u4E2D\u672A\u68C0\u6D4B\u5230\u5BF9\u5E94\u5165\u53E3\u3002",
6839
+ suggestion: "\u8865\u9F50\u53F3\u4E0A\u89D2\u5E2E\u52A9/\u5BA2\u670D\u5165\u53E3\uFF0C\u5E76\u4FDD\u6301\u4E0E\u8BBE\u8BA1\u56FE\u5C42\u8BED\u4E49\u4E00\u81F4\uFF0C\u4E0D\u8981\u628A Help Button \u66FF\u6362\u6210\u5176\u4ED6\u5BA2\u670D\u56FE\u6807\u3002"
6761
6840
  });
6762
6841
  }
6763
6842
  }
@@ -6825,6 +6904,7 @@ function buildContractValidator(payload, codeContent) {
6825
6904
  function buildAssetValidator(payload, codeContent, args) {
6826
6905
  const findings = [];
6827
6906
  const assetContracts = collectAssetContracts(payload.tree);
6907
+ const criticalBitmapContracts = collectCriticalBitmapContracts(payload, assetContracts);
6828
6908
  const expectedVectorFiles = new Set(
6829
6909
  assetContracts.filter((item) => item.sourceType === "vector" && item.fileName).map((item) => item.fileName.toLowerCase())
6830
6910
  );
@@ -6832,6 +6912,9 @@ function buildAssetValidator(payload, codeContent, args) {
6832
6912
  const codeAssets = extractCodeAssets(codeContent);
6833
6913
  const codeVectorAssets = codeAssets.filter((asset) => asset.endsWith(".svg"));
6834
6914
  const codeBitmapAssets = codeAssets.filter((asset) => /\.(png|jpg|jpeg|webp)$/.test(asset));
6915
+ const codeBitmapFileNames = new Set(
6916
+ codeBitmapAssets.map((asset) => path20.posix.basename(asset).toLowerCase())
6917
+ );
6835
6918
  const unexpectedVectorAssets = codeVectorAssets.filter((asset) => !expectedVectorFiles.has(path20.posix.basename(asset).toLowerCase()));
6836
6919
  if (unexpectedVectorAssets.length > 0) {
6837
6920
  findings.push({
@@ -6845,35 +6928,75 @@ function buildAssetValidator(payload, codeContent, args) {
6845
6928
  if (expectedBitmapCount > 0 && codeBitmapAssets.length === 0) {
6846
6929
  findings.push({
6847
6930
  id: "missing_bitmap_assets",
6848
- severity: "warning",
6849
- message: `\u5F53\u524D\u8BBE\u8BA1\u7A3F\u5305\u542B ${expectedBitmapCount} \u4E2A Image \u56FE\u5C42\uFF0C\u4F46\u4EE3\u7801\u91CC\u6CA1\u6709\u4EFB\u4F55\u4F4D\u56FE\u8D44\u6E90\u5F15\u7528\u3002`,
6850
- suggestion: "\u68C0\u67E5\u662F\u5426\u628A Sketch Image \u56FE\u5C42\u9519\u8BEF\u5F53\u6210 SVG \u6216\u4EE3\u7801\u7ED8\u5236\u4E86\u3002"
6931
+ severity: criticalBitmapContracts.length > 0 ? "error" : "warning",
6932
+ message: criticalBitmapContracts.length > 0 ? `\u5F53\u524D\u8BBE\u8BA1\u7A3F\u5305\u542B ${expectedBitmapCount} \u4E2A Image \u56FE\u5C42\uFF0C\u4E14\u5173\u952E\u5165\u53E3\u4F4D\u56FE\u4E0D\u80FD\u88AB\u8BED\u4E49\u76F8\u8FD1\u7684 SVG/\u81EA\u7ED8\u5B9E\u73B0\u66FF\u4EE3\uFF0C\u4F46\u4EE3\u7801\u91CC\u6CA1\u6709\u4EFB\u4F55\u4F4D\u56FE\u8D44\u6E90\u5F15\u7528\u3002` : `\u5F53\u524D\u8BBE\u8BA1\u7A3F\u5305\u542B ${expectedBitmapCount} \u4E2A Image \u56FE\u5C42\uFF0C\u4F46\u4EE3\u7801\u91CC\u6CA1\u6709\u4EFB\u4F55\u4F4D\u56FE\u8D44\u6E90\u5F15\u7528\u3002`,
6933
+ suggestion: criticalBitmapContracts.length > 0 ? "\u5173\u952E\u5165\u53E3\u7C7B Image block \u5FC5\u987B\u5148\u6309\u8BBE\u8BA1\u56FE\u5C42\u540D\u548C bitmapLookupCandidates \u590D\u7528\u771F\u5B9E\u4F4D\u56FE\uFF1B\u4E0D\u8981\u518D\u628A Help / Customer Service / Support Entry \u753B\u6210\u81EA\u5B9A\u4E49 SVG \u6216\u58F3\u5B50\u7EC4\u4EF6\u3002" : "\u68C0\u67E5\u662F\u5426\u628A Sketch Image \u56FE\u5C42\u9519\u8BEF\u5F53\u6210 SVG \u6216\u4EE3\u7801\u7ED8\u5236\u4E86\u3002"
6851
6934
  });
6852
6935
  }
6936
+ if (criticalBitmapContracts.length > 0 && codeBitmapAssets.length > 0) {
6937
+ const mismatchedCriticalBitmapEntries = criticalBitmapContracts.map((contract) => {
6938
+ const candidateSet = buildBitmapCandidateSet(contract);
6939
+ const matched = [...codeBitmapFileNames].some((fileName) => candidateSet.has(fileName));
6940
+ return matched ? null : `${contract.sketchName} -> expected one of ${[...candidateSet].join(", ")}`;
6941
+ }).filter((item) => item !== null);
6942
+ if (mismatchedCriticalBitmapEntries.length > 0) {
6943
+ findings.push({
6944
+ id: "critical_bitmap_entry_not_reused",
6945
+ severity: "error",
6946
+ message: "\u5173\u952E Image \u5165\u53E3\u6CA1\u6709\u590D\u7528\u8BBE\u8BA1\u6E90\u4F4D\u56FE\uFF0C\u5B58\u5728\u628A Help / Customer Service \u7B49\u8BED\u4E49\u76F8\u8FD1\u56FE\u6807\u4E92\u6362\u7684\u98CE\u9669\u3002",
6947
+ evidence: mismatchedCriticalBitmapEntries,
6948
+ suggestion: "\u5148\u5BF9\u7167 restorationContract.blockOrder \u4E2D\u7684\u771F\u5B9E\u5757\u540D\uFF0C\u518D\u6309 bitmapLookupCandidates \u6216 canonical \u6587\u4EF6\u540D\u590D\u7528\u4F4D\u56FE\uFF1B\u672A\u547D\u4E2D\u65F6\u5E94\u505C\u6B62\u731C\u6D4B\u5E76\u8981\u6C42\u91CD\u65B0\u786E\u8BA4\u8BBE\u8BA1\u5BFC\u51FA\u3002"
6949
+ });
6950
+ }
6951
+ }
6853
6952
  if (codeBitmapAssets.length > 0) {
6854
6953
  findings.push({
6855
6954
  id: "bitmap_asset_source_check_required",
6856
6955
  severity: "info",
6857
- message: "\u4EE3\u7801\u4E2D\u5B58\u5728\u4F4D\u56FE\u8D44\u6E90\uFF0C\u9700\u4EBA\u5DE5\u786E\u8BA4\u5B83\u4EEC\u6765\u81EA\u5F53\u524D\u753B\u677F\u800C\u4E0D\u662F\u65E7\u9875\u9762\u7F13\u5B58\u8D44\u6E90\u3002",
6956
+ message: "\u4EE3\u7801\u4E2D\u5B58\u5728\u4F4D\u56FE\u8D44\u6E90\uFF0C\u9700\u4EBA\u5DE5\u786E\u8BA4\u5B83\u4EEC\u6765\u81EA\u5F53\u524D\u753B\u677F\uFF0C\u6216\u5DF2\u6B63\u786E\u590D\u7528\u5F53\u524D\u8BBE\u8BA1\u7EA6\u5B9A\u7684 canonical \u5171\u4EAB\u8D44\u4EA7\u3002",
6858
6957
  evidence: codeBitmapAssets,
6859
- suggestion: "\u4F4D\u56FE\u8D44\u6E90\u4F18\u5148\u6309\u5F53\u524D\u753B\u677F\u76F4\u63A5\u5BFC\u51FA\u5230 assets/icons/\uFF0C\u907F\u514D\u7EE7\u7EED\u590D\u7528\u65E7\u9875\u9762 PNG\u3002"
6958
+ suggestion: "\u4F18\u5148\u5148\u5728 assets/icons/ \u4E2D\u67E5\u627E\u7528\u6237\u624B\u5DE5\u5BFC\u51FA\u7684\u8BBE\u8BA1\u540C\u540D *_icon@4x.png\uFF1B\u53EA\u6709\u7F3A\u5931\u65F6\uFF0C\u624D\u6309 contract \u6307\u5B9A\u65B9\u5F0F\u8865\u5BFC\u6216\u590D\u7528\u5171\u4EAB\u8D44\u4EA7\u3002"
6860
6959
  });
6861
6960
  }
6862
- if (expectedBitmapCount > 0 && codeBitmapAssets.length > 0) {
6863
- const pageClassName = extractPageClassName(codeContent) || "";
6864
- const normalizedStem = pageClassName.replace(/Page$/, "").replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
6865
- if (normalizedStem) {
6866
- const hasPageLocalBitmap = codeBitmapAssets.some(
6867
- (asset) => path20.posix.basename(asset).toLowerCase().includes(normalizedStem)
6868
- );
6869
- if (!hasPageLocalBitmap) {
6870
- findings.push({
6871
- id: "bitmap_assets_not_page_local",
6872
- severity: "warning",
6873
- message: "\u5F53\u524D\u9875\u9762\u4F4D\u56FE\u8D44\u6E90\u770B\u8D77\u6765\u4E0D\u662F page-local \u547D\u540D\uFF0C\u5B58\u5728\u7EE7\u7EED\u590D\u7528\u65E7\u9875\u9762 PNG \u7684\u98CE\u9669\u3002",
6874
- evidence: codeBitmapAssets,
6875
- suggestion: "\u4F4D\u56FE\u8D44\u6E90\u4F18\u5148\u6309\u5F53\u524D\u9875\u9762\u547D\u540D\u5BFC\u51FA\uFF0C\u4F8B\u5982\u5E26\u4E0A\u9875\u9762 stem\uFF0C\u907F\u514D\u6DF7\u5165\u65E7\u9875\u9762\u901A\u7528 PNG\u3002"
6876
- });
6961
+ const sharedBitmapFiles = new Set(
6962
+ assetContracts.filter((item) => item.sourceType === "bitmap" && item.reuseMode === "shared-canonical" && item.fileName).map((item) => item.fileName.toLowerCase())
6963
+ );
6964
+ if (sharedBitmapFiles.size > 0) {
6965
+ const usedBitmapFiles = new Set(
6966
+ codeBitmapAssets.map((asset) => path20.posix.basename(asset).toLowerCase())
6967
+ );
6968
+ const missingSharedBitmapFiles = [...sharedBitmapFiles].filter((fileName) => !usedBitmapFiles.has(fileName));
6969
+ if (missingSharedBitmapFiles.length > 0) {
6970
+ findings.push({
6971
+ id: "shared_bitmap_asset_not_reused",
6972
+ severity: "warning",
6973
+ message: "\u8BBE\u8BA1\u7A3F\u5DF2\u6807\u8BB0\u8BE5\u4F4D\u56FE\u4E3A\u5171\u4EAB canonical \u8D44\u4EA7\uFF0C\u4F46\u4EE3\u7801\u6CA1\u6709\u590D\u7528\u5BF9\u5E94\u6587\u4EF6\u540D\uFF0C\u5B58\u5728\u91CD\u590D\u7F13\u5B58\u6216\u628A Help/Customer Service \u8BED\u4E49\u8BEF\u5408\u5E76\u7684\u98CE\u9669\u3002",
6974
+ evidence: missingSharedBitmapFiles,
6975
+ suggestion: "\u4F18\u5148\u590D\u7528 contract \u8FD4\u56DE\u7684\u5171\u4EAB bitmap \u6587\u4EF6\u540D\uFF1BHelp Button \u4E0E Customer Service Button \u5FC5\u987B\u6309\u5404\u81EA canonical \u6587\u4EF6\u72EC\u7ACB\u590D\u7528\uFF0C\u4E0D\u80FD\u518D\u5408\u5E76\u6210\u540C\u4E00\u4E2A\u540D\u5B57\u3002"
6976
+ });
6977
+ }
6978
+ }
6979
+ const requiresPageLocalBitmap = assetContracts.some(
6980
+ (item) => item.sourceType === "bitmap" && item.reuseMode !== "shared-canonical"
6981
+ );
6982
+ if (expectedBitmapCount > 0 && codeBitmapAssets.length > 0 && requiresPageLocalBitmap) {
6983
+ const hasManualExport4xBitmap = codeBitmapAssets.some((asset) => isManualExport4xBitmapAsset(asset));
6984
+ if (!hasManualExport4xBitmap) {
6985
+ const pageClassName = extractPageClassName(codeContent) || "";
6986
+ const normalizedStem = pageClassName.replace(/Page$/, "").replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
6987
+ if (normalizedStem) {
6988
+ const hasPageLocalBitmap = codeBitmapAssets.some(
6989
+ (asset) => path20.posix.basename(asset).toLowerCase().includes(normalizedStem)
6990
+ );
6991
+ if (!hasPageLocalBitmap) {
6992
+ findings.push({
6993
+ id: "bitmap_assets_not_page_local",
6994
+ severity: "warning",
6995
+ message: "\u5F53\u524D\u9875\u9762\u4F4D\u56FE\u8D44\u6E90\u770B\u8D77\u6765\u4E0D\u662F page-local \u547D\u540D\uFF0C\u5B58\u5728\u7EE7\u7EED\u590D\u7528\u65E7\u9875\u9762 PNG \u7684\u98CE\u9669\u3002",
6996
+ evidence: codeBitmapAssets,
6997
+ suggestion: "\u4F4D\u56FE\u8D44\u6E90\u4F18\u5148\u6309\u5F53\u524D\u9875\u9762\u547D\u540D\u5BFC\u51FA\uFF0C\u4F8B\u5982\u5E26\u4E0A\u9875\u9762 stem\uFF1B\u82E5\u9879\u76EE\u5DF2\u6539\u4E3A\u624B\u5DE5 *_icon@4x.png \u7EA6\u5B9A\uFF0C\u5219\u65E0\u9700\u518D\u8FFD\u52A0 page-local stem\u3002"
6998
+ });
6999
+ }
6877
7000
  }
6878
7001
  }
6879
7002
  }
@@ -6885,6 +7008,9 @@ function buildAssetValidator(payload, codeContent, args) {
6885
7008
  if (requiresRetinaVariants) {
6886
7009
  const missingRetinaEvidence = [];
6887
7010
  for (const asset of codeBitmapAssets) {
7011
+ if (isManualExport4xBitmapAsset(asset)) {
7012
+ continue;
7013
+ }
6888
7014
  const assetOnDisk = resolveProjectAssetPath(args.projectPath, asset);
6889
7015
  if (!fs20.existsSync(assetOnDisk)) {
6890
7016
  missingRetinaEvidence.push(`${asset} (base missing)`);
@@ -6900,9 +7026,9 @@ function buildAssetValidator(payload, codeContent, args) {
6900
7026
  findings.push({
6901
7027
  id: "bitmap_retina_variants_missing",
6902
7028
  severity: "error",
6903
- message: "\u4F4D\u56FE\u8D44\u6E90\u7F3A\u5C11 2.0x/3.0x \u53D8\u4F53\uFF0C\u9AD8\u5206\u5C4F\u4E0B\u5BB9\u6613\u53D1\u7CCA\u3002",
7029
+ message: "\u4F4D\u56FE\u8D44\u6E90\u65E2\u6CA1\u6709\u4F7F\u7528\u624B\u5DE5\u5BFC\u51FA\u7684 *_icon@4x.png \u7EA6\u5B9A\uFF0C\u4E5F\u7F3A\u5C11 2.0x/3.0x \u53D8\u4F53\uFF0C\u9AD8\u5206\u5C4F\u4E0B\u5BB9\u6613\u53D1\u7CCA\u3002",
6904
7030
  evidence: missingRetinaEvidence,
6905
- suggestion: "\u6309\u5F53\u524D\u753B\u677F\u91CD\u65B0\u5BFC\u51FA PNG\uFF0C\u5E76\u8865\u9F50 assets/.../2.0x \u4E0E 3.0x \u53D8\u4F53\u540E\u518D\u9A8C\u6536\u3002"
7031
+ suggestion: "\u4F18\u5148\u6539\u4E3A assets/icons/*_icon@4x.png \u7684\u624B\u5DE5\u5BFC\u51FA\u7EA6\u5B9A\uFF1B\u5982\u679C\u4ECD\u8D70\u65E7\u4F4D\u56FE\u6D41\u7A0B\uFF0C\u518D\u8865\u9F50 assets/.../2.0x \u4E0E 3.0x \u53D8\u4F53\u3002"
6906
7032
  });
6907
7033
  }
6908
7034
  }