mta-mcp 3.15.6 → 3.16.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.
@@ -36,11 +36,23 @@
36
36
  5. 只有当 compact 无法解释局部布局/样式时,再补用 measure/style
37
37
  ```
38
38
 
39
+ ### 图标资产强制规则(v4.7.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
+ - 只有当 `assets/icons/` 中不存在对应的手工导出资源时,才允许提示用户补导;不要再优先产出 page-local PNG 副本。
46
+ - 手工 `*_icon@4x.png` 约定下,默认不再强制要求补齐 `2.0x/3.0x`;只有旧位图流程才继续要求多倍率变体。
47
+ - 已存在但内容损坏的 SVG 视为无效资产,必须修复或替换;禁止为了绕过损坏文件再平行创建第二个语义相同的新图标。
48
+ - 设计源是 `Image` 时,回复里必须明确说明“来源优先是 assets/icons 下的手工导出 PNG,不是设计稿原生 SVG”,避免误报为高清 SVG。
49
+
39
50
  **注意:**
40
51
  - 不要手动读取 artboard-measure.js(已废弃,合并进 sketch-tools.js)
41
52
  - 不要手动拼接 _SKETCH_CMD(由 sketch_measure skill 自动处理)
42
53
  - 测量结果中所有颜色均包含 `flutterColor: "Color(0xAARRGGBB)"` 格式,可直接使用
43
54
  - 设计稿还原时,restorationContract 优先于现有业务组件、路由标题、i18n 文案和常见 CTA 习惯
55
+ - 图标资产落地前,必须先做一次 `assets/icons/` 精确搜索;先看设计稿图层名对应的 `*_icon@4x.png`,再考虑其他复用路径
44
56
 
45
57
  ### 测量数据关键字段(v4.6.0 新增带★标记)
46
58
 
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**: \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",
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,7 @@ 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\u624B\u5DE5\u5BFC\u51FA\u7684 *_icon@4x.png\uFF0C\u547D\u4E2D\u540E\u76F4\u63A5\u590D\u7528\uFF0C\u4E0D\u518D\u4F2A\u9020 SVG",
6427
6427
  "- \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
6428
  "- 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
6429
  "- \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 +6493,20 @@ async function sketchMeasure(args) {
6493
6493
  const scriptVersion = vm ? vm[1] : "1.0.0";
6494
6494
  const source = `plugin:${scriptFile}`;
6495
6495
  let fullScript = scriptContent;
6496
+ const measureOutputPath = cmd !== "svg" ? args.outputPath || path19.join(os.tmpdir(), `mta_sketch_${cmd}_output.json`) : null;
6496
6497
  if (cmd === "svg" && args.outputPath) {
6497
- fullScript = `var _SVG_OUTPUT_PATH = '${args.outputPath.replace(/'/g, "\\'")}';\\n` + scriptContent;
6498
+ fullScript = `var _SVG_OUTPUT_PATH = '${args.outputPath.replace(/'/g, "\\'")}';
6499
+ ` + scriptContent;
6498
6500
  } else if (cmd === "compact") {
6501
+ if (measureOutputPath) {
6502
+ fullScript = `var _MEASURE_OUTPUT_PATH = '${measureOutputPath.replace(/'/g, "\\'")}';
6503
+ ` + fullScript;
6504
+ }
6499
6505
  fullScript = `var _FLUTTER_COMPACT = true;
6500
- ` + scriptContent;
6506
+ ` + fullScript;
6507
+ } else if (measureOutputPath) {
6508
+ fullScript = `var _MEASURE_OUTPUT_PATH = '${measureOutputPath.replace(/'/g, "\\'")}';
6509
+ ` + fullScript;
6501
6510
  }
6502
6511
  const tempScriptPath = path19.join(os.tmpdir(), "mta_sketch_script.js");
6503
6512
  fs19.writeFileSync(tempScriptPath, fullScript, "utf-8");
@@ -6518,13 +6527,14 @@ async function sketchMeasure(args) {
6518
6527
  scriptPath: tempScriptPath,
6519
6528
  scriptLength: fullScript.length,
6520
6529
  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",
6530
+ 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\u67E5\u627E\u624B\u5DE5\u5BFC\u51FA\u7684 *_icon@4x.png\uFF0C\u5E76\u590D\u67E5 colorStrategy\u3001viewBox\u3001containerSize/contentSize\u3001vf \u4E0E bitmapLookupCandidates\uFF0C\u907F\u514D\u7EE7\u7EED\u63A8\u65AD\u65E7\u9875\u9762\u8D44\u4EA7\u6216\u4F2A\u9020 SVG\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
+ measureOutputPath,
6522
6532
  restoreChecklist: [
6523
6533
  "\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",
6534
+ "\u5355\u4E2A\u56FE\u6807\u504F\u5DEE\u5148\u67E5 assets/icons/ \u4E2D\u8BBE\u8BA1\u7A3F\u540C\u540D *_icon@4x.png\uFF0C\u518D\u51B3\u5B9A\u662F\u5426\u8865\u5BFC",
6525
6535
  "preserve \u6A21\u5F0F SVG \u7981\u6B62\u8FFD\u52A0 ColorFilter",
6526
6536
  "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",
6537
+ "Sketch Image \u56FE\u5C42\u4F18\u5148\u590D\u7528 assets/icons/*_icon@4x.png\uFF0C\u4E0D\u8981\u4F2A\u88C5\u6210 SVG\uFF1B\u53EA\u6709\u65E7\u4F4D\u56FE\u6D41\u7A0B\u624D\u8865 2.0x/3.0x",
6528
6538
  "\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
6539
  "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
6540
  "\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 +6546,7 @@ async function sketchMeasure(args) {
6536
6546
  required: true,
6537
6547
  checks: ["contract-validator", "asset-source-validator", "route-review-validator"],
6538
6548
  exampleArgs: {
6539
- measurePath: "/path/to/sketch-compact-output.json",
6549
+ measurePath: measureOutputPath || "/path/to/sketch-compact-output.json",
6540
6550
  codeFilePath: "/path/to/generated_page.dart",
6541
6551
  routeFilePath: "/path/to/app_pages.dart",
6542
6552
  routeName: "AppRoutes.xxx",
@@ -6557,30 +6567,57 @@ function isRecord2(value) {
6557
6567
  return typeof value === "object" && value !== null && !Array.isArray(value);
6558
6568
  }
6559
6569
  function parseNestedJson(raw) {
6560
- let candidate = raw.trim();
6561
- for (let attempt = 0; attempt < 4; attempt += 1) {
6570
+ var _a;
6571
+ const queue = [raw.trim()];
6572
+ const seen = /* @__PURE__ */ new Set();
6573
+ let lastError = null;
6574
+ while (queue.length > 0) {
6575
+ const candidate = (_a = queue.shift()) == null ? void 0 : _a.trim();
6576
+ if (!candidate || seen.has(candidate)) {
6577
+ continue;
6578
+ }
6579
+ seen.add(candidate);
6562
6580
  try {
6563
6581
  const parsed = JSON.parse(candidate);
6564
6582
  if (typeof parsed === "string") {
6565
- candidate = parsed.trim();
6583
+ queue.push(parsed.trim());
6566
6584
  continue;
6567
6585
  }
6568
6586
  if (isRecord2(parsed) && Array.isArray(parsed.content) && parsed.content[0] && isRecord2(parsed.content[0])) {
6569
6587
  const text = parsed.content[0].text;
6570
6588
  if (typeof text === "string") {
6571
- candidate = text.trim();
6589
+ queue.push(text.trim());
6572
6590
  continue;
6573
6591
  }
6574
6592
  }
6575
6593
  return parsed;
6576
- } catch {
6577
- if (candidate.startsWith("'") && candidate.endsWith("'") || candidate.startsWith('"') && candidate.endsWith('"')) {
6578
- candidate = candidate.slice(1, -1);
6594
+ } catch (error) {
6595
+ lastError = error;
6596
+ }
6597
+ if (candidate.startsWith("'") && candidate.endsWith("'") || candidate.startsWith('"') && candidate.endsWith('"')) {
6598
+ queue.push(candidate.slice(1, -1));
6599
+ }
6600
+ const decoded = candidate.replace(/\\r/g, "\r").replace(/\\n/g, "\n").replace(/\\t/g, " ").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
6601
+ if (decoded !== candidate) {
6602
+ queue.push(decoded);
6603
+ }
6604
+ const firstBrace = candidate.indexOf("{");
6605
+ const lastBrace = candidate.lastIndexOf("}");
6606
+ if (firstBrace !== -1 && lastBrace > firstBrace) {
6607
+ const sliced = candidate.slice(firstBrace, lastBrace + 1);
6608
+ if (sliced !== candidate) {
6609
+ queue.push(sliced);
6610
+ }
6611
+ const decodedSliced = sliced.replace(/\\r/g, "\r").replace(/\\n/g, "\n").replace(/\\t/g, " ").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
6612
+ if (decodedSliced !== sliced) {
6613
+ queue.push(decodedSliced);
6579
6614
  }
6580
- candidate = candidate.replace(/\\n/g, "\n").replace(/\\"/g, '"').replace(/\\\\/g, "\\");
6581
6615
  }
6582
6616
  }
6583
- return JSON.parse(candidate);
6617
+ if (lastError instanceof Error) {
6618
+ throw lastError;
6619
+ }
6620
+ throw new Error("measure payload \u89E3\u6790\u5931\u8D25");
6584
6621
  }
6585
6622
  function loadMeasurePayload(args) {
6586
6623
  const raw = args.measureContent || (args.measurePath ? fs20.readFileSync(args.measurePath, "utf-8") : "");
@@ -6615,7 +6652,9 @@ function collectAssetContracts(node, bucket = []) {
6615
6652
  sketchName: node.n || "Image",
6616
6653
  fileName: typeof node.bitmapFile === "string" ? path20.posix.basename(node.bitmapFile) : typeof node.bitmapPath === "string" ? path20.posix.basename(node.bitmapPath) : void 0,
6617
6654
  sourceType: "bitmap",
6618
- scaleVariants: node.bitmapScales
6655
+ scaleVariants: node.bitmapScales,
6656
+ reuseMode: node.bitmapReuseMode,
6657
+ reuseKey: node.bitmapReuseKey
6619
6658
  });
6620
6659
  }
6621
6660
  for (const child of node.ch || []) {
@@ -6624,9 +6663,12 @@ function collectAssetContracts(node, bucket = []) {
6624
6663
  return bucket;
6625
6664
  }
6626
6665
  function extractCodeAssets(codeContent) {
6627
- const matches = codeContent.match(/assets\/[A-Za-z0-9_./-]+\.(svg|png|jpg|jpeg|webp)/g);
6666
+ const matches = codeContent.match(/assets\/[^'")]+\.(svg|png|jpg|jpeg|webp)/g);
6628
6667
  return matches ? [...new Set(matches)] : [];
6629
6668
  }
6669
+ function isManualExport4xBitmapAsset(assetPath) {
6670
+ return /_icon@4x\.(png|jpg|jpeg|webp)$/i.test(path20.posix.basename(assetPath));
6671
+ }
6630
6672
  function hasCodePropertyLiteral(codeContent, property, value) {
6631
6673
  const escapedValue = String(value).replace(".", "\\.");
6632
6674
  return new RegExp(`${property}\\s*:\\s*${escapedValue}(?:\\.0+)?\\b`).test(codeContent);
@@ -6747,17 +6789,17 @@ function buildContractValidator(payload, codeContent) {
6747
6789
  });
6748
6790
  }
6749
6791
  }
6750
- const needsSupportButton = blockOrder.some((block) => /customer service|support|客服/i.test(`${block.name || ""} ${block.label || ""}`));
6792
+ const needsSupportButton = blockOrder.some((block) => /customer service|support|客服|help[_ -]?button|帮助/i.test(`${block.name || ""} ${block.label || ""}`));
6751
6793
  if (needsSupportButton) {
6752
6794
  requiredBlocks += 1;
6753
- if (hasAnyPattern(codeContent, [/customer[_ ]service/i, /support/i, /recipient_customer_service_button/i, /客服/])) {
6795
+ if (hasAnyPattern(codeContent, [/customer[_ -]?service/i, /support/i, /help[_ -]?button/i, /recipient_customer_service_button/i, /客服/])) {
6754
6796
  matchedBlocks += 1;
6755
6797
  } else {
6756
6798
  findings.push({
6757
6799
  id: "missing_customer_service_block",
6758
6800
  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"
6801
+ message: "\u8BBE\u8BA1\u7A3F\u5B58\u5728 Help/Customer Service Button\uFF0C\u4F46\u4EE3\u7801\u4E2D\u672A\u68C0\u6D4B\u5230\u5BF9\u5E94\u5165\u53E3\u3002",
6802
+ 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
6803
  });
6762
6804
  }
6763
6805
  }
@@ -6854,26 +6896,50 @@ function buildAssetValidator(payload, codeContent, args) {
6854
6896
  findings.push({
6855
6897
  id: "bitmap_asset_source_check_required",
6856
6898
  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",
6899
+ 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
6900
  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"
6901
+ 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
6902
  });
6861
6903
  }
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
- });
6904
+ const sharedBitmapFiles = new Set(
6905
+ assetContracts.filter((item) => item.sourceType === "bitmap" && item.reuseMode === "shared-canonical" && item.fileName).map((item) => item.fileName.toLowerCase())
6906
+ );
6907
+ if (sharedBitmapFiles.size > 0) {
6908
+ const usedBitmapFiles = new Set(
6909
+ codeBitmapAssets.map((asset) => path20.posix.basename(asset).toLowerCase())
6910
+ );
6911
+ const missingSharedBitmapFiles = [...sharedBitmapFiles].filter((fileName) => !usedBitmapFiles.has(fileName));
6912
+ if (missingSharedBitmapFiles.length > 0) {
6913
+ findings.push({
6914
+ id: "shared_bitmap_asset_not_reused",
6915
+ severity: "warning",
6916
+ 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",
6917
+ evidence: missingSharedBitmapFiles,
6918
+ 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"
6919
+ });
6920
+ }
6921
+ }
6922
+ const requiresPageLocalBitmap = assetContracts.some(
6923
+ (item) => item.sourceType === "bitmap" && item.reuseMode !== "shared-canonical"
6924
+ );
6925
+ if (expectedBitmapCount > 0 && codeBitmapAssets.length > 0 && requiresPageLocalBitmap) {
6926
+ const hasManualExport4xBitmap = codeBitmapAssets.some((asset) => isManualExport4xBitmapAsset(asset));
6927
+ if (!hasManualExport4xBitmap) {
6928
+ const pageClassName = extractPageClassName(codeContent) || "";
6929
+ const normalizedStem = pageClassName.replace(/Page$/, "").replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
6930
+ if (normalizedStem) {
6931
+ const hasPageLocalBitmap = codeBitmapAssets.some(
6932
+ (asset) => path20.posix.basename(asset).toLowerCase().includes(normalizedStem)
6933
+ );
6934
+ if (!hasPageLocalBitmap) {
6935
+ findings.push({
6936
+ id: "bitmap_assets_not_page_local",
6937
+ severity: "warning",
6938
+ 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",
6939
+ evidence: codeBitmapAssets,
6940
+ 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"
6941
+ });
6942
+ }
6877
6943
  }
6878
6944
  }
6879
6945
  }
@@ -6885,6 +6951,9 @@ function buildAssetValidator(payload, codeContent, args) {
6885
6951
  if (requiresRetinaVariants) {
6886
6952
  const missingRetinaEvidence = [];
6887
6953
  for (const asset of codeBitmapAssets) {
6954
+ if (isManualExport4xBitmapAsset(asset)) {
6955
+ continue;
6956
+ }
6888
6957
  const assetOnDisk = resolveProjectAssetPath(args.projectPath, asset);
6889
6958
  if (!fs20.existsSync(assetOnDisk)) {
6890
6959
  missingRetinaEvidence.push(`${asset} (base missing)`);
@@ -6900,9 +6969,9 @@ function buildAssetValidator(payload, codeContent, args) {
6900
6969
  findings.push({
6901
6970
  id: "bitmap_retina_variants_missing",
6902
6971
  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",
6972
+ 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
6973
  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"
6974
+ 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
6975
  });
6907
6976
  }
6908
6977
  }