sketch-design-mcp 3.0.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.
package/dist/index.js ADDED
@@ -0,0 +1,2642 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { readFileSync as readFileSync3 } from "fs";
5
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
+ import { z } from "zod";
8
+
9
+ // src/engine/core.ts
10
+ import { createHash } from "crypto";
11
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
12
+ import { tmpdir } from "os";
13
+ import { join } from "path";
14
+
15
+ // src/rules.ts
16
+ import { existsSync, readFileSync } from "fs";
17
+ var SHADOW_ZH_RULE_SET = {
18
+ noiseNumberFonts: ["DIN Alternate"],
19
+ annotationNamePrefixes: ["0.\u4EA4\u4E92", "0.\u4EA4\u4E92\u6807\u8BB0"],
20
+ annotationFontIncludes: ["FZLanTingHeiS"],
21
+ anchorNamePrefixes: ["0.\u4EA4\u4E92\u6807\u8BB0"],
22
+ specFontIncludes: ["FZLanTingHeiS"],
23
+ marginNumberedItemPattern: "^[\\d\u2460\u2461\u2462\u2463\u2464\u2465\u2466\u2467\u2468\u2469\u4E00\u4E8C\u4E09\u56DB\u4E94\u516D\u4E03\u516B\u4E5D\u5341]+[.\u3001\\s\uFF09)]",
24
+ marginSpecPatterns: [
25
+ "^\u652F\u6301",
26
+ "^\u4E0D\u652F\u6301",
27
+ "^\u672C\u671F\u4EC5",
28
+ "^\u9ED8\u8BA4",
29
+ "^\u7EC4\u4EF6\u5E93",
30
+ "\u70B9\u51FB\u540E",
31
+ "\u5F39\u7A97\u9AD8\u5EA6",
32
+ "\u52A8\u6001\u9002\u914D",
33
+ "\u5185\u5BB9\u4E3A\u540E\u7AEF",
34
+ "\u5EFA\u8BAE\u63A7\u5236",
35
+ "\u60AC\u6D6E\u4E0D\u5C55\u793A",
36
+ "\u8D85\u51FA\u663E\u793A",
37
+ "\u5173\u952E\u8BCD\u6A21\u7CCA",
38
+ "\u65B0\u5F00\u9875\u9762",
39
+ "\u6D4B\u8BD5\u73AF\u5883",
40
+ "^\u9009\u586B",
41
+ "^\u5FC5\u586B",
42
+ "\u7B49\u957F\u6587\u672C"
43
+ ],
44
+ interactionStepPatterns: {
45
+ trigger: [
46
+ "\u70B9\u51FB",
47
+ "\u5355\u51FB",
48
+ "\u5207\u6362",
49
+ "\u9009\u62E9",
50
+ "\u8F93\u5165",
51
+ "\u641C\u7D22",
52
+ "hover",
53
+ "\u60AC\u6D6E",
54
+ "\u5C55\u5F00",
55
+ "\u6536\u8D77",
56
+ "\u62D6\u62FD",
57
+ "\u63D0\u4EA4",
58
+ "\u5220\u9664",
59
+ "\u65B0\u589E"
60
+ ],
61
+ condition: [
62
+ "\u5982\u679C",
63
+ "\u82E5",
64
+ "\u5F53",
65
+ "\u4EC5",
66
+ "\u9ED8\u8BA4",
67
+ "\u4E3A\u7A7A",
68
+ "\u65E0\u6743\u9650",
69
+ "\u5DF2",
70
+ "\u672A",
71
+ "\u6210\u529F",
72
+ "\u5931\u8D25",
73
+ "\u8D85\u8FC7",
74
+ "\u5927\u4E8E",
75
+ "\u5C0F\u4E8E",
76
+ "\u5FC5\u586B",
77
+ "\u9009\u586B"
78
+ ],
79
+ action: [
80
+ "\u5C55\u793A",
81
+ "\u663E\u793A",
82
+ "\u9690\u85CF",
83
+ "\u5237\u65B0",
84
+ "\u66F4\u65B0",
85
+ "\u8DF3\u8F6C",
86
+ "\u6253\u5F00",
87
+ "\u5173\u95ED",
88
+ "\u8BF7\u6C42",
89
+ "\u56DE\u663E",
90
+ "\u8FC7\u6EE4",
91
+ "\u6392\u5E8F",
92
+ "\u5206\u9875",
93
+ "\u6821\u9A8C",
94
+ "\u63D0\u793A",
95
+ "\u5F39\u7A97",
96
+ "\u4E0B\u8F7D",
97
+ "\u4E0A\u4F20",
98
+ "\u6267\u884C",
99
+ "\u521B\u5EFA",
100
+ "\u4FDD\u5B58"
101
+ ]
102
+ }
103
+ };
104
+ var GENERIC_RULE_SET = {
105
+ noiseNumberFonts: ["DIN Alternate", "DIN"],
106
+ annotationNamePrefixes: [
107
+ "0.\u4EA4\u4E92",
108
+ "0.\u4EA4\u4E92\u6807\u8BB0",
109
+ "interaction",
110
+ "annot",
111
+ "annotation",
112
+ "hotspot",
113
+ "flow"
114
+ ],
115
+ annotationFontIncludes: ["FZLanTingHeiS", "Annotation", "Comment"],
116
+ anchorNamePrefixes: ["0.\u4EA4\u4E92\u6807\u8BB0", "interaction", "hotspot", "anchor", "flow"],
117
+ specFontIncludes: ["FZLanTingHeiS", "Annotation", "Comment"],
118
+ marginNumberedItemPattern: "^[\\d\u2460\u2461\u2462\u2463\u2464\u2465\u2466\u2467\u2468\u2469\u4E00\u4E8C\u4E09\u56DB\u4E94\u516D\u4E03\u516B\u4E5D\u5341ivxIVX]+[.\u3001\\s\uFF09)]",
119
+ marginSpecPatterns: [
120
+ "^\u652F\u6301",
121
+ "^\u4E0D\u652F\u6301",
122
+ "^\u672C\u671F\u4EC5",
123
+ "^\u9ED8\u8BA4",
124
+ "^\u7EC4\u4EF6\u5E93",
125
+ "\u70B9\u51FB\u540E",
126
+ "\u5F39\u7A97\u9AD8\u5EA6",
127
+ "\u52A8\u6001\u9002\u914D",
128
+ "\u5185\u5BB9\u4E3A\u540E\u7AEF",
129
+ "\u5EFA\u8BAE\u63A7\u5236",
130
+ "\u60AC\u6D6E\u4E0D\u5C55\u793A",
131
+ "\u8D85\u51FA\u663E\u793A",
132
+ "\u5173\u952E\u8BCD\u6A21\u7CCA",
133
+ "\u65B0\u5F00\u9875\u9762",
134
+ "\u6D4B\u8BD5\u73AF\u5883",
135
+ "^\u9009\u586B",
136
+ "^\u5FC5\u586B",
137
+ "\u7B49\u957F\u6587\u672C",
138
+ "^support",
139
+ "^does not support",
140
+ "^default",
141
+ "^optional",
142
+ "^required",
143
+ "after click",
144
+ "click to",
145
+ "modal",
146
+ "dialog",
147
+ "dynamic",
148
+ "backend",
149
+ "open in new",
150
+ "test env",
151
+ "ellipsis"
152
+ ],
153
+ interactionStepPatterns: {
154
+ trigger: [
155
+ "\u70B9\u51FB",
156
+ "\u5355\u51FB",
157
+ "\u5207\u6362",
158
+ "\u9009\u62E9",
159
+ "\u8F93\u5165",
160
+ "\u641C\u7D22",
161
+ "hover",
162
+ "\u60AC\u6D6E",
163
+ "\u5C55\u5F00",
164
+ "\u6536\u8D77",
165
+ "\u62D6\u62FD",
166
+ "\u63D0\u4EA4",
167
+ "\u5220\u9664",
168
+ "\u65B0\u589E",
169
+ "click",
170
+ "tap",
171
+ "toggle",
172
+ "type",
173
+ "input",
174
+ "search",
175
+ "select",
176
+ "submit",
177
+ "remove",
178
+ "create",
179
+ "open",
180
+ "close"
181
+ ],
182
+ condition: [
183
+ "\u5982\u679C",
184
+ "\u82E5",
185
+ "\u5F53",
186
+ "\u4EC5",
187
+ "\u9ED8\u8BA4",
188
+ "\u4E3A\u7A7A",
189
+ "\u65E0\u6743\u9650",
190
+ "\u5DF2",
191
+ "\u672A",
192
+ "\u6210\u529F",
193
+ "\u5931\u8D25",
194
+ "\u8D85\u8FC7",
195
+ "\u5927\u4E8E",
196
+ "\u5C0F\u4E8E",
197
+ "\u5FC5\u586B",
198
+ "\u9009\u586B",
199
+ "if",
200
+ "when",
201
+ "only",
202
+ "default",
203
+ "empty",
204
+ "without permission",
205
+ "with permission",
206
+ "success",
207
+ "failed",
208
+ "greater than",
209
+ "less than",
210
+ "required",
211
+ "optional"
212
+ ],
213
+ action: [
214
+ "\u5C55\u793A",
215
+ "\u663E\u793A",
216
+ "\u9690\u85CF",
217
+ "\u5237\u65B0",
218
+ "\u66F4\u65B0",
219
+ "\u8DF3\u8F6C",
220
+ "\u6253\u5F00",
221
+ "\u5173\u95ED",
222
+ "\u8BF7\u6C42",
223
+ "\u56DE\u663E",
224
+ "\u8FC7\u6EE4",
225
+ "\u6392\u5E8F",
226
+ "\u5206\u9875",
227
+ "\u6821\u9A8C",
228
+ "\u63D0\u793A",
229
+ "\u5F39\u7A97",
230
+ "\u4E0B\u8F7D",
231
+ "\u4E0A\u4F20",
232
+ "\u6267\u884C",
233
+ "\u521B\u5EFA",
234
+ "\u4FDD\u5B58",
235
+ "show",
236
+ "hide",
237
+ "refresh",
238
+ "update",
239
+ "navigate",
240
+ "redirect",
241
+ "request",
242
+ "render",
243
+ "filter",
244
+ "sort",
245
+ "paginate",
246
+ "validate",
247
+ "toast",
248
+ "download",
249
+ "upload",
250
+ "execute",
251
+ "save"
252
+ ]
253
+ }
254
+ };
255
+ var BUILTIN_RULESETS = {
256
+ generic: GENERIC_RULE_SET,
257
+ shadow_zh: SHADOW_ZH_RULE_SET
258
+ };
259
+ function toArray(v) {
260
+ if (!Array.isArray(v)) return void 0;
261
+ const items = v.filter((x) => typeof x === "string").map((x) => x.trim());
262
+ return items.length > 0 ? items : [];
263
+ }
264
+ function toStringOrUndefined(v) {
265
+ if (typeof v !== "string") return void 0;
266
+ const s = v.trim();
267
+ return s.length > 0 ? s : "";
268
+ }
269
+ function mergeRuleSet(base, patch) {
270
+ return {
271
+ noiseNumberFonts: toArray(patch.noiseNumberFonts) ?? base.noiseNumberFonts,
272
+ annotationNamePrefixes: toArray(patch.annotationNamePrefixes) ?? base.annotationNamePrefixes,
273
+ annotationFontIncludes: toArray(patch.annotationFontIncludes) ?? base.annotationFontIncludes,
274
+ anchorNamePrefixes: toArray(patch.anchorNamePrefixes) ?? base.anchorNamePrefixes,
275
+ specFontIncludes: toArray(patch.specFontIncludes) ?? base.specFontIncludes,
276
+ marginNumberedItemPattern: toStringOrUndefined(patch.marginNumberedItemPattern) ?? base.marginNumberedItemPattern,
277
+ marginSpecPatterns: toArray(patch.marginSpecPatterns) ?? base.marginSpecPatterns,
278
+ interactionStepPatterns: {
279
+ trigger: toArray(patch.interactionStepPatterns?.trigger) ?? base.interactionStepPatterns.trigger,
280
+ condition: toArray(patch.interactionStepPatterns?.condition) ?? base.interactionStepPatterns.condition,
281
+ action: toArray(patch.interactionStepPatterns?.action) ?? base.interactionStepPatterns.action
282
+ }
283
+ };
284
+ }
285
+ function buildKeywordRegex(keywords) {
286
+ const escaped = keywords.map((k) => k.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
287
+ if (escaped.length === 0) return /^$/;
288
+ return new RegExp(escaped.join("|"), "i");
289
+ }
290
+ function compileRules(raw) {
291
+ return {
292
+ raw,
293
+ profileName: "custom",
294
+ marginNumberedItemRegex: new RegExp(raw.marginNumberedItemPattern),
295
+ marginSpecRegexes: raw.marginSpecPatterns.map((s) => new RegExp(s)),
296
+ interactionTriggerRegex: buildKeywordRegex(raw.interactionStepPatterns.trigger),
297
+ interactionConditionRegex: buildKeywordRegex(raw.interactionStepPatterns.condition),
298
+ interactionActionRegex: buildKeywordRegex(raw.interactionStepPatterns.action)
299
+ };
300
+ }
301
+ function loadPatchFromEnv() {
302
+ const path = process.env.SKETCH_MCP_RULESET_PATH;
303
+ if (!path) return {};
304
+ if (!existsSync(path)) {
305
+ throw new Error(`SKETCH_MCP_RULESET_PATH \u6587\u4EF6\u4E0D\u5B58\u5728: ${path}`);
306
+ }
307
+ const raw = readFileSync(path, "utf-8");
308
+ const parsed = JSON.parse(raw);
309
+ return parsed ?? {};
310
+ }
311
+ var cachedPath;
312
+ var cachedProfileKey;
313
+ var cachedRules = null;
314
+ var runtimeProfileOverride = null;
315
+ var runtimeProfileConfidence = 0.5;
316
+ function getBaseRuleSet(profile) {
317
+ return BUILTIN_RULESETS[profile] ?? BUILTIN_RULESETS.shadow_zh;
318
+ }
319
+ function containsCJK(text) {
320
+ return /[\u4e00-\u9fff]/.test(text);
321
+ }
322
+ function normalizeForProfileCompare(value) {
323
+ return value.trim().toLowerCase();
324
+ }
325
+ function detectRuleProfile(layers) {
326
+ let zhScore = 0;
327
+ let genericScore = 0;
328
+ const sample = layers.slice(0, 1800);
329
+ for (const l of sample) {
330
+ const name = normalizeForProfileCompare(l.name ?? "");
331
+ const ff = normalizeForProfileCompare(l.fontFace ?? l.fontFamily ?? "");
332
+ const content = normalizeForProfileCompare(l.content ?? "");
333
+ if (name.startsWith("0.\u4EA4\u4E92\u6807\u8BB0") || name.startsWith("0.\u4EA4\u4E92")) zhScore += 4;
334
+ if (ff.includes("fzlantingheis")) zhScore += 3;
335
+ if (ff.includes("din alternate") && /^\d+$/.test(content)) zhScore += 1;
336
+ if (containsCJK(content) || containsCJK(name)) zhScore += 0.4;
337
+ if (name.includes("interaction") || name.includes("annot") || name.includes("hotspot") || name.includes("flow")) {
338
+ genericScore += 2;
339
+ }
340
+ if (/\b(click|tap|hover|select|input|search)\b/.test(content)) genericScore += 1;
341
+ }
342
+ const profile = zhScore >= 6 && zhScore >= genericScore * 1.2 ? "shadow_zh" : "generic";
343
+ const maxScore = Math.max(zhScore, genericScore, 1);
344
+ const delta = Math.abs(zhScore - genericScore);
345
+ const confidence = Math.max(
346
+ 0.5,
347
+ Math.min(0.99, 0.5 + delta / (maxScore + 6) * 0.49)
348
+ );
349
+ return {
350
+ profile,
351
+ confidence: Number(confidence.toFixed(2)),
352
+ zhScore: Number(zhScore.toFixed(2)),
353
+ genericScore: Number(genericScore.toFixed(2))
354
+ };
355
+ }
356
+ function applyAutoRuleProfile(layers) {
357
+ const detection = detectRuleProfile(layers);
358
+ runtimeProfileOverride = detection.profile;
359
+ runtimeProfileConfidence = detection.confidence;
360
+ return detection;
361
+ }
362
+ function getEffectiveRuleProfileName() {
363
+ return process.env.SKETCH_MCP_RULESET_PROFILE ?? runtimeProfileOverride ?? "shadow_zh";
364
+ }
365
+ function getRuleRuntimeMeta() {
366
+ const forced = process.env.SKETCH_MCP_RULESET_PROFILE;
367
+ if (forced) {
368
+ const resolved = BUILTIN_RULESETS[forced] ? forced : "shadow_zh";
369
+ return {
370
+ profile: resolved,
371
+ confidence: 1,
372
+ source: "forced_env"
373
+ };
374
+ }
375
+ if (runtimeProfileOverride) {
376
+ return {
377
+ profile: runtimeProfileOverride,
378
+ confidence: runtimeProfileConfidence,
379
+ source: "auto"
380
+ };
381
+ }
382
+ return {
383
+ profile: "shadow_zh",
384
+ confidence: 0.5,
385
+ source: "default"
386
+ };
387
+ }
388
+ function getCompiledRules() {
389
+ const profile = getEffectiveRuleProfileName();
390
+ const path = process.env.SKETCH_MCP_RULESET_PATH;
391
+ if (cachedRules && cachedPath === path && cachedProfileKey === profile) return cachedRules;
392
+ const patch = loadPatchFromEnv();
393
+ const merged = mergeRuleSet(getBaseRuleSet(profile), patch);
394
+ cachedPath = path;
395
+ cachedProfileKey = profile;
396
+ cachedRules = {
397
+ ...compileRules(merged),
398
+ profileName: profile
399
+ };
400
+ return cachedRules;
401
+ }
402
+ function startsWithAny(value, prefixes) {
403
+ const left = normalizeForProfileCompare(value);
404
+ return prefixes.some((p) => {
405
+ const right = normalizeForProfileCompare(p);
406
+ return right.length > 0 && left.startsWith(right);
407
+ });
408
+ }
409
+ function includesAny(value, fragments) {
410
+ const left = normalizeForProfileCompare(value);
411
+ return fragments.some((f) => {
412
+ const right = normalizeForProfileCompare(f);
413
+ return right.length > 0 && left.includes(right);
414
+ });
415
+ }
416
+
417
+ // src/engine/core.ts
418
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
419
+ var CACHE_DIR = join(tmpdir(), "sketch_design_cache");
420
+ mkdirSync(CACHE_DIR, { recursive: true });
421
+ function cachePath(url) {
422
+ const h = createHash("md5").update(url).digest("hex");
423
+ return join(CACHE_DIR, `${h}.json`);
424
+ }
425
+ async function download(url) {
426
+ const cp = cachePath(url);
427
+ if (existsSync2(cp)) {
428
+ return readFileSync2(cp, "utf-8");
429
+ }
430
+ const resp = await fetch(url, {
431
+ signal: AbortSignal.timeout(3e4)
432
+ });
433
+ if (!resp.ok) {
434
+ throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
435
+ }
436
+ const html = await resp.text();
437
+ writeFileSync(cp, html, "utf-8");
438
+ return html;
439
+ }
440
+ function parseWudiApp(html) {
441
+ const marker = 'WudiApp({"';
442
+ const pos = html.lastIndexOf(marker);
443
+ if (pos === -1) {
444
+ throw new Error("\u672A\u627E\u5230 WudiApp \u6570\u636E\uFF0C\u8BF7\u68C0\u67E5 URL \u662F\u5426\u6B63\u786E");
445
+ }
446
+ const start = pos + "WudiApp(".length;
447
+ let depth = 0;
448
+ let i = start;
449
+ while (i < html.length) {
450
+ const c = html[i];
451
+ if (c === "\\" && i + 1 < html.length) {
452
+ i += 2;
453
+ continue;
454
+ }
455
+ if (c === '"') {
456
+ let j = i + 1;
457
+ while (j < html.length) {
458
+ if (html[j] === "\\" && j + 1 < html.length) {
459
+ j += 2;
460
+ continue;
461
+ }
462
+ if (html[j] === '"') break;
463
+ j++;
464
+ }
465
+ i = j + 1;
466
+ continue;
467
+ }
468
+ if (c === "{") depth++;
469
+ else if (c === "}") {
470
+ depth--;
471
+ if (depth === 0) break;
472
+ }
473
+ i++;
474
+ }
475
+ const jsonStr = html.substring(start, i + 1);
476
+ return JSON.parse(jsonStr);
477
+ }
478
+ var wudiCache = /* @__PURE__ */ new Map();
479
+ var htmlOverride = null;
480
+ function setHtmlOverride(filePath) {
481
+ htmlOverride = filePath;
482
+ }
483
+ async function getWudi(url) {
484
+ const cached = wudiCache.get(url);
485
+ if (cached) return cached;
486
+ const html = htmlOverride ? readFileSync2(htmlOverride, "utf-8") : await download(url);
487
+ const wudi = parseWudiApp(html);
488
+ const profileProbe = [...wudi.artboards].sort((a, b) => b.layers.length - a.layers.length)[0];
489
+ if (profileProbe) {
490
+ applyAutoRuleProfile(profileProbe.layers);
491
+ }
492
+ wudiCache.set(url, wudi);
493
+ return wudi;
494
+ }
495
+ function findArtboard(wudi, name = "") {
496
+ const query = (name ?? "").trim();
497
+ if (!query) {
498
+ const first = wudi.artboards[0];
499
+ if (!first) throw new Error("\u5F53\u524D\u9875\u9762\u6CA1\u6709\u53EF\u7528\u7684 artboard");
500
+ applyAutoRuleProfile(first.layers);
501
+ return first;
502
+ }
503
+ for (const ab of wudi.artboards) {
504
+ if (ab.name.includes(query) || query.includes(ab.name)) {
505
+ applyAutoRuleProfile(ab.layers);
506
+ return ab;
507
+ }
508
+ }
509
+ for (const ab of wudi.artboards) {
510
+ if (ab.slug && ab.slug.includes(query)) {
511
+ applyAutoRuleProfile(ab.layers);
512
+ return ab;
513
+ }
514
+ }
515
+ const available = wudi.artboards.map((a) => a.name);
516
+ throw new Error(`\u672A\u627E\u5230 artboard: ${query}\uFF0C\u53EF\u7528: ${JSON.stringify(available)}`);
517
+ }
518
+
519
+ // src/aggregator/base.ts
520
+ function rect(l) {
521
+ const r = l.rect;
522
+ if (!r) return { x: 0, y: 0, width: 0, height: 0 };
523
+ return r;
524
+ }
525
+ function colorHex(colorDict) {
526
+ if (!colorDict) return "";
527
+ const val = colorDict["color-hex"] ?? "";
528
+ return val ? val.split(" ")[0] : "";
529
+ }
530
+ function bgColor(l) {
531
+ const fills = l.fills ?? [];
532
+ for (const fill of fills) {
533
+ const c = colorHex(fill.color);
534
+ if (c && c !== "#FFFFFF") return c;
535
+ }
536
+ for (const fill of fills) {
537
+ const c = colorHex(fill.color);
538
+ if (c) return c;
539
+ }
540
+ return "";
541
+ }
542
+ function borderInfo(l) {
543
+ const borders = l.borders ?? [];
544
+ if (borders.length === 0) return null;
545
+ const b = borders[0];
546
+ return {
547
+ color: colorHex(b.color),
548
+ width: b.thickness,
549
+ style: "solid"
550
+ };
551
+ }
552
+ function radius(l) {
553
+ const r = l.radius;
554
+ if (Array.isArray(r) && r.length > 0) return Math.floor(r[0]);
555
+ if (typeof r === "number") return Math.floor(r);
556
+ return 0;
557
+ }
558
+ function shadowInfo(l) {
559
+ const shadows = l.shadows ?? [];
560
+ if (shadows.length === 0) return null;
561
+ const s = shadows[0];
562
+ return {
563
+ color: colorHex(s.color),
564
+ x: s.x,
565
+ y: s.y,
566
+ blur: s.blur,
567
+ spread: s.spread
568
+ };
569
+ }
570
+
571
+ // src/aggregator/color.ts
572
+ function hexToRgb(hex) {
573
+ const h = hex.replace("#", "");
574
+ return [
575
+ parseInt(h.substring(0, 2), 16),
576
+ parseInt(h.substring(2, 4), 16),
577
+ parseInt(h.substring(4, 6), 16)
578
+ ];
579
+ }
580
+ function luminance(hex) {
581
+ if (!hex || !hex.startsWith("#") || hex.length < 7) return -1;
582
+ const [r, g, b] = hexToRgb(hex);
583
+ const [rs, gs, bs] = [r / 255, g / 255, b / 255].map(
584
+ (c) => c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)
585
+ );
586
+ return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
587
+ }
588
+ function isWhite(hex) {
589
+ if (!hex) return false;
590
+ const h = hex.toUpperCase();
591
+ return h === "#FFFFFF" || h === "#FFF" || luminance(h) > 0.95;
592
+ }
593
+
594
+ // src/aggregator/filters.ts
595
+ function isNoise(l) {
596
+ const rules = getCompiledRules();
597
+ const name = l.name ?? "";
598
+ if (rules.raw.noiseNumberFonts.includes(l.fontFace ?? "")) {
599
+ const hex = l.color?.["color-hex"] ?? "";
600
+ if (isWhite(hex.replace(/ .*/, ""))) return true;
601
+ }
602
+ if (/^\d+$/.test(name)) return true;
603
+ return false;
604
+ }
605
+ function isAnnotation(l) {
606
+ const rules = getCompiledRules();
607
+ const name = l.name ?? "";
608
+ if (startsWithAny(name, rules.raw.annotationNamePrefixes)) return true;
609
+ if (includesAny(l.fontFace ?? "", rules.raw.annotationFontIncludes)) return true;
610
+ return false;
611
+ }
612
+ function detectDesignViewportWidth(layers) {
613
+ if (!layers || layers.length < 20) return 1440;
614
+ const rightEdges = [];
615
+ for (const l of layers) {
616
+ if (isNoise(l) || isAnnotation(l)) continue;
617
+ if (l.type !== "shape" && l.type !== "symbol" && l.type !== "group") continue;
618
+ const r = rect(l);
619
+ const w = r.width;
620
+ const h = r.height;
621
+ if (w >= 300 && h >= 100 && w <= 2e3 && h <= 5e3) {
622
+ rightEdges.push(Math.round(r.x + w));
623
+ }
624
+ }
625
+ if (rightEdges.length < 3) return 1440;
626
+ rightEdges.sort((a, b) => a - b);
627
+ let bestCenter = 1440;
628
+ let bestCount = 0;
629
+ for (let i = 0; i < rightEdges.length; i++) {
630
+ const center = rightEdges[i];
631
+ let count = 0;
632
+ for (let j = 0; j < rightEdges.length; j++) {
633
+ if (Math.abs(rightEdges[j] - center) <= 25) count++;
634
+ }
635
+ if (count > bestCount) {
636
+ bestCount = count;
637
+ bestCenter = center;
638
+ }
639
+ }
640
+ if (bestCount < rightEdges.length * 0.3) return 1440;
641
+ return Math.round(bestCenter / 10) * 10;
642
+ }
643
+ function isMarginSpecText(l, designWidth) {
644
+ const rules = getCompiledRules();
645
+ if (l.type !== "text") return false;
646
+ const r = rect(l);
647
+ if (r.x < designWidth) return false;
648
+ const content = (l.content ?? "").trim();
649
+ if (!content) return false;
650
+ if (rules.marginNumberedItemRegex.test(content)) {
651
+ return true;
652
+ }
653
+ for (const p of rules.marginSpecRegexes) {
654
+ if (p.test(content)) return true;
655
+ }
656
+ return false;
657
+ }
658
+
659
+ // src/v3/manifest.ts
660
+ function scoreRecommended(name, width, height, layers) {
661
+ const normalized = name.toLowerCase();
662
+ let score = 0;
663
+ if (width >= 1200 && height >= 900) score += 0.5;
664
+ if (layers >= 80) score += Math.min(0.35, layers / 2200);
665
+ if (/工作空间|应用|知识库|评测|管理|列表|页面|主界面|问答|结果|发送|导航|菜单|配置|设置|编辑|创建|详情|消息|对话|聊天|参数|切片|prompt|检索|知识/i.test(
666
+ normalized
667
+ )) {
668
+ score += 0.5;
669
+ }
670
+ if (/封面|修订记录|说明|示例|规范|目录/.test(normalized)) score -= 0.4;
671
+ return score;
672
+ }
673
+ function buildArtboardManifest(artboards) {
674
+ return artboards.map((ab) => {
675
+ let textLayers = 0;
676
+ let shapeLikeLayers = 0;
677
+ for (const l of ab.layers) {
678
+ if (l.type === "text") textLayers++;
679
+ if (l.type === "shape" || l.type === "symbol" || l.type === "group") {
680
+ shapeLikeLayers++;
681
+ }
682
+ }
683
+ const designWidth = detectDesignViewportWidth(ab.layers);
684
+ const complexityScore = Number(
685
+ (ab.layers.length * 2e-3 + Math.max(0, ab.height / 1800) + Math.max(0, shapeLikeLayers / 500) + Math.max(0, textLayers / 300)).toFixed(2)
686
+ );
687
+ const recommended = scoreRecommended(ab.name, ab.width, ab.height, ab.layers.length) >= 0.9;
688
+ return {
689
+ name: ab.name,
690
+ slug: ab.slug,
691
+ width: ab.width,
692
+ height: ab.height,
693
+ totalLayers: ab.layers.length,
694
+ textLayers,
695
+ shapeLikeLayers,
696
+ designWidth,
697
+ complexityScore,
698
+ recommended
699
+ };
700
+ }).sort(
701
+ (a, b) => Number(b.recommended) - Number(a.recommended) || b.complexityScore - a.complexityScore
702
+ );
703
+ }
704
+
705
+ // src/v3/constants.ts
706
+ var DEFAULT_MAX_NODES = 900;
707
+ var DEFAULT_MAX_COMPONENTS = 500;
708
+
709
+ // src/v3/scene.ts
710
+ function area(r) {
711
+ return Math.max(0, r.width) * Math.max(0, r.height);
712
+ }
713
+ function inRegion(r, yMin, yMax) {
714
+ const top = r.y;
715
+ const bottom = r.y + r.height;
716
+ return bottom >= yMin - 40 && top <= yMax + 40;
717
+ }
718
+ function withRegionDefaults(artboard, options) {
719
+ const yMin = options?.pageY ?? 0;
720
+ const pageHeight = options?.pageHeight ?? Math.max(artboard.height, 2e3);
721
+ const yMax = yMin + pageHeight;
722
+ return { yMin, yMax };
723
+ }
724
+ function guessKind(rawType) {
725
+ if (rawType === "text") return "text";
726
+ if (rawType === "shape") return "shape";
727
+ if (rawType === "symbol") return "symbol";
728
+ if (rawType === "group") return "group";
729
+ return "unknown";
730
+ }
731
+ function containsRect(outer, inner, padding = 2) {
732
+ return outer.x - padding <= inner.x && outer.y - padding <= inner.y && inner.x + inner.width <= outer.x + outer.width + padding && inner.y + inner.height <= outer.y + outer.height + padding;
733
+ }
734
+ function estimateConstraints(parent, child) {
735
+ const left = Math.round(child.x - parent.x);
736
+ const right = Math.round(parent.x + parent.width - (child.x + child.width));
737
+ const top = Math.round(child.y - parent.y);
738
+ const bottom = Math.round(parent.y + parent.height - (child.y + child.height));
739
+ let horizontal = "free";
740
+ let vertical = "free";
741
+ if (left <= 8 && right <= 8) horizontal = "stretch";
742
+ else if (Math.abs(left - right) <= 6) horizontal = "center";
743
+ else if (left <= 12) horizontal = "pin_left";
744
+ else if (right <= 12) horizontal = "pin_right";
745
+ if (top <= 8 && bottom <= 8) vertical = "stretch";
746
+ else if (Math.abs(top - bottom) <= 6) vertical = "center";
747
+ else if (top <= 12) vertical = "pin_top";
748
+ else if (bottom <= 12) vertical = "pin_bottom";
749
+ return { horizontal, vertical, left, right, top, bottom };
750
+ }
751
+ function normalizeLayer(layer, index) {
752
+ const layerRect = rect(layer);
753
+ const kind = guessKind(layer.type ?? "unknown");
754
+ const fontFace = layer.fontFace ?? layer.fontFamily ?? "";
755
+ const fontWeight = /Medium|Bold|Semibold|Heavy|Black/i.test(fontFace) ? "Medium" : "Regular";
756
+ const style = {
757
+ bg: bgColor(layer) || void 0,
758
+ border: borderInfo(layer),
759
+ radius: radius(layer),
760
+ shadow: shadowInfo(layer),
761
+ opacity: layer.opacity ?? 1,
762
+ textColor: colorHex(layer.color) || void 0,
763
+ fontSize: layer.fontSize,
764
+ fontFamily: fontFace || void 0,
765
+ fontWeight,
766
+ lineHeight: layer.lineHeight,
767
+ letterSpacing: layer.letterSpacing,
768
+ textAlign: layer.textAlign
769
+ };
770
+ return {
771
+ id: `n${index + 1}`,
772
+ rawIndex: index,
773
+ rawType: layer.type ?? "unknown",
774
+ name: layer.name ?? "",
775
+ kind,
776
+ rect: layerRect,
777
+ style,
778
+ text: layer.type === "text" ? (layer.content ?? "").trim() : void 0,
779
+ zIndex: index
780
+ };
781
+ }
782
+ function isThinSeparator(r) {
783
+ return r.width >= 48 && r.height <= 3 || r.height >= 48 && r.width <= 3;
784
+ }
785
+ function median(values, fallback) {
786
+ const sorted = values.filter((v) => Number.isFinite(v) && v > 0).sort((a, b) => a - b);
787
+ if (sorted.length === 0) return fallback;
788
+ const mid = Math.floor(sorted.length / 2);
789
+ return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
790
+ }
791
+ function normalizeLabel(text) {
792
+ return (text ?? "").replace(/\s+/g, " ").trim();
793
+ }
794
+ function isMeaningfulSectionLabel(text) {
795
+ const value = normalizeLabel(text);
796
+ if (value.length < 2) return false;
797
+ if (/^[\d\s.、()::-]+$/.test(value)) return false;
798
+ if (/^[-–—·•|/\\]+$/.test(value)) return false;
799
+ return true;
800
+ }
801
+ function isStrongFontWeight(weight) {
802
+ return /Medium|Semibold|Demi|Bold|Heavy|Black/i.test(weight ?? "");
803
+ }
804
+ function isSectionHeadingSeed(node, medianFontSize) {
805
+ if (node.kind !== "text" || !isMeaningfulSectionLabel(node.text)) return false;
806
+ const fs = node.style.fontSize ?? medianFontSize;
807
+ const threshold = Math.max(14, medianFontSize * 1.14);
808
+ return fs >= threshold || isStrongFontWeight(node.style.fontWeight);
809
+ }
810
+ function pickSectionName(nodes) {
811
+ const textNodes = nodes.filter((n) => n.kind === "text" && isMeaningfulSectionLabel(n.text)).sort((a, b) => {
812
+ const fa = a.style.fontSize ?? 12;
813
+ const fb = b.style.fontSize ?? 12;
814
+ if (fb !== fa) return fb - fa;
815
+ const wa = isStrongFontWeight(a.style.fontWeight) ? 1 : 0;
816
+ const wb = isStrongFontWeight(b.style.fontWeight) ? 1 : 0;
817
+ if (wb !== wa) return wb - wa;
818
+ return a.rect.y - b.rect.y || a.rect.x - b.rect.x;
819
+ });
820
+ if (textNodes.length === 0) return "";
821
+ return normalizeLabel(textNodes[0].text);
822
+ }
823
+ function sectionBounds(nodes) {
824
+ const useful = nodes.filter((n) => !isThinSeparator(n.rect));
825
+ const candidates = useful.length > 0 ? useful : nodes;
826
+ const xMin = Math.min(...candidates.map((n) => n.rect.x));
827
+ const yMin = Math.min(...candidates.map((n) => n.rect.y));
828
+ const xMax = Math.max(...candidates.map((n) => n.rect.x + n.rect.width));
829
+ const yMax = Math.max(...candidates.map((n) => n.rect.y + n.rect.height));
830
+ return { x: xMin, y: yMin, width: xMax - xMin, height: yMax - yMin };
831
+ }
832
+ function collectSectionMembers(allNodes, seeds, medianFontSize) {
833
+ const top = Math.min(...seeds.map((n) => n.rect.y));
834
+ const bottom = Math.max(...seeds.map((n) => n.rect.y + n.rect.height));
835
+ const pad = Math.max(64, Math.min(140, medianFontSize * 6));
836
+ const seedIds = new Set(seeds.map((n) => n.id));
837
+ const members = [];
838
+ for (const node of allNodes) {
839
+ if (node.id === "root") continue;
840
+ if (isThinSeparator(node.rect)) continue;
841
+ const cy = node.rect.y + node.rect.height / 2;
842
+ const nearBand = cy >= top - pad && cy <= bottom + pad;
843
+ const seed = seedIds.has(node.id);
844
+ if (nearBand || seed) members.push(node);
845
+ }
846
+ return members.length > 0 ? members : seeds;
847
+ }
848
+ function detectSections(nodes) {
849
+ const textNodes = nodes.filter((n) => n.kind === "text" && n.text);
850
+ const medianFontSize = median(
851
+ textNodes.map((n) => n.style.fontSize ?? 0),
852
+ 14
853
+ );
854
+ const headingSeeds = textNodes.filter((n) => isSectionHeadingSeed(n, medianFontSize)).sort((a, b) => a.rect.y - b.rect.y || a.rect.x - b.rect.x);
855
+ const fallbackSeeds = nodes.filter((n) => n.parentId === "root" && n.id !== "root").filter((n) => !isThinSeparator(n.rect)).filter(
856
+ (n) => n.rect.width >= 80 || n.kind === "text" && (n.style.fontSize ?? 0) >= medianFontSize
857
+ ).sort((a, b) => a.rect.y - b.rect.y || a.rect.x - b.rect.x);
858
+ const seeds = headingSeeds.length > 0 ? headingSeeds : fallbackSeeds;
859
+ if (seeds.length === 0) return [];
860
+ const groups = [];
861
+ let current = [seeds[0]];
862
+ for (let i = 1; i < seeds.length; i++) {
863
+ const prev = seeds[i - 1];
864
+ const curr = seeds[i];
865
+ const gap = curr.rect.y - (prev.rect.y + prev.rect.height);
866
+ const gapLimit = Math.max(
867
+ 72,
868
+ Math.min(220, Math.max(prev.rect.height, curr.rect.height, medianFontSize * 6))
869
+ );
870
+ if (gap > gapLimit) {
871
+ groups.push(current);
872
+ current = [curr];
873
+ } else {
874
+ current.push(curr);
875
+ }
876
+ }
877
+ groups.push(current);
878
+ const sections = [];
879
+ for (const group of groups) {
880
+ const members = collectSectionMembers(nodes, group, medianFontSize);
881
+ if (members.length === 0) continue;
882
+ const bounds = sectionBounds(members);
883
+ if (bounds.width < 20 || bounds.height < 8) continue;
884
+ const name = pickSectionName(members) || pickSectionName(group) || `Section ${sections.length + 1}`;
885
+ sections.push({
886
+ id: `s${sections.length + 1}`,
887
+ name,
888
+ bounds,
889
+ nodeIds: members.map((n) => n.id)
890
+ });
891
+ }
892
+ return sections;
893
+ }
894
+ function nearestSectionId(sections, bounds) {
895
+ if (sections.length === 0) return void 0;
896
+ const cy = bounds.y + bounds.height / 2;
897
+ let best = null;
898
+ for (const s of sections) {
899
+ const sy = s.bounds.y + s.bounds.height / 2;
900
+ const score = Math.abs(cy - sy);
901
+ if (!best || score < best.score) best = { id: s.id, score };
902
+ }
903
+ return best?.id;
904
+ }
905
+ function isLikelyGray(hex) {
906
+ if (!hex || !hex.startsWith("#") || hex.length < 7) return false;
907
+ const r = parseInt(hex.slice(1, 3), 16);
908
+ const g = parseInt(hex.slice(3, 5), 16);
909
+ const b = parseInt(hex.slice(5, 7), 16);
910
+ const max = Math.max(r, g, b);
911
+ const min = Math.min(r, g, b);
912
+ return max - min < 28;
913
+ }
914
+ function getNodeMap(nodes) {
915
+ const map = /* @__PURE__ */ new Map();
916
+ for (const n of nodes) map.set(n.id, n);
917
+ return map;
918
+ }
919
+ function directChildren(node, map) {
920
+ return node.children.map((id) => map.get(id)).filter((v) => Boolean(v));
921
+ }
922
+ function getDescendantText(node, map, limit = 16) {
923
+ const out = [];
924
+ const stack = [...node.children];
925
+ while (stack.length > 0 && out.length < limit) {
926
+ const id = stack.pop();
927
+ const n = map.get(id);
928
+ if (!n) continue;
929
+ if (n.kind === "text" && n.text) out.push(n);
930
+ for (const child of n.children) stack.push(child);
931
+ }
932
+ return out;
933
+ }
934
+ function centeredScore(parent, child) {
935
+ const px = parent.x + parent.width / 2;
936
+ const py = parent.y + parent.height / 2;
937
+ const cx = child.x + child.width / 2;
938
+ const cy = child.y + child.height / 2;
939
+ const dx = Math.abs(px - cx) / Math.max(1, parent.width);
940
+ const dy = Math.abs(py - cy) / Math.max(1, parent.height);
941
+ return Math.max(0, 1 - (dx + dy) * 1.8);
942
+ }
943
+ function extractComponentCandidates(nodes, sections) {
944
+ const map = getNodeMap(nodes);
945
+ const byNode = /* @__PURE__ */ new Map();
946
+ const kindPriority = {
947
+ heading: 100,
948
+ button: 90,
949
+ select: 86,
950
+ input: 85,
951
+ textarea: 84,
952
+ tab_group: 78,
953
+ table: 70,
954
+ modal: 68,
955
+ card: 60,
956
+ list_item: 52,
957
+ tag: 40
958
+ };
959
+ const pushCandidate = (candidate) => {
960
+ const arr = byNode.get(candidate.nodeId) ?? [];
961
+ arr.push(candidate);
962
+ byNode.set(candidate.nodeId, arr);
963
+ };
964
+ const sortByGeometry = (a, b) => a.rect.y - b.rect.y || a.rect.x - b.rect.x || a.zIndex - b.zIndex;
965
+ for (const node of nodes) {
966
+ if (node.id === "root") continue;
967
+ if (node.kind === "text" && node.text) {
968
+ const fs = node.style.fontSize ?? 12;
969
+ const fw = node.style.fontWeight ?? "Regular";
970
+ const label = normalizeLabel(node.text);
971
+ if (label.length >= 2 && (fs >= 18 || fs >= 16 && isStrongFontWeight(fw))) {
972
+ pushCandidate({
973
+ id: `c-${node.id}-heading`,
974
+ nodeId: node.id,
975
+ kind: "heading",
976
+ confidence: fs >= 18 ? 0.95 : 0.84,
977
+ bounds: node.rect,
978
+ text: label,
979
+ style: node.style,
980
+ sectionId: nearestSectionId(sections, node.rect),
981
+ reasons: ["\u6587\u672C\u5B57\u53F7\u8F83\u5927\uFF0C\u7B26\u5408\u6807\u9898\u7279\u5F81"]
982
+ });
983
+ }
984
+ continue;
985
+ }
986
+ if (node.kind === "shape" || node.kind === "symbol" || node.kind === "group" || node.kind === "unknown") {
987
+ if (isThinSeparator(node.rect)) continue;
988
+ const children = directChildren(node, map);
989
+ const textChildren = children.filter((c) => c.kind === "text" && c.text).sort(sortByGeometry);
990
+ const textDesc = getDescendantText(node, map, 24).sort(sortByGeometry);
991
+ const w = node.rect.width;
992
+ const h = node.rect.height;
993
+ const bg = node.style.bg;
994
+ const hasBorder = Boolean(node.style.border);
995
+ const primaryText = normalizeLabel(textChildren[0]?.text ?? textDesc[0]?.text);
996
+ const labelList = [
997
+ ...new Set(
998
+ [...textChildren, ...textDesc].map((t) => normalizeLabel(t.text)).filter((t) => isMeaningfulSectionLabel(t))
999
+ )
1000
+ ];
1001
+ const reasons = [];
1002
+ const hasIconLikeChild = children.some(
1003
+ (c) => c.kind !== "text" && c.id !== "root" && area(c.rect) <= 900 && c.rect.width <= 32 && c.rect.height <= 32
1004
+ );
1005
+ const tabCandidates = textChildren.filter((t) => isMeaningfulSectionLabel(t.text) && (t.text ?? "").trim().length <= 18);
1006
+ let tabGroupScore = 0;
1007
+ if (tabCandidates.length >= 2 && w >= 120 && w <= 900 && h <= 88) {
1008
+ const baseY = tabCandidates[0].rect.y;
1009
+ const aligned = tabCandidates.every((t) => Math.abs(t.rect.y - baseY) <= 18);
1010
+ if (aligned) {
1011
+ tabGroupScore += 0.45;
1012
+ reasons.push("\u591A\u4E2A\u77ED\u6587\u672C\u6A2A\u5411\u6392\u5E03\uFF0C\u7B26\u5408 Tab/\u5206\u6BB5\u63A7\u4EF6\u7279\u5F81");
1013
+ }
1014
+ if (tabCandidates.length >= 3) tabGroupScore += 0.2;
1015
+ if (hasBorder || bg) tabGroupScore += 0.12;
1016
+ if (tabGroupScore >= 0.72) {
1017
+ pushCandidate({
1018
+ id: `c-${node.id}-tab-group`,
1019
+ nodeId: node.id,
1020
+ kind: "tab_group",
1021
+ confidence: Math.min(0.96, tabGroupScore),
1022
+ bounds: node.rect,
1023
+ text: tabCandidates.map((t) => normalizeLabel(t.text)).join(" / "),
1024
+ style: node.style,
1025
+ sectionId: nearestSectionId(sections, node.rect),
1026
+ reasons: [...reasons]
1027
+ });
1028
+ }
1029
+ }
1030
+ let buttonScore = 0;
1031
+ if (w >= 48 && w <= 360 && h >= 24 && h <= 72) {
1032
+ buttonScore += 0.22;
1033
+ reasons.push("\u5C3A\u5BF8\u63A5\u8FD1\u6309\u94AE\u5E38\u89C1\u89C4\u683C");
1034
+ }
1035
+ if (labelList.length === 1 && labelList[0].length <= 18) {
1036
+ buttonScore += 0.24;
1037
+ reasons.push("\u5355\u6587\u672C\u4E14\u957F\u5EA6\u77ED");
1038
+ if (textChildren.length === 1) {
1039
+ buttonScore += centeredScore(node.rect, textChildren[0].rect) * 0.2;
1040
+ }
1041
+ }
1042
+ if (bg && bg !== "#FFFFFF") {
1043
+ buttonScore += 0.2;
1044
+ reasons.push("\u5B58\u5728\u975E\u767D\u5E95\u80CC\u666F\u8272");
1045
+ }
1046
+ if (hasBorder) buttonScore += 0.12;
1047
+ if ((node.style.radius ?? 0) >= 4) buttonScore += 0.08;
1048
+ if (hasIconLikeChild) buttonScore += 0.06;
1049
+ if (buttonScore >= 0.62) {
1050
+ pushCandidate({
1051
+ id: `c-${node.id}-button`,
1052
+ nodeId: node.id,
1053
+ kind: "button",
1054
+ confidence: Math.min(0.99, buttonScore),
1055
+ bounds: node.rect,
1056
+ text: primaryText,
1057
+ style: node.style,
1058
+ sectionId: nearestSectionId(sections, node.rect),
1059
+ reasons: [...reasons]
1060
+ });
1061
+ }
1062
+ let inputScore = 0;
1063
+ const placeholderText = textDesc.find(
1064
+ (t) => /请输入|搜索|请选择|筛选|任务名称|关键词|内容|输入|查询|选择/i.test(
1065
+ normalizeLabel(t.text)
1066
+ )
1067
+ );
1068
+ const multilineText = textDesc.some((t) => (t.text ?? "").includes("\n"));
1069
+ const inputCue = Boolean(placeholderText) || textDesc.some((t) => isLikelyGray(t.style.textColor)) || /输入|搜索|选择|筛选|关键词|查询/i.test((node.name ?? "") + (primaryText ?? ""));
1070
+ if (w >= 140 && w <= 720 && h >= 28 && h <= 64) inputScore += 0.22;
1071
+ if (hasBorder || bg === "#FFFFFF") inputScore += 0.16;
1072
+ if (placeholderText) inputScore += 0.35;
1073
+ if (textDesc.some((t) => isLikelyGray(t.style.textColor))) inputScore += 0.08;
1074
+ if (textChildren.length === 0 && textDesc.length <= 2) inputScore += 0.08;
1075
+ if (!inputCue) inputScore -= 0.14;
1076
+ if (inputCue && inputScore >= 0.7) {
1077
+ pushCandidate({
1078
+ id: `c-${node.id}-input`,
1079
+ nodeId: node.id,
1080
+ kind: /选择|下拉/i.test((node.name ?? "") + (primaryText ?? "")) ? "select" : "input",
1081
+ confidence: Math.min(0.98, inputScore),
1082
+ bounds: node.rect,
1083
+ text: primaryText,
1084
+ style: node.style,
1085
+ sectionId: nearestSectionId(sections, node.rect),
1086
+ reasons: ["\u5C3A\u5BF8+\u8FB9\u6846+\u5360\u4F4D\u6587\u672C\u7B26\u5408\u8F93\u5165\u7EC4\u4EF6\u7279\u5F81"]
1087
+ });
1088
+ }
1089
+ let textareaScore = 0;
1090
+ const textareaCue = Boolean(placeholderText) || multilineText;
1091
+ if (w >= 180 && w <= 960 && h >= 72 && h <= 420) textareaScore += 0.24;
1092
+ if (multilineText) textareaScore += 0.32;
1093
+ if (placeholderText) textareaScore += 0.28;
1094
+ if (hasBorder || bg === "#FFFFFF") textareaScore += 0.18;
1095
+ if (textDesc.length >= 2 && textDesc.length <= 6) textareaScore += 0.08;
1096
+ if (!textareaCue) textareaScore -= 0.16;
1097
+ if (textareaCue && textareaScore >= 0.68) {
1098
+ pushCandidate({
1099
+ id: `c-${node.id}-textarea`,
1100
+ nodeId: node.id,
1101
+ kind: "textarea",
1102
+ confidence: Math.min(0.96, textareaScore),
1103
+ bounds: node.rect,
1104
+ text: primaryText,
1105
+ style: node.style,
1106
+ sectionId: nearestSectionId(sections, node.rect),
1107
+ reasons: ["\u5927\u5C3A\u5BF8\u8F93\u5165\u533A\u5757\uFF0C\u7B26\u5408\u591A\u884C\u6587\u672C\u533A\u57DF\u7279\u5F81"]
1108
+ });
1109
+ }
1110
+ let tagScore = 0;
1111
+ if (w <= 180 && h <= 44) tagScore += 0.28;
1112
+ if (labelList.length === 1 && labelList[0].length <= 12) {
1113
+ tagScore += 0.28;
1114
+ }
1115
+ if (bg || hasBorder) tagScore += 0.2;
1116
+ if ((node.style.radius ?? 0) >= 6) tagScore += 0.1;
1117
+ if (w <= 120 && h <= 32) tagScore += 0.08;
1118
+ if (tagScore >= 0.66) {
1119
+ pushCandidate({
1120
+ id: `c-${node.id}-tag`,
1121
+ nodeId: node.id,
1122
+ kind: "tag",
1123
+ confidence: Math.min(0.95, tagScore),
1124
+ bounds: node.rect,
1125
+ text: primaryText,
1126
+ style: node.style,
1127
+ sectionId: nearestSectionId(sections, node.rect),
1128
+ reasons: ["\u5C0F\u5C3A\u5BF8+\u5355\u6587\u672C\uFF0C\u7B26\u5408\u6807\u7B7E/\u7B5B\u9009\u9879\u7279\u5F81"]
1129
+ });
1130
+ }
1131
+ let modalScore = 0;
1132
+ if (w >= 280 && w <= 980 && h >= 180) modalScore += 0.35;
1133
+ if ((node.style.shadow?.blur ?? 0) > 0) modalScore += 0.2;
1134
+ if ((node.style.radius ?? 0) >= 4) modalScore += 0.1;
1135
+ if (textDesc.length >= 2) modalScore += 0.15;
1136
+ if (modalScore >= 0.72) {
1137
+ pushCandidate({
1138
+ id: `c-${node.id}-modal`,
1139
+ nodeId: node.id,
1140
+ kind: "modal",
1141
+ confidence: Math.min(0.93, modalScore),
1142
+ bounds: node.rect,
1143
+ text: primaryText,
1144
+ style: node.style,
1145
+ sectionId: nearestSectionId(sections, node.rect),
1146
+ reasons: ["\u5927\u9762\u79EF\u5BB9\u5668+\u9634\u5F71/\u5706\u89D2\uFF0C\u7B26\u5408\u5F39\u5C42\u7279\u5F81"]
1147
+ });
1148
+ }
1149
+ let tableScore = 0;
1150
+ const wideChildren = children.filter(
1151
+ (c) => c.kind !== "text" && c.rect.width >= Math.max(400, w * 0.55) && c.rect.height >= 28 && c.rect.height <= 84
1152
+ );
1153
+ if (w >= 560 && h >= 180) tableScore += 0.25;
1154
+ if (wideChildren.length >= 3) tableScore += 0.45;
1155
+ if (textDesc.length >= 8) tableScore += 0.15;
1156
+ if (tableScore >= 0.72) {
1157
+ pushCandidate({
1158
+ id: `c-${node.id}-table`,
1159
+ nodeId: node.id,
1160
+ kind: "table",
1161
+ confidence: Math.min(0.96, tableScore),
1162
+ bounds: node.rect,
1163
+ style: node.style,
1164
+ sectionId: nearestSectionId(sections, node.rect),
1165
+ reasons: ["\u5BBD\u5217\u8868\u7ED3\u6784\u660E\u663E\uFF0C\u542B\u591A\u884C\u91CD\u590D\u5757"]
1166
+ });
1167
+ }
1168
+ if (w >= 220 && w <= 720 && h >= 100 && h <= 420 && (bg || hasBorder)) {
1169
+ const cardScore = 0.62 + Math.min(0.28, textDesc.length * 0.03);
1170
+ if (cardScore >= 0.7) {
1171
+ pushCandidate({
1172
+ id: `c-${node.id}-card`,
1173
+ nodeId: node.id,
1174
+ kind: "card",
1175
+ confidence: Math.min(0.9, cardScore),
1176
+ bounds: node.rect,
1177
+ text: primaryText,
1178
+ style: node.style,
1179
+ sectionId: nearestSectionId(sections, node.rect),
1180
+ reasons: ["\u4E2D\u7B49\u5C3A\u5BF8\u5BB9\u5668\uFF0C\u7B26\u5408\u5361\u7247\u7ED3\u6784"]
1181
+ });
1182
+ }
1183
+ }
1184
+ }
1185
+ }
1186
+ const bestByNode = /* @__PURE__ */ new Map();
1187
+ for (const candidates of byNode.values()) {
1188
+ candidates.sort((a, b) => {
1189
+ if (b.confidence !== a.confidence) return b.confidence - a.confidence;
1190
+ return (kindPriority[b.kind] ?? 0) - (kindPriority[a.kind] ?? 0);
1191
+ });
1192
+ const best = candidates[0];
1193
+ if (!best) continue;
1194
+ const current = bestByNode.get(best.nodeId);
1195
+ if (!current || best.confidence > current.confidence || best.confidence === current.confidence && (kindPriority[best.kind] ?? 0) > (kindPriority[current.kind] ?? 0)) {
1196
+ bestByNode.set(best.nodeId, best);
1197
+ }
1198
+ }
1199
+ return [...bestByNode.values()].sort((a, b) => {
1200
+ if (b.confidence !== a.confidence) return b.confidence - a.confidence;
1201
+ return (kindPriority[b.kind] ?? 0) - (kindPriority[a.kind] ?? 0);
1202
+ });
1203
+ }
1204
+ function buildSceneGraph(artboard, options) {
1205
+ const { yMin, yMax } = withRegionDefaults(artboard, options);
1206
+ const designWidth = detectDesignViewportWidth(artboard.layers);
1207
+ const normalized = [];
1208
+ for (let i = 0; i < artboard.layers.length; i++) {
1209
+ const raw = artboard.layers[i];
1210
+ if (isNoise(raw) || isAnnotation(raw)) continue;
1211
+ if (isMarginSpecText(raw, designWidth)) continue;
1212
+ const layer = normalizeLayer(raw, i);
1213
+ if (!inRegion(layer.rect, yMin, yMax)) continue;
1214
+ if (layer.rect.width <= 0 || layer.rect.height <= 0) continue;
1215
+ normalized.push(layer);
1216
+ }
1217
+ const maxNodes = options?.maxNodes ?? DEFAULT_MAX_NODES;
1218
+ const truncated = normalized.length > maxNodes;
1219
+ const sliced = normalized.slice(0, maxNodes);
1220
+ const nodes = [
1221
+ {
1222
+ id: "root",
1223
+ rawIndex: -1,
1224
+ rawType: "artboard",
1225
+ kind: "root",
1226
+ name: artboard.name,
1227
+ rect: { x: 0, y: 0, width: designWidth, height: artboard.height },
1228
+ zIndex: -1,
1229
+ parentId: "",
1230
+ children: [],
1231
+ style: {}
1232
+ }
1233
+ ];
1234
+ for (const layer of sliced) {
1235
+ nodes.push({
1236
+ id: layer.id,
1237
+ rawIndex: layer.rawIndex,
1238
+ rawType: layer.rawType,
1239
+ kind: layer.kind,
1240
+ name: layer.name,
1241
+ text: layer.text,
1242
+ rect: layer.rect,
1243
+ zIndex: layer.zIndex,
1244
+ parentId: "root",
1245
+ children: [],
1246
+ style: layer.style
1247
+ });
1248
+ }
1249
+ const map = getNodeMap(nodes);
1250
+ const candidates = nodes.filter((n) => n.id !== "root").map((n) => ({ node: n, area: area(n.rect) })).sort((a, b) => a.area - b.area);
1251
+ for (const { node } of candidates) {
1252
+ let bestParent;
1253
+ for (const maybe of nodes) {
1254
+ if (maybe.id === "root" || maybe.id === node.id) continue;
1255
+ if (maybe.kind === "text") continue;
1256
+ const parentArea = area(maybe.rect);
1257
+ const childArea = area(node.rect);
1258
+ if (parentArea <= childArea * 1.05) continue;
1259
+ if (!containsRect(maybe.rect, node.rect, 2)) continue;
1260
+ if (!bestParent || area(bestParent.rect) > parentArea) {
1261
+ bestParent = maybe;
1262
+ }
1263
+ }
1264
+ if (bestParent) node.parentId = bestParent.id;
1265
+ }
1266
+ for (const node of nodes) {
1267
+ if (node.id === "root") continue;
1268
+ const parent = map.get(node.parentId) ?? map.get("root");
1269
+ if (parent) parent.children.push(node.id);
1270
+ }
1271
+ for (const node of nodes) {
1272
+ node.children.sort((a, b) => {
1273
+ const na = map.get(a);
1274
+ const nb = map.get(b);
1275
+ if (!na || !nb) return 0;
1276
+ return na.rect.y - nb.rect.y || na.rect.x - nb.rect.x || na.zIndex - nb.zIndex;
1277
+ });
1278
+ }
1279
+ for (const node of nodes) {
1280
+ if (node.id === "root") continue;
1281
+ const parent = map.get(node.parentId);
1282
+ if (!parent) continue;
1283
+ node.constraints = estimateConstraints(parent.rect, node.rect);
1284
+ }
1285
+ const sections = detectSections(nodes);
1286
+ return {
1287
+ artboard: artboard.name,
1288
+ width: artboard.width,
1289
+ height: artboard.height,
1290
+ designWidth,
1291
+ region: { yMin, yMax },
1292
+ rootId: "root",
1293
+ nodeCount: nodes.length,
1294
+ truncated,
1295
+ sections,
1296
+ nodes
1297
+ };
1298
+ }
1299
+
1300
+ // src/v3/ai.ts
1301
+ import { createHash as createHash2 } from "crypto";
1302
+ var MAX_BLOCKS_PER_REQUEST = 6;
1303
+ var REQUEST_TIMEOUT_MS = 3e4;
1304
+ var analysisCache = /* @__PURE__ */ new Map();
1305
+ var visualStateEnum = [
1306
+ "hover",
1307
+ "active",
1308
+ "selected",
1309
+ "disabled",
1310
+ "loading",
1311
+ "empty",
1312
+ "error",
1313
+ "focus",
1314
+ "open",
1315
+ "closed",
1316
+ "expanded",
1317
+ "collapsed",
1318
+ "pressed",
1319
+ "unknown"
1320
+ ];
1321
+ var interactionRoleEnum = [
1322
+ "trigger",
1323
+ "condition",
1324
+ "action",
1325
+ "visual_state",
1326
+ "note"
1327
+ ];
1328
+ function env(name) {
1329
+ return process.env[name]?.trim() ?? "";
1330
+ }
1331
+ function resolveConfig() {
1332
+ const providerEnv = env("SKETCH_MCP_INTERACTION_PROVIDER").toLowerCase().trim();
1333
+ const provider = providerEnv === "fallback" ? "fallback" : "anthropic";
1334
+ const apiKey = env("SKETCH_MCP_AUTH_TOKEN");
1335
+ const enabled = provider !== "fallback" && Boolean(apiKey);
1336
+ const model = env("SKETCH_MCP_INTERACTION_MODEL");
1337
+ const baseUrl = env("SKETCH_MCP_BASE_URL");
1338
+ return { provider, enabled, model, baseUrl, apiKey };
1339
+ }
1340
+ function stableHash(value) {
1341
+ return createHash2("sha256").update(JSON.stringify(value)).digest("hex");
1342
+ }
1343
+ function compactRect(rect2) {
1344
+ return {
1345
+ x: Math.round(rect2.x),
1346
+ y: Math.round(rect2.y),
1347
+ width: Math.round(rect2.width),
1348
+ height: Math.round(rect2.height)
1349
+ };
1350
+ }
1351
+ function summarizeNode(node) {
1352
+ return {
1353
+ id: node.id,
1354
+ kind: node.kind,
1355
+ name: node.name,
1356
+ text: node.text,
1357
+ rect: compactRect(node.rect),
1358
+ style: {
1359
+ bg: node.style.bg,
1360
+ border: node.style.border ? {
1361
+ color: node.style.border.color,
1362
+ width: node.style.border.width
1363
+ } : null,
1364
+ radius: node.style.radius,
1365
+ shadow: node.style.shadow ? {
1366
+ color: node.style.shadow.color,
1367
+ x: node.style.shadow.x,
1368
+ y: node.style.shadow.y,
1369
+ blur: node.style.shadow.blur,
1370
+ spread: node.style.shadow.spread
1371
+ } : null,
1372
+ opacity: node.style.opacity,
1373
+ textColor: node.style.textColor,
1374
+ fontSize: node.style.fontSize,
1375
+ fontWeight: node.style.fontWeight
1376
+ }
1377
+ };
1378
+ }
1379
+ function buildUserPayload(blocks) {
1380
+ return JSON.stringify(
1381
+ {
1382
+ blocks: blocks.map((block) => ({
1383
+ blockId: block.blockId,
1384
+ anchor: block.anchor,
1385
+ target: block.target,
1386
+ section: block.section,
1387
+ specLines: block.specLines,
1388
+ nearbyNodes: block.nearbyNodes
1389
+ }))
1390
+ },
1391
+ null,
1392
+ 2
1393
+ );
1394
+ }
1395
+ function buildSystemPrompt() {
1396
+ return [
1397
+ "\u4F60\u662F\u8BBE\u8BA1\u7A3F\u4EA4\u4E92\u7406\u89E3\u5668\u3002",
1398
+ "\u4F60\u7684\u4EFB\u52A1\u662F\u57FA\u4E8E\u8F93\u5165\u7684\u4EA4\u4E92\u5757\u3001\u951A\u70B9\u3001\u76EE\u6807\u7EC4\u4EF6\u548C\u9644\u8FD1\u89C6\u89C9\u8282\u70B9\uFF0C\u7406\u89E3\u8FD9\u6BB5\u8BF4\u660E\u771F\u6B63\u63CF\u8FF0\u7684\u4EA4\u4E92\u4E0E\u89C6\u89C9\u72B6\u6001\u3002",
1399
+ "\u4E0D\u8981\u7528\u5B57\u9762\u5173\u952E\u8BCD\u89C4\u5219\u53BB\u5206\u7C7B\uFF1B\u8981\u6309\u8BED\u4E49\u7406\u89E3\u6BCF\u4E00\u884C\u5C5E\u4E8E\u89E6\u53D1\u3001\u6761\u4EF6\u3001\u52A8\u4F5C\u3001\u89C6\u89C9\u72B6\u6001\u8FD8\u662F\u5907\u6CE8\u3002",
1400
+ "\u5982\u679C\u4E00\u6BB5\u8BF4\u660E\u540C\u65F6\u5305\u542B\u884C\u4E3A\u548C\u89C6\u89C9\u72B6\u6001\uFF0C\u8BF7\u8F93\u51FA mixed\uFF0C\u5E76\u5728 visualHints \u91CC\u63CF\u8FF0\u72B6\u6001\u53D8\u5316\u3002",
1401
+ "\u5982\u679C\u8BF4\u660E\u63CF\u8FF0\u7684\u662F hover\u3001active\u3001selected\u3001disabled\u3001loading\u3001empty\u3001error\u3001focus\u3001open\u3001closed\u3001expanded\u3001collapsed\u3001pressed \u7B49\u72B6\u6001\uFF0C\u8BF7\u4F18\u5148\u5199\u8FDB visualHints\u3002",
1402
+ "\u5982\u679C\u6CA1\u6709\u628A\u63E1\uFF0C\u4E0D\u8981\u786C\u731C\u5177\u4F53\u8BED\u4E49\uFF0C\u4FDD\u7559 note \u5E76\u964D\u4F4E confidence\u3002",
1403
+ "steps \u5FC5\u987B\u4E0E\u8F93\u5165 specLines \u4E00\u4E00\u5BF9\u5E94\uFF0ClineIndex \u4ECE 0 \u5F00\u59CB\u3002"
1404
+ ].join("\n");
1405
+ }
1406
+ function interactionAnalysisSchema() {
1407
+ return {
1408
+ type: "object",
1409
+ additionalProperties: false,
1410
+ required: ["blocks"],
1411
+ properties: {
1412
+ blocks: {
1413
+ type: "array",
1414
+ items: {
1415
+ type: "object",
1416
+ additionalProperties: false,
1417
+ required: [
1418
+ "blockId",
1419
+ "blockType",
1420
+ "relation",
1421
+ "summary",
1422
+ "confidence",
1423
+ "steps",
1424
+ "visualHints",
1425
+ "notes"
1426
+ ],
1427
+ properties: {
1428
+ blockId: { type: "string" },
1429
+ blockType: {
1430
+ type: "string",
1431
+ enum: ["interaction", "visual_state", "mixed", "note"]
1432
+ },
1433
+ relation: {
1434
+ type: "string",
1435
+ enum: [
1436
+ "same_component",
1437
+ "modal",
1438
+ "popover",
1439
+ "sidebar",
1440
+ "page",
1441
+ "overlay",
1442
+ "unknown"
1443
+ ]
1444
+ },
1445
+ summary: { type: "string" },
1446
+ confidence: { type: "number" },
1447
+ steps: {
1448
+ type: "array",
1449
+ items: {
1450
+ type: "object",
1451
+ additionalProperties: false,
1452
+ required: ["lineIndex", "role"],
1453
+ properties: {
1454
+ lineIndex: { type: "integer" },
1455
+ role: {
1456
+ type: "string",
1457
+ enum: interactionRoleEnum
1458
+ },
1459
+ detail: { type: ["string", "null"] }
1460
+ }
1461
+ }
1462
+ },
1463
+ visualHints: {
1464
+ type: "array",
1465
+ items: {
1466
+ type: "object",
1467
+ additionalProperties: false,
1468
+ required: ["state", "description", "confidence"],
1469
+ properties: {
1470
+ state: {
1471
+ type: "string",
1472
+ enum: visualStateEnum
1473
+ },
1474
+ description: { type: "string" },
1475
+ confidence: { type: "number" }
1476
+ }
1477
+ }
1478
+ },
1479
+ notes: {
1480
+ type: "array",
1481
+ items: { type: "string" }
1482
+ }
1483
+ }
1484
+ }
1485
+ }
1486
+ }
1487
+ };
1488
+ }
1489
+ function extractAnthropicText(data) {
1490
+ if (typeof data?.content === "string" && data.content.trim()) {
1491
+ return data.content;
1492
+ }
1493
+ if (!Array.isArray(data?.content)) return void 0;
1494
+ const chunks = [];
1495
+ for (const item of data.content) {
1496
+ if (typeof item?.text === "string" && item.text.trim()) {
1497
+ chunks.push(item.text);
1498
+ }
1499
+ }
1500
+ return chunks.length > 0 ? chunks.join("") : void 0;
1501
+ }
1502
+ function extractJsonText(text) {
1503
+ const trimmed = text.trim();
1504
+ if (!trimmed) throw new Error("\u6A21\u578B\u54CD\u5E94\u4E3A\u7A7A");
1505
+ if (trimmed.startsWith("```")) {
1506
+ const withoutFence = trimmed.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "");
1507
+ if (withoutFence.trim()) return withoutFence.trim();
1508
+ }
1509
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
1510
+ return trimmed;
1511
+ }
1512
+ const firstObject = trimmed.indexOf("{");
1513
+ const lastObject = trimmed.lastIndexOf("}");
1514
+ if (firstObject >= 0 && lastObject > firstObject) {
1515
+ return trimmed.slice(firstObject, lastObject + 1);
1516
+ }
1517
+ const firstArray = trimmed.indexOf("[");
1518
+ const lastArray = trimmed.lastIndexOf("]");
1519
+ if (firstArray >= 0 && lastArray > firstArray) {
1520
+ return trimmed.slice(firstArray, lastArray + 1);
1521
+ }
1522
+ return trimmed;
1523
+ }
1524
+ function parseAnalysisResponse(text) {
1525
+ return JSON.parse(extractJsonText(text));
1526
+ }
1527
+ function normalizeAnalysis(raw, fallback, block, model, provider) {
1528
+ const rawSteps = Array.isArray(raw.steps) ? raw.steps : [];
1529
+ const steps = block.specLines.map((line, index) => {
1530
+ const matched = rawSteps.find((step) => step.lineIndex === index);
1531
+ if (matched) {
1532
+ return {
1533
+ type: matched.role,
1534
+ text: line,
1535
+ detail: matched.detail ?? void 0
1536
+ };
1537
+ }
1538
+ return fallback.steps[index] ?? {
1539
+ type: "note",
1540
+ text: line
1541
+ };
1542
+ });
1543
+ const visualHints = Array.isArray(raw.visualHints) ? raw.visualHints.map((hint) => ({
1544
+ state: hint.state,
1545
+ description: hint.description,
1546
+ confidence: Number(hint.confidence)
1547
+ })).filter((hint) => hint.description.trim().length > 0) : [];
1548
+ return {
1549
+ ...fallback,
1550
+ provider,
1551
+ model,
1552
+ blockType: raw.blockType,
1553
+ relation: raw.relation,
1554
+ summary: raw.summary,
1555
+ confidence: Number(raw.confidence),
1556
+ steps,
1557
+ visualHints,
1558
+ notes: Array.isArray(raw.notes) ? raw.notes.filter((note) => note.trim().length > 0) : []
1559
+ };
1560
+ }
1561
+ function inferVisualHintsFromText(lines) {
1562
+ const hints = [];
1563
+ const text = lines.join(" ").toLowerCase();
1564
+ const add = (state, description, confidence) => {
1565
+ hints.push({ state, description, confidence });
1566
+ };
1567
+ if (text.includes("hover")) add("hover", "\u6587\u672C\u63CF\u8FF0\u5305\u542B hover \u6001", 0.55);
1568
+ if (text.includes("selected") || text.includes("\u9009\u4E2D")) add("selected", "\u6587\u672C\u63CF\u8FF0\u5305\u542B\u9009\u4E2D\u6001", 0.55);
1569
+ if (text.includes("disabled") || text.includes("\u7981\u7528")) add("disabled", "\u6587\u672C\u63CF\u8FF0\u5305\u542B\u7981\u7528\u6001", 0.55);
1570
+ if (text.includes("loading") || text.includes("\u52A0\u8F7D")) add("loading", "\u6587\u672C\u63CF\u8FF0\u5305\u542B\u52A0\u8F7D\u6001", 0.55);
1571
+ if (text.includes("empty") || text.includes("\u7A7A\u6001")) add("empty", "\u6587\u672C\u63CF\u8FF0\u5305\u542B\u7A7A\u6001", 0.55);
1572
+ if (text.includes("error") || text.includes("\u9519\u8BEF")) add("error", "\u6587\u672C\u63CF\u8FF0\u5305\u542B\u9519\u8BEF\u6001", 0.55);
1573
+ if (text.includes("\u6253\u5F00") || text.includes("open")) add("open", "\u6587\u672C\u63CF\u8FF0\u5305\u542B\u6253\u5F00\u52A8\u4F5C", 0.5);
1574
+ if (text.includes("\u5173\u95ED") || text.includes("close")) add("closed", "\u6587\u672C\u63CF\u8FF0\u5305\u542B\u5173\u95ED\u52A8\u4F5C", 0.5);
1575
+ return hints;
1576
+ }
1577
+ function buildFallbackAnalysis(block) {
1578
+ const rules = getCompiledRules();
1579
+ const steps = block.specLines.map((line) => {
1580
+ if (rules.interactionTriggerRegex.test(line)) {
1581
+ return { type: "trigger", text: line };
1582
+ }
1583
+ if (rules.interactionConditionRegex.test(line)) {
1584
+ return { type: "condition", text: line };
1585
+ }
1586
+ if (rules.interactionActionRegex.test(line)) {
1587
+ return { type: "action", text: line };
1588
+ }
1589
+ return { type: "note", text: line };
1590
+ });
1591
+ const visualHints = inferVisualHintsFromText(block.specLines);
1592
+ const text = block.specLines.join(" ");
1593
+ const hasVisualKeywords = visualHints.length > 0;
1594
+ const blockType = steps.some((step) => step.type !== "note") && hasVisualKeywords ? "mixed" : steps.some((step) => step.type !== "note") ? "interaction" : hasVisualKeywords ? "visual_state" : "note";
1595
+ return {
1596
+ provider: "fallback",
1597
+ model: void 0,
1598
+ blockType,
1599
+ relation: "unknown",
1600
+ summary: block.specLines[0] ?? `\u4EA4\u4E92\u6807\u8BB0 ${block.anchor.number ?? ""}`.trim(),
1601
+ confidence: block.specLines.length > 0 ? 0.5 : 0.3,
1602
+ steps,
1603
+ visualHints,
1604
+ notes: text ? [text] : []
1605
+ };
1606
+ }
1607
+ async function requestAnthropicChunk(blocks, config, fetchImpl) {
1608
+ const payload = buildUserPayload(blocks);
1609
+ const apiRoot = config.baseUrl.replace(/\/$/, "").replace(/\/v1$/, "");
1610
+ const response = await fetchImpl(`${apiRoot}/v1/messages`, {
1611
+ method: "POST",
1612
+ headers: {
1613
+ "x-api-key": config.apiKey,
1614
+ "anthropic-version": "2023-06-01",
1615
+ "content-type": "application/json"
1616
+ },
1617
+ body: JSON.stringify({
1618
+ model: config.model,
1619
+ system: buildJsonInstruction(),
1620
+ messages: [
1621
+ {
1622
+ role: "user",
1623
+ content: payload
1624
+ }
1625
+ ],
1626
+ max_tokens: 2e3,
1627
+ temperature: 0
1628
+ }),
1629
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
1630
+ });
1631
+ if (!response.ok) {
1632
+ const detail = await response.text().catch(() => "");
1633
+ throw new Error(
1634
+ `Anthropic Messages API \u8BF7\u6C42\u5931\u8D25: HTTP ${response.status} ${response.statusText}${detail ? ` - ${detail}` : ""}`
1635
+ );
1636
+ }
1637
+ const data = await response.json();
1638
+ const text = extractAnthropicText(data);
1639
+ if (!text) {
1640
+ throw new Error("Anthropic \u54CD\u5E94\u4E2D\u672A\u627E\u5230\u53EF\u89E3\u6790\u6587\u672C");
1641
+ }
1642
+ return parseAnalysisChunk(blocks, text, config, "anthropic");
1643
+ }
1644
+ function buildJsonInstruction() {
1645
+ return [
1646
+ buildSystemPrompt(),
1647
+ "\u53EA\u8F93\u51FA\u5408\u6CD5 JSON\uFF0C\u4E0D\u8981\u8F93\u51FA markdown\u3001\u89E3\u91CA\u3001\u524D\u540E\u7F00\u6587\u5B57\u6216\u4EE3\u7801\u5757\u3002",
1648
+ "JSON \u5FC5\u987B\u7B26\u5408\u4EE5\u4E0B schema\uFF1A",
1649
+ JSON.stringify(interactionAnalysisSchema(), null, 2)
1650
+ ].join("\n\n");
1651
+ }
1652
+ function parseAnalysisChunk(blocks, text, config, provider) {
1653
+ const parsed = parseAnalysisResponse(text);
1654
+ const out = /* @__PURE__ */ new Map();
1655
+ for (const block of blocks) {
1656
+ const raw = parsed.blocks.find((item) => item.blockId === block.blockId);
1657
+ if (!raw) {
1658
+ out.set(block.blockId, buildFallbackAnalysis(block));
1659
+ continue;
1660
+ }
1661
+ const fallback = buildFallbackAnalysis(block);
1662
+ out.set(block.blockId, normalizeAnalysis(raw, fallback, block, config.model, provider));
1663
+ }
1664
+ return out;
1665
+ }
1666
+ async function analyzeInteractionBlocks(blocks, fetchImpl = fetch) {
1667
+ const config = resolveConfig();
1668
+ if (!config.enabled || blocks.length === 0) {
1669
+ const analyses2 = /* @__PURE__ */ new Map();
1670
+ for (const block of blocks) analyses2.set(block.blockId, buildFallbackAnalysis(block));
1671
+ return {
1672
+ analyses: analyses2,
1673
+ provider: "fallback",
1674
+ model: void 0
1675
+ };
1676
+ }
1677
+ const analyses = /* @__PURE__ */ new Map();
1678
+ let provider = "fallback";
1679
+ for (let i = 0; i < blocks.length; i += MAX_BLOCKS_PER_REQUEST) {
1680
+ const chunk = blocks.slice(i, i + MAX_BLOCKS_PER_REQUEST);
1681
+ const pending = [];
1682
+ for (const block of chunk) {
1683
+ const cacheKey = stableHash({ provider: config.provider, baseUrl: config.baseUrl, model: config.model, block });
1684
+ const cached = analysisCache.get(cacheKey);
1685
+ if (cached) {
1686
+ analyses.set(block.blockId, cached);
1687
+ provider = cached.provider !== "fallback" ? cached.provider : provider;
1688
+ } else {
1689
+ pending.push(block);
1690
+ }
1691
+ }
1692
+ if (pending.length > 0) {
1693
+ try {
1694
+ const responseMap = await requestAnthropicChunk(pending, config, fetchImpl);
1695
+ if (responseMap) {
1696
+ provider = config.provider;
1697
+ for (const [blockId, analysis] of responseMap.entries()) {
1698
+ const block = pending.find((item) => item.blockId === blockId);
1699
+ if (block) {
1700
+ analysisCache.set(
1701
+ stableHash({ provider: config.provider, baseUrl: config.baseUrl, model: config.model, block }),
1702
+ analysis
1703
+ );
1704
+ }
1705
+ analyses.set(blockId, analysis);
1706
+ }
1707
+ continue;
1708
+ }
1709
+ } catch (error) {
1710
+ console.error(
1711
+ `[sketch-mcp] AI interaction analysis failed for ${pending.map((b) => b.blockId).join(", ")}:`,
1712
+ error instanceof Error ? error.message : String(error)
1713
+ );
1714
+ }
1715
+ }
1716
+ for (const block of pending) {
1717
+ const fallback = buildFallbackAnalysis(block);
1718
+ analyses.set(block.blockId, fallback);
1719
+ analysisCache.set(
1720
+ stableHash({ provider: config.provider, baseUrl: config.baseUrl, model: config.model, block }),
1721
+ fallback
1722
+ );
1723
+ }
1724
+ }
1725
+ return {
1726
+ analyses,
1727
+ provider,
1728
+ model: provider === "fallback" ? void 0 : config.model
1729
+ };
1730
+ }
1731
+ function summarizeNodeForAi(node) {
1732
+ return summarizeNode(node);
1733
+ }
1734
+
1735
+ // src/v3/interactions.ts
1736
+ function normalizeSpecText(text) {
1737
+ return text.replace(/\s+/g, "").replace(/[,。;、!?!?::]/g, "").toLowerCase();
1738
+ }
1739
+ function splitSpecLines(layers, yMin, yMax, designWidth, specFontIncludes) {
1740
+ const lines = [];
1741
+ for (const l of layers) {
1742
+ if (l.type !== "text") continue;
1743
+ const content = (l.content ?? "").trim();
1744
+ if (!content) continue;
1745
+ const r = rect(l);
1746
+ if (!inRegion(r, yMin, yMax)) continue;
1747
+ const ff = l.fontFace ?? "";
1748
+ const isSpecLayer = includesAny(ff, specFontIncludes) || isMarginSpecText(l, designWidth);
1749
+ if (!isSpecLayer) continue;
1750
+ if ((l.fontSize ?? 0) > 32) continue;
1751
+ const lineHeight = l.lineHeight ?? Math.round((l.fontSize ?? 14) * 1.45);
1752
+ const parts = content.split("\n").map((x) => x.trim()).filter(Boolean);
1753
+ parts.forEach((text, i) => {
1754
+ lines.push({
1755
+ text,
1756
+ x: r.x,
1757
+ y: r.y + i * lineHeight
1758
+ });
1759
+ });
1760
+ }
1761
+ lines.sort((a, b) => a.y - b.y || a.x - b.x);
1762
+ const deduped = [];
1763
+ const seen = /* @__PURE__ */ new Set();
1764
+ for (const line of lines) {
1765
+ const key = `${Math.round(line.x)}|${Math.round(line.y)}|${normalizeSpecText(line.text)}`;
1766
+ if (seen.has(key)) continue;
1767
+ seen.add(key);
1768
+ deduped.push(line);
1769
+ }
1770
+ return deduped;
1771
+ }
1772
+ function clusterSpecBlocks(lines) {
1773
+ if (lines.length === 0) return [];
1774
+ const blocks = [];
1775
+ let current = {
1776
+ id: "sb1",
1777
+ x: lines[0].x,
1778
+ y: lines[0].y,
1779
+ lines: [lines[0]]
1780
+ };
1781
+ for (let i = 1; i < lines.length; i++) {
1782
+ const prev = lines[i - 1];
1783
+ const now = lines[i];
1784
+ const sameColumn = Math.abs(now.x - current.x) <= 64;
1785
+ const contiguousY = now.y - prev.y <= 62;
1786
+ if (sameColumn && contiguousY) {
1787
+ current.lines.push(now);
1788
+ } else {
1789
+ blocks.push(current);
1790
+ current = {
1791
+ id: `sb${blocks.length + 2}`,
1792
+ x: now.x,
1793
+ y: now.y,
1794
+ lines: [now]
1795
+ };
1796
+ }
1797
+ }
1798
+ blocks.push(current);
1799
+ return blocks;
1800
+ }
1801
+ function nearestSectionIdFromY(sections, y) {
1802
+ if (sections.length === 0) return void 0;
1803
+ let best = null;
1804
+ for (const section of sections) {
1805
+ const cy = section.bounds.y + section.bounds.height / 2;
1806
+ const score = Math.abs(cy - y);
1807
+ if (!best || score < best.score) best = { id: section.id, score };
1808
+ }
1809
+ return best?.id;
1810
+ }
1811
+ function extractAnchorNumber(anchor, textLayers) {
1812
+ const ar = rect(anchor);
1813
+ const acx = ar.x + ar.width / 2;
1814
+ const acy = ar.y + ar.height / 2;
1815
+ for (const t of textLayers) {
1816
+ if (t.type !== "text") continue;
1817
+ const content = (t.content ?? "").trim();
1818
+ if (!/^\d{1,2}$/.test(content)) continue;
1819
+ const tr = rect(t);
1820
+ const tcx = tr.x + tr.width / 2;
1821
+ const tcy = tr.y + tr.height / 2;
1822
+ if (Math.abs(tcx - acx) <= ar.width && Math.abs(tcy - acy) <= ar.height) {
1823
+ return content;
1824
+ }
1825
+ }
1826
+ return void 0;
1827
+ }
1828
+ function nearestTargetNode(anchorRect, nodes, designWidth) {
1829
+ const acx = anchorRect.x + anchorRect.width / 2;
1830
+ const acy = anchorRect.y + anchorRect.height / 2;
1831
+ let best = null;
1832
+ for (const node of nodes) {
1833
+ if (node.id === "root") continue;
1834
+ if (node.kind === "text") continue;
1835
+ if (node.rect.x + node.rect.width > designWidth + 2) continue;
1836
+ if (node.rect.width < 16 && node.rect.height < 16) continue;
1837
+ if (area(node.rect) < 300) continue;
1838
+ const ncx = node.rect.x + node.rect.width / 2;
1839
+ const ncy = node.rect.y + node.rect.height / 2;
1840
+ const dist = Math.sqrt((ncx - acx) ** 2 + (ncy - acy) ** 2);
1841
+ if (dist > 640) continue;
1842
+ const sizePenalty = Math.max(0, Math.log10(Math.max(1, area(node.rect))) - 4) * 20;
1843
+ const score = dist + sizePenalty;
1844
+ if (!best || score < best.score) best = { node, score };
1845
+ }
1846
+ return best?.node;
1847
+ }
1848
+ function nearestSceneNodes(anchorRect, nodes, limit = 8) {
1849
+ const acx = anchorRect.x + anchorRect.width / 2;
1850
+ const acy = anchorRect.y + anchorRect.height / 2;
1851
+ return nodes.filter((node) => node.id !== "root" && area(node.rect) >= 80).sort((a, b) => {
1852
+ const adx = a.rect.x + a.rect.width / 2 - acx;
1853
+ const ady = a.rect.y + a.rect.height / 2 - acy;
1854
+ const bdx = b.rect.x + b.rect.width / 2 - acx;
1855
+ const bdy = b.rect.y + b.rect.height / 2 - acy;
1856
+ const ad = Math.sqrt(adx * adx + ady * ady);
1857
+ const bd = Math.sqrt(bdx * bdx + bdy * bdy);
1858
+ return ad - bd;
1859
+ }).slice(0, limit);
1860
+ }
1861
+ function buildBlockInputs(scene, anchors, blocks, textLayers) {
1862
+ const usedBlockIds = /* @__PURE__ */ new Set();
1863
+ const edges = [];
1864
+ const inputs = [];
1865
+ for (let i = 0; i < anchors.length; i++) {
1866
+ const anchor = anchors[i];
1867
+ const ar = rect(anchor);
1868
+ const num = extractAnchorNumber(anchor, textLayers);
1869
+ const analysisId = `ia-${i + 1}`;
1870
+ let bestBlock;
1871
+ let bestScore = Number.POSITIVE_INFINITY;
1872
+ for (const block of blocks) {
1873
+ const dx = block.x >= ar.x ? (block.x - ar.x) * 0.2 : 600 + (ar.x - block.x);
1874
+ const dy = Math.abs(block.y - ar.y);
1875
+ const score = dx + dy;
1876
+ if (score < bestScore) {
1877
+ bestScore = score;
1878
+ bestBlock = block;
1879
+ }
1880
+ }
1881
+ const target = nearestTargetNode(ar, scene.nodes, scene.designWidth);
1882
+ if (bestBlock) usedBlockIds.add(bestBlock.id);
1883
+ const rawLines = bestBlock ? bestBlock.lines.map((l) => l.text) : [];
1884
+ const uniqLines = [];
1885
+ const seen = /* @__PURE__ */ new Set();
1886
+ for (const line of rawLines) {
1887
+ const key = normalizeSpecText(line);
1888
+ if (seen.has(key)) continue;
1889
+ seen.add(key);
1890
+ uniqLines.push(line);
1891
+ }
1892
+ const targetCenterY = target ? target.rect.y + target.rect.height / 2 : ar.y + ar.height / 2;
1893
+ const sectionId = nearestSectionIdFromY(scene.sections, targetCenterY);
1894
+ const section = sectionId ? scene.sections.find((item) => item.id === sectionId) : void 0;
1895
+ const nearbyNodes = nearestSceneNodes(ar, scene.nodes).filter((node) => node.id !== target?.id).map((node) => summarizeNodeForAi(node));
1896
+ if (target) {
1897
+ nearbyNodes.unshift(summarizeNodeForAi(target));
1898
+ }
1899
+ inputs.push({
1900
+ blockId: analysisId,
1901
+ anchor: {
1902
+ x: ar.x,
1903
+ y: ar.y,
1904
+ number: num
1905
+ },
1906
+ target: target ? {
1907
+ id: target.id,
1908
+ name: target.name,
1909
+ kind: target.kind,
1910
+ text: target.text,
1911
+ rect: target.rect
1912
+ } : void 0,
1913
+ section: section ? {
1914
+ id: section.id,
1915
+ name: section.name,
1916
+ bounds: section.bounds
1917
+ } : void 0,
1918
+ specLines: uniqLines,
1919
+ nearbyNodes
1920
+ });
1921
+ edges.push({
1922
+ analysisId,
1923
+ specBlockId: bestBlock?.id,
1924
+ anchorRect: ar,
1925
+ target,
1926
+ sectionId,
1927
+ sectionName: section?.name,
1928
+ specLines: uniqLines
1929
+ });
1930
+ }
1931
+ return { inputs, usedBlockIds, edges };
1932
+ }
1933
+ async function buildInteractionGraph(artboard, options) {
1934
+ const rules = getCompiledRules();
1935
+ const scene = buildSceneGraph(artboard, {
1936
+ pageY: options?.pageY,
1937
+ pageHeight: options?.pageHeight,
1938
+ maxNodes: DEFAULT_MAX_NODES
1939
+ });
1940
+ const yMin = scene.region.yMin;
1941
+ const yMax = scene.region.yMax;
1942
+ const designWidth = scene.designWidth;
1943
+ const textLayers = artboard.layers.filter((l) => l.type === "text");
1944
+ const anchors = artboard.layers.filter((l) => {
1945
+ if (l.type !== "symbol") return false;
1946
+ const name = l.name ?? "";
1947
+ if (!startsWithAny(name, rules.raw.anchorNamePrefixes)) return false;
1948
+ return inRegion(rect(l), yMin, yMax);
1949
+ }).sort((a, b) => rect(a).y - rect(b).y || rect(a).x - rect(b).x);
1950
+ const specLines = splitSpecLines(
1951
+ artboard.layers,
1952
+ yMin,
1953
+ yMax,
1954
+ designWidth,
1955
+ rules.raw.specFontIncludes
1956
+ );
1957
+ const blocks = clusterSpecBlocks(specLines);
1958
+ const { inputs, usedBlockIds, edges } = buildBlockInputs(
1959
+ scene,
1960
+ anchors,
1961
+ blocks,
1962
+ textLayers
1963
+ );
1964
+ const aiResult = await analyzeInteractionBlocks(inputs);
1965
+ const interactions = edges.map((edge, index) => {
1966
+ const analysis = aiResult.analyses.get(edge.analysisId) ?? {
1967
+ provider: "fallback",
1968
+ model: void 0,
1969
+ blockType: "note",
1970
+ relation: "unknown",
1971
+ summary: edge.specLines[0] ?? `\u4EA4\u4E92\u6807\u8BB0 ${index + 1}`,
1972
+ confidence: 0.3,
1973
+ steps: edge.specLines.map((line) => ({ type: "note", text: line })),
1974
+ visualHints: [],
1975
+ notes: []
1976
+ };
1977
+ return {
1978
+ id: `ia-${index + 1}`,
1979
+ anchor: {
1980
+ x: edge.anchorRect.x,
1981
+ y: edge.anchorRect.y,
1982
+ number: inputs[index]?.anchor.number
1983
+ },
1984
+ targetNodeId: edge.target?.id,
1985
+ targetName: edge.target?.name || edge.target?.text,
1986
+ targetBounds: edge.target?.rect,
1987
+ summary: analysis.summary,
1988
+ confidence: Number(analysis.confidence.toFixed(2)),
1989
+ steps: analysis.steps,
1990
+ specLines: edge.specLines,
1991
+ blockType: analysis.blockType,
1992
+ relation: analysis.relation,
1993
+ analysisProvider: analysis.provider,
1994
+ analysisModel: analysis.model,
1995
+ visualHints: analysis.visualHints,
1996
+ notes: analysis.notes
1997
+ };
1998
+ });
1999
+ const unboundSpecLines = blocks.filter((b) => !usedBlockIds.has(b.id)).flatMap((b) => b.lines.map((l) => l.text));
2000
+ return {
2001
+ artboard: artboard.name,
2002
+ region: { yMin, yMax },
2003
+ analysisProvider: aiResult.provider,
2004
+ analysisModel: aiResult.model,
2005
+ interactions,
2006
+ unboundSpecLines
2007
+ };
2008
+ }
2009
+
2010
+ // src/v3/blueprint.ts
2011
+ function topN(map, n) {
2012
+ const result = {};
2013
+ const sorted = [...map.entries()].sort((a, b) => b[1] - a[1]).slice(0, n);
2014
+ for (const [key, value] of sorted) result[key] = value;
2015
+ return result;
2016
+ }
2017
+ function collectTokens(layers, yMin, yMax, designWidth) {
2018
+ const textColors = /* @__PURE__ */ new Map();
2019
+ const bgColors = /* @__PURE__ */ new Map();
2020
+ const fontSizes = /* @__PURE__ */ new Map();
2021
+ const fontFamilies = /* @__PURE__ */ new Map();
2022
+ const radii = /* @__PURE__ */ new Map();
2023
+ for (const l of layers) {
2024
+ if (isNoise(l) || isAnnotation(l)) continue;
2025
+ if (isMarginSpecText(l, designWidth)) continue;
2026
+ const r = rect(l);
2027
+ if (!inRegion(r, yMin, yMax)) continue;
2028
+ if (l.type === "text") {
2029
+ const c = colorHex(l.color);
2030
+ if (c) textColors.set(c, (textColors.get(c) ?? 0) + 1);
2031
+ if (l.fontSize) {
2032
+ const key = String(l.fontSize);
2033
+ fontSizes.set(key, (fontSizes.get(key) ?? 0) + 1);
2034
+ }
2035
+ const family = (l.fontFace ?? l.fontFamily ?? "").trim();
2036
+ if (family) fontFamilies.set(family, (fontFamilies.get(family) ?? 0) + 1);
2037
+ }
2038
+ const bg = bgColor(l);
2039
+ if (bg) bgColors.set(bg, (bgColors.get(bg) ?? 0) + 1);
2040
+ const rad = radius(l);
2041
+ if (rad > 0) radii.set(String(rad), (radii.get(String(rad)) ?? 0) + 1);
2042
+ }
2043
+ return {
2044
+ textColors: topN(textColors, 24),
2045
+ bgColors: topN(bgColors, 20),
2046
+ fontSizes: topN(fontSizes, 12),
2047
+ fontFamilies: topN(fontFamilies, 12),
2048
+ radii: topN(radii, 10)
2049
+ };
2050
+ }
2051
+ async function buildBlueprint(artboard, options) {
2052
+ const scene = buildSceneGraph(artboard, options);
2053
+ const maxComponents = options?.maxComponents ?? DEFAULT_MAX_COMPONENTS;
2054
+ const components = extractComponentCandidates(scene.nodes, scene.sections).slice(
2055
+ 0,
2056
+ maxComponents
2057
+ );
2058
+ const interactionGraph = await buildInteractionGraph(artboard, {
2059
+ pageY: options?.pageY,
2060
+ pageHeight: options?.pageHeight
2061
+ });
2062
+ const interactions = interactionGraph.interactions;
2063
+ const visualHintSeen = /* @__PURE__ */ new Set();
2064
+ const visualHints = interactions.flatMap(
2065
+ (interaction) => interaction.visualHints.map((hint) => ({
2066
+ ...hint,
2067
+ description: `${interaction.targetName ?? interaction.summary}: ${hint.description}`
2068
+ })).filter((hint) => {
2069
+ const key = `${hint.state}|${hint.description}`;
2070
+ if (visualHintSeen.has(key)) return false;
2071
+ visualHintSeen.add(key);
2072
+ return true;
2073
+ })
2074
+ );
2075
+ const tokens = collectTokens(
2076
+ artboard.layers,
2077
+ scene.region.yMin,
2078
+ scene.region.yMax,
2079
+ scene.designWidth
2080
+ );
2081
+ return {
2082
+ meta: {
2083
+ artboard: artboard.name,
2084
+ width: artboard.width,
2085
+ height: artboard.height,
2086
+ designWidth: scene.designWidth,
2087
+ region: scene.region
2088
+ },
2089
+ layout: {
2090
+ sections: scene.sections
2091
+ },
2092
+ tokens,
2093
+ components,
2094
+ visualHints,
2095
+ interactions,
2096
+ implementationHints: {
2097
+ recommendedStack: "React + Ant Design",
2098
+ codingChecklist: [
2099
+ "\u5148\u6309 sections \u642D\u5EFA\u9875\u9762\u9AA8\u67B6\uFF0C\u518D\u653E\u7F6E\u9AD8\u7F6E\u4FE1\u5EA6\u7EC4\u4EF6\u3002",
2100
+ "\u6240\u6709\u989C\u8272/\u5B57\u53F7/\u5706\u89D2\u4F18\u5148\u4F7F\u7528 tokens\uFF0C\u907F\u514D\u786C\u7F16\u7801\u6563\u843D\u6837\u5F0F\u3002",
2101
+ "\u5148\u5B8C\u6210\u9ED8\u8BA4\u6001\uFF0C\u518D\u8865\u9F50 hover/active/disabled/empty/loading\u3002",
2102
+ "\u4EA4\u4E92\u5B9E\u73B0\u4EE5 AI \u7406\u89E3\u540E\u7684 interactions.steps \u4E0E visualHints \u4E3A\u51C6\uFF0C\u5148\u8FD8\u539F\u72B6\u6001\u89C6\u89C9\u518D\u6302\u4E8B\u4EF6\u3002",
2103
+ "\u5B9E\u73B0\u540E\u8FDB\u884C\u622A\u56FE diff\uFF0C\u9010\u8F6E\u4FEE\u590D\u95F4\u8DDD\u3001\u5B57\u53F7\u3001\u989C\u8272\u504F\u5DEE\u3002"
2104
+ ],
2105
+ claudePromptSkeleton: "\u57FA\u4E8E blueprint \u751F\u6210 React + Ant Design \u9875\u9762\u3002\u8981\u6C42\uFF1A1) section \u987A\u5E8F\u4E0E bounds \u4E00\u81F4\uFF1B2) \u4F18\u5148\u5B9E\u73B0 confidence>=0.75 \u7684\u7EC4\u4EF6\uFF1B3) \u6309 interactions.steps \u5B9E\u73B0\u4E8B\u4EF6\uFF1B4) \u6309 visualHints \u5B9E\u73B0 hover/active/disabled/empty/loading/open \u7B49\u72B6\u6001\u89C6\u89C9\uFF1B5) \u8F93\u51FA\u5B8C\u6574\u53EF\u8FD0\u884C\u7EC4\u4EF6\u4E0E\u6837\u5F0F\u3002"
2106
+ }
2107
+ };
2108
+ }
2109
+
2110
+ // src/v3/validate.ts
2111
+ function normalizeComponentText(v) {
2112
+ if (typeof v !== "string") return "";
2113
+ return v.toLowerCase().replace(/\s+/g, "");
2114
+ }
2115
+ function matchKey(component) {
2116
+ const kind = String(component?.kind ?? component?.type ?? "unknown").toLowerCase();
2117
+ const text = normalizeComponentText(component?.text ?? component?.content ?? "");
2118
+ return `${kind}:${text}`;
2119
+ }
2120
+ function metricFromDelta(delta, tolerance) {
2121
+ if (delta <= 0) return 1;
2122
+ if (delta >= tolerance) return 0;
2123
+ return Number((1 - delta / tolerance).toFixed(4));
2124
+ }
2125
+ function validateImplementation(expectedInput, actualInput) {
2126
+ const expected = expectedInput ?? {};
2127
+ const actual = actualInput ?? {};
2128
+ const expectedComponents = expected.components ?? expected.blueprint?.components ?? [];
2129
+ const actualComponents = actual.components ?? actual.blueprint?.components ?? [];
2130
+ const actualByKey = /* @__PURE__ */ new Map();
2131
+ for (const comp of actualComponents) {
2132
+ const key = matchKey(comp);
2133
+ const arr = actualByKey.get(key) ?? [];
2134
+ arr.push(comp);
2135
+ actualByKey.set(key, arr);
2136
+ }
2137
+ let layoutAcc = 0;
2138
+ let styleAcc = 0;
2139
+ let matched = 0;
2140
+ const findings = [];
2141
+ for (const exp of expectedComponents) {
2142
+ const key = matchKey(exp);
2143
+ const candidates = actualByKey.get(key) ?? [];
2144
+ let best = candidates[0];
2145
+ if (!best && candidates.length === 0) {
2146
+ const kind = String(exp?.kind ?? exp?.type ?? "").toLowerCase();
2147
+ best = actualComponents.find(
2148
+ (a) => String(a?.kind ?? a?.type ?? "").toLowerCase() === kind
2149
+ );
2150
+ }
2151
+ if (!best) {
2152
+ findings.push({
2153
+ severity: "high",
2154
+ title: `\u7F3A\u5C11\u7EC4\u4EF6: ${exp?.kind ?? exp?.type ?? "unknown"}`,
2155
+ detail: `\u672A\u5728\u5B9E\u73B0\u4E2D\u627E\u5230\u4E0E\u300C${exp?.text ?? exp?.content ?? "\u65E0\u6587\u672C"}\u300D\u5339\u914D\u7684\u7EC4\u4EF6\u3002`
2156
+ });
2157
+ continue;
2158
+ }
2159
+ matched++;
2160
+ const eb = exp.bounds ?? exp.rect;
2161
+ const ab = best.bounds ?? best.rect;
2162
+ if (eb && ab) {
2163
+ const dx = Math.abs((ab.x ?? 0) - (eb.x ?? 0));
2164
+ const dy = Math.abs((ab.y ?? 0) - (eb.y ?? 0));
2165
+ const dw = Math.abs((ab.width ?? 0) - (eb.width ?? 0));
2166
+ const dh = Math.abs((ab.height ?? 0) - (eb.height ?? 0));
2167
+ const score = metricFromDelta(dx, 36) * 0.3 + metricFromDelta(dy, 36) * 0.3 + metricFromDelta(dw, 42) * 0.2 + metricFromDelta(dh, 42) * 0.2;
2168
+ layoutAcc += score;
2169
+ if (score < 0.58) {
2170
+ findings.push({
2171
+ severity: "medium",
2172
+ title: `\u5E03\u5C40\u504F\u5DEE: ${exp?.kind ?? exp?.type ?? "component"}`,
2173
+ detail: `\u4F4D\u7F6E/\u5C3A\u5BF8\u504F\u5DEE\u8F83\u5927 (dx=${dx}, dy=${dy}, dw=${dw}, dh=${dh})\u3002`
2174
+ });
2175
+ }
2176
+ } else {
2177
+ layoutAcc += 0.45;
2178
+ }
2179
+ const expColor = exp?.style?.bg ?? exp?.style?.textColor ?? "";
2180
+ const actColor = best?.style?.bg ?? best?.style?.textColor ?? "";
2181
+ const expFont = exp?.style?.fontSize ?? 0;
2182
+ const actFont = best?.style?.fontSize ?? 0;
2183
+ const colorScore = expColor && actColor ? expColor === actColor ? 1 : 0.4 : 0.6;
2184
+ const fontScore = expFont && actFont ? metricFromDelta(Math.abs(expFont - actFont), 4) : 0.6;
2185
+ styleAcc += colorScore * 0.6 + fontScore * 0.4;
2186
+ }
2187
+ const totalExpected = Math.max(1, expectedComponents.length);
2188
+ const layoutScore = Number((layoutAcc / totalExpected * 100).toFixed(1));
2189
+ const styleScore = Number((styleAcc / totalExpected * 100).toFixed(1));
2190
+ const expectedInteractions = expected.interactions ?? expected.blueprint?.interactions ?? [];
2191
+ const actualInteractions = actual.interactions ?? actual.blueprint?.interactions ?? [];
2192
+ let interactionScore = 0;
2193
+ if (expectedInteractions.length === 0) {
2194
+ interactionScore = actualInteractions.length > 0 ? 80 : 60;
2195
+ } else {
2196
+ const coverage = Math.min(1, actualInteractions.length / expectedInteractions.length);
2197
+ interactionScore = Number((coverage * 100).toFixed(1));
2198
+ if (coverage < 0.75) {
2199
+ findings.push({
2200
+ severity: "medium",
2201
+ title: "\u4EA4\u4E92\u8986\u76D6\u4E0D\u8DB3",
2202
+ detail: `\u671F\u671B\u4EA4\u4E92 ${expectedInteractions.length} \u6761\uFF0C\u5F53\u524D\u5B9E\u73B0\u4EC5\u8BC6\u522B\u5230 ${actualInteractions.length} \u6761\u3002`
2203
+ });
2204
+ }
2205
+ }
2206
+ const overallScore = Number(
2207
+ (layoutScore * 0.5 + styleScore * 0.3 + interactionScore * 0.2).toFixed(1)
2208
+ );
2209
+ if (matched / totalExpected < 0.8) {
2210
+ findings.push({
2211
+ severity: "high",
2212
+ title: "\u6838\u5FC3\u7EC4\u4EF6\u7F3A\u5931\u8F83\u591A",
2213
+ detail: `\u53EA\u5339\u914D\u5230 ${matched}/${totalExpected} \u4E2A\u671F\u671B\u7EC4\u4EF6\uFF0C\u5EFA\u8BAE\u5148\u8865\u9F50\u7ED3\u6784\u518D\u8C03\u6837\u5F0F\u3002`
2214
+ });
2215
+ }
2216
+ return {
2217
+ overallScore,
2218
+ layoutScore,
2219
+ styleScore,
2220
+ interactionScore,
2221
+ matchedComponents: matched,
2222
+ totalExpectedComponents: totalExpected,
2223
+ findings,
2224
+ nextPrompt: "\u8BF7\u57FA\u4E8E findings \u4F18\u5148\u4FEE\u590D high \u4E25\u91CD\u5EA6\u95EE\u9898\uFF0C\u7136\u540E\u6309 layout->style->interaction \u987A\u5E8F\u8FED\u4EE3\uFF1B\u6BCF\u6B21\u6539\u52A8\u540E\u91CD\u65B0\u622A\u56FE\u5E76\u518D\u6B21\u8C03\u7528 validate_implementation\u3002"
2225
+ };
2226
+ }
2227
+
2228
+ // src/engine/output.ts
2229
+ var MAX_TEXT_RESPONSE = 2e5;
2230
+ function isRecord(v) {
2231
+ return typeof v === "object" && v !== null && !Array.isArray(v);
2232
+ }
2233
+ function attachRuntimeMeta(value, toolName) {
2234
+ const runtime = getRuleRuntimeMeta();
2235
+ const meta = {
2236
+ tool: toolName,
2237
+ ruleProfile: runtime.profile,
2238
+ ruleConfidence: runtime.confidence,
2239
+ ruleSource: runtime.source
2240
+ };
2241
+ if (Array.isArray(value)) {
2242
+ return {
2243
+ meta,
2244
+ items: value
2245
+ };
2246
+ }
2247
+ if (isRecord(value)) {
2248
+ const existingMeta = isRecord(value.meta) ? value.meta : {};
2249
+ const { meta: _discard, ...rest } = value;
2250
+ return {
2251
+ meta: {
2252
+ ...existingMeta,
2253
+ ...meta
2254
+ },
2255
+ ...rest
2256
+ };
2257
+ }
2258
+ return { meta, value };
2259
+ }
2260
+ function toJsonOrThrow(value, toolName, hint) {
2261
+ const withMeta = attachRuntimeMeta(value, toolName);
2262
+ const output = JSON.stringify(withMeta, null, 2);
2263
+ if (output.length > MAX_TEXT_RESPONSE) {
2264
+ const advice = hint ?? "\u8BF7\u7F29\u5C0F page_height\uFF0C\u6216\u4F7F\u7528 offset/limit \u5206\u9875\u53C2\u6570\u5206\u6279\u62C9\u53D6\u3002";
2265
+ throw new Error(
2266
+ `${toolName} \u8F93\u51FA\u8FC7\u5927(${output.length} chars > ${MAX_TEXT_RESPONSE})\u3002\u5DF2\u53D6\u6D88\u8FD4\u56DE\uFF0C\u907F\u514D\u9759\u9ED8\u622A\u65AD\u5F71\u54CD\u8FD8\u539F\u5EA6\u3002${advice}`
2267
+ );
2268
+ }
2269
+ return output;
2270
+ }
2271
+ function resolvePaging(input, defaultLimit, maxLimit = 2e3) {
2272
+ let offset = Math.max(0, Math.floor(input.offset ?? 0));
2273
+ if (input.cursor) {
2274
+ const m = /^offset:(\d+)$/.exec(input.cursor.trim());
2275
+ if (!m) {
2276
+ throw new Error("cursor \u683C\u5F0F\u9519\u8BEF\uFF0C\u671F\u671B\u793A\u4F8B: offset:200");
2277
+ }
2278
+ offset = Number(m[1]);
2279
+ }
2280
+ const rawLimit = input.limit ?? defaultLimit;
2281
+ if (!Number.isFinite(rawLimit) || rawLimit <= 0) {
2282
+ throw new Error("limit \u5FC5\u987B\u662F\u6B63\u6574\u6570");
2283
+ }
2284
+ const limit = Math.min(maxLimit, Math.floor(rawLimit));
2285
+ return { offset, limit };
2286
+ }
2287
+ function paginateArray(items, paging) {
2288
+ const total = items.length;
2289
+ const start = Math.min(total, paging.offset);
2290
+ const end = Math.min(total, start + paging.limit);
2291
+ const sliced = items.slice(start, end);
2292
+ const hasMore = end < total;
2293
+ return {
2294
+ items: sliced,
2295
+ page: {
2296
+ offset: start,
2297
+ limit: paging.limit,
2298
+ total,
2299
+ hasMore,
2300
+ nextOffset: hasMore ? end : null,
2301
+ nextCursor: hasMore ? `offset:${end}` : null
2302
+ }
2303
+ };
2304
+ }
2305
+
2306
+ // src/engine/v3-tools.ts
2307
+ async function toolGetArtboardManifestPaged(url, offset = 0, limit = 100, cursor) {
2308
+ const wudi = await getWudi(url);
2309
+ const manifest = buildArtboardManifest(wudi.artboards);
2310
+ const paging = resolvePaging({ offset, limit, cursor }, 100, 1e3);
2311
+ const paged = paginateArray(manifest, paging);
2312
+ return toJsonOrThrow(
2313
+ {
2314
+ url,
2315
+ totalArtboards: paged.page.total,
2316
+ recommendedFirst: manifest.filter((x) => x.recommended).map((x) => x.name),
2317
+ pagination: paged.page,
2318
+ artboards: paged.items
2319
+ },
2320
+ "get_artboard_manifest"
2321
+ );
2322
+ }
2323
+ async function toolGetSceneGraph(url, artboardName, pageY = 0, pageHeight = 4e3, maxNodes = 900, offset = 0, limit = 300, cursor) {
2324
+ const wudi = await getWudi(url);
2325
+ const ab = findArtboard(wudi, artboardName);
2326
+ const scene = buildSceneGraph(ab, { pageY, pageHeight, maxNodes });
2327
+ const root = scene.nodes.find((n) => n.id === "root");
2328
+ const nonRoot = scene.nodes.filter((n) => n.id !== "root");
2329
+ const paging = resolvePaging({ offset, limit, cursor }, 300, 2e3);
2330
+ const paged = paginateArray(nonRoot, paging);
2331
+ return toJsonOrThrow(
2332
+ {
2333
+ ...scene,
2334
+ pagination: {
2335
+ target: "nodes",
2336
+ ...paged.page
2337
+ },
2338
+ nodes: root ? [root, ...paged.items] : paged.items
2339
+ },
2340
+ "get_scene_graph",
2341
+ "\u8BF7\u51CF\u5C0F page_height\uFF0C\u6216\u964D\u4F4E limit \u5E76\u7ED3\u5408 nextCursor \u5206\u9875\u62C9\u53D6\u3002"
2342
+ );
2343
+ }
2344
+ async function toolGetBlueprint(url, artboardName, pageY = 0, pageHeight = 4e3, maxNodes = 900, maxComponents = 500, offset = 0, limit = 200, cursor, includeInteractions = true) {
2345
+ const wudi = await getWudi(url);
2346
+ const ab = findArtboard(wudi, artboardName);
2347
+ const blueprint = await buildBlueprint(ab, { pageY, pageHeight, maxNodes, maxComponents });
2348
+ const paging = resolvePaging({ offset, limit, cursor }, 200, 2e3);
2349
+ const compPaged = paginateArray(blueprint.components, paging);
2350
+ const result = {
2351
+ ...blueprint,
2352
+ components: compPaged.items,
2353
+ interactions: includeInteractions ? blueprint.interactions : [],
2354
+ pagination: {
2355
+ target: "components",
2356
+ ...compPaged.page,
2357
+ interactionsIncluded: includeInteractions,
2358
+ interactionsTotal: blueprint.interactions.length
2359
+ }
2360
+ };
2361
+ return toJsonOrThrow(
2362
+ result,
2363
+ "get_blueprint",
2364
+ "\u8BF7\u51CF\u5C0F page_height\uFF0C\u6216\u4F7F\u7528 offset/limit \u7FFB\u9875\uFF1B\u4E5F\u53EF\u8BBE\u7F6E include_interactions=false \u518D\u5355\u72EC\u8C03\u7528 get_interaction_graph\u3002"
2365
+ );
2366
+ }
2367
+ async function toolGetInteractionGraph(url, artboardName, pageY = 0, pageHeight = 5e3, offset = 0, limit = 200, cursor) {
2368
+ const wudi = await getWudi(url);
2369
+ const ab = findArtboard(wudi, artboardName);
2370
+ const graph = await buildInteractionGraph(ab, { pageY, pageHeight });
2371
+ const paging = resolvePaging({ offset, limit, cursor }, 200, 2e3);
2372
+ const paged = paginateArray(graph.interactions, paging);
2373
+ return toJsonOrThrow(
2374
+ {
2375
+ ...graph,
2376
+ interactions: paged.items,
2377
+ pagination: {
2378
+ target: "interactions",
2379
+ ...paged.page
2380
+ }
2381
+ },
2382
+ "get_interaction_graph",
2383
+ "\u8BF7\u51CF\u5C0F page_height\uFF0C\u6216\u4F7F\u7528 offset/limit + nextCursor \u5206\u9875\u62C9\u53D6\u4EA4\u4E92\u3002"
2384
+ );
2385
+ }
2386
+ async function toolValidateImplementation(expectedJson, actualJson) {
2387
+ let expected = expectedJson;
2388
+ let actual = actualJson;
2389
+ try {
2390
+ expected = JSON.parse(expectedJson);
2391
+ } catch (e) {
2392
+ throw new Error("expected_json \u4E0D\u662F\u5408\u6CD5 JSON");
2393
+ }
2394
+ try {
2395
+ actual = JSON.parse(actualJson);
2396
+ } catch (e) {
2397
+ throw new Error("actual_json \u4E0D\u662F\u5408\u6CD5 JSON");
2398
+ }
2399
+ const result = validateImplementation(expected, actual);
2400
+ return toJsonOrThrow(result, "validate_implementation");
2401
+ }
2402
+
2403
+ // src/index.ts
2404
+ var cliArgs = process.argv.slice(2);
2405
+ var toolNames = [
2406
+ "get_artboard_manifest",
2407
+ "get_scene_graph",
2408
+ "get_blueprint",
2409
+ "get_interaction_graph",
2410
+ "validate_implementation"
2411
+ ];
2412
+ var hasHtmlFile = cliArgs.includes("--html-file");
2413
+ if (hasHtmlFile || cliArgs.length > 0 && toolNames.includes(cliArgs[0])) {
2414
+ if (hasHtmlFile) {
2415
+ const fileIdx = cliArgs.indexOf("--html-file");
2416
+ const htmlFile = cliArgs[fileIdx + 1];
2417
+ setHtmlOverride(htmlFile);
2418
+ cliArgs.splice(fileIdx, 2);
2419
+ }
2420
+ const tool = cliArgs[0];
2421
+ const toolArgs = cliArgs.slice(1);
2422
+ if (!tool) {
2423
+ console.error("Usage: node dist/index.js [--html-file <path>] <tool> [args...]");
2424
+ console.error(
2425
+ "Tools: get_artboard_manifest, get_scene_graph, get_blueprint, get_interaction_graph, validate_implementation"
2426
+ );
2427
+ process.exit(1);
2428
+ }
2429
+ try {
2430
+ const url = hasHtmlFile ? "file://local" : toolArgs[0] || "file://local";
2431
+ const argsOffset = hasHtmlFile ? 0 : 1;
2432
+ let result;
2433
+ switch (tool) {
2434
+ case "get_artboard_manifest":
2435
+ result = await toolGetArtboardManifestPaged(
2436
+ url,
2437
+ Number(toolArgs[argsOffset]) || 0,
2438
+ Number(toolArgs[argsOffset + 1]) || 100,
2439
+ toolArgs[argsOffset + 2]
2440
+ );
2441
+ break;
2442
+ case "get_scene_graph":
2443
+ result = await toolGetSceneGraph(
2444
+ url,
2445
+ toolArgs[argsOffset],
2446
+ Number(toolArgs[argsOffset + 1]) || 0,
2447
+ Number(toolArgs[argsOffset + 2]) || 4e3,
2448
+ Number(toolArgs[argsOffset + 3]) || 900,
2449
+ Number(toolArgs[argsOffset + 4]) || 0,
2450
+ Number(toolArgs[argsOffset + 5]) || 300,
2451
+ toolArgs[argsOffset + 6]
2452
+ );
2453
+ break;
2454
+ case "get_blueprint":
2455
+ result = await toolGetBlueprint(
2456
+ url,
2457
+ toolArgs[argsOffset],
2458
+ Number(toolArgs[argsOffset + 1]) || 0,
2459
+ Number(toolArgs[argsOffset + 2]) || 4e3,
2460
+ Number(toolArgs[argsOffset + 3]) || 900,
2461
+ Number(toolArgs[argsOffset + 4]) || 500,
2462
+ Number(toolArgs[argsOffset + 5]) || 0,
2463
+ Number(toolArgs[argsOffset + 6]) || 200,
2464
+ toolArgs[argsOffset + 7],
2465
+ toolArgs[argsOffset + 8] !== "false"
2466
+ );
2467
+ break;
2468
+ case "get_interaction_graph":
2469
+ result = await toolGetInteractionGraph(
2470
+ url,
2471
+ toolArgs[argsOffset],
2472
+ Number(toolArgs[argsOffset + 1]) || 0,
2473
+ Number(toolArgs[argsOffset + 2]) || 5e3,
2474
+ Number(toolArgs[argsOffset + 3]) || 0,
2475
+ Number(toolArgs[argsOffset + 4]) || 200,
2476
+ toolArgs[argsOffset + 5]
2477
+ );
2478
+ break;
2479
+ case "validate_implementation":
2480
+ result = await toolValidateImplementation(
2481
+ toolArgs[0] || "{}",
2482
+ toolArgs[1] || "{}"
2483
+ );
2484
+ break;
2485
+ default:
2486
+ console.error("Unknown tool:", tool);
2487
+ process.exit(1);
2488
+ }
2489
+ console.log(result);
2490
+ } catch (e) {
2491
+ console.error("Error:", e.message);
2492
+ process.exit(1);
2493
+ }
2494
+ process.exit(0);
2495
+ }
2496
+ var pkg = JSON.parse(
2497
+ readFileSync3(new URL("../package.json", import.meta.url), "utf-8")
2498
+ );
2499
+ var server = new McpServer({
2500
+ name: "sketch-design",
2501
+ version: pkg.version
2502
+ });
2503
+ server.tool(
2504
+ "get_artboard_manifest",
2505
+ "V3\u63A8\u8350\u5165\u53E3\uFF1A\u8FD4\u56DE\u753B\u677F\u6E05\u5355\u3001\u590D\u6742\u5EA6\u8BC4\u5206\u3001\u63A8\u8350\u4F18\u5148\u5B9E\u73B0\u7684\u9875\u9762\u3002\u652F\u6301 offset/limit/cursor \u5206\u9875\u3002",
2506
+ {
2507
+ url: z.string().describe("\u8BBE\u8BA1\u7A3F\u9875\u9762 URL"),
2508
+ offset: z.number().describe("\u5206\u9875\u8D77\u59CB\u504F\u79FB").default(0),
2509
+ limit: z.number().describe("\u5206\u9875\u5927\u5C0F").default(100),
2510
+ cursor: z.string().optional().describe("\u5206\u9875\u6E38\u6807\uFF0C\u683C\u5F0F\u793A\u4F8B: offset:200")
2511
+ },
2512
+ async ({ url, offset = 0, limit = 100, cursor }) => {
2513
+ const data = await toolGetArtboardManifestPaged(url, offset, limit, cursor);
2514
+ return { content: [{ type: "text", text: data }] };
2515
+ }
2516
+ );
2517
+ server.tool(
2518
+ "get_scene_graph",
2519
+ "V3\u6838\u5FC3\uFF1A\u8FD4\u56DE\u5C42\u7EA7\u5316 scene graph\uFF08\u7236\u5B50\u7ED3\u6784\u3001\u7EA6\u675F\u3001\u6837\u5F0F\uFF09\uFF0C\u652F\u6301 nodes \u5206\u9875\uFF0C\u907F\u514D\u5927\u7ED3\u679C\u622A\u65AD\u3002",
2520
+ {
2521
+ url: z.string().describe("\u8BBE\u8BA1\u7A3F\u9875\u9762 URL"),
2522
+ artboard: z.string().describe("\u753B\u677F\u540D\u79F0"),
2523
+ page_y: z.number().describe("Y \u8D77\u59CB\u5750\u6807").default(0),
2524
+ page_height: z.number().describe("Y \u8303\u56F4\u9AD8\u5EA6").default(4e3),
2525
+ max_nodes: z.number().describe("\u6700\u5927\u8FD4\u56DE\u8282\u70B9\u6570").default(900),
2526
+ offset: z.number().describe("nodes \u5206\u9875\u8D77\u59CB\u504F\u79FB").default(0),
2527
+ limit: z.number().describe("nodes \u5206\u9875\u5927\u5C0F").default(300),
2528
+ cursor: z.string().optional().describe("\u5206\u9875\u6E38\u6807\uFF0C\u683C\u5F0F\u793A\u4F8B: offset:300")
2529
+ },
2530
+ async ({
2531
+ url,
2532
+ artboard,
2533
+ page_y = 0,
2534
+ page_height = 4e3,
2535
+ max_nodes = 900,
2536
+ offset = 0,
2537
+ limit = 300,
2538
+ cursor
2539
+ }) => {
2540
+ const data = await toolGetSceneGraph(
2541
+ url,
2542
+ artboard,
2543
+ page_y,
2544
+ page_height,
2545
+ max_nodes,
2546
+ offset,
2547
+ limit,
2548
+ cursor
2549
+ );
2550
+ return { content: [{ type: "text", text: data }] };
2551
+ }
2552
+ );
2553
+ server.tool(
2554
+ "get_blueprint",
2555
+ "V3\u63A8\u8350\uFF1A\u8FD4\u56DE\u53EF\u76F4\u63A5\u7F16\u7801\u7684\u5B9E\u73B0\u5951\u7EA6\uFF08layout/tokens/components/interactions/hints\uFF09\uFF0C\u652F\u6301 components \u5206\u9875\u3002",
2556
+ {
2557
+ url: z.string().describe("\u8BBE\u8BA1\u7A3F\u9875\u9762 URL"),
2558
+ artboard: z.string().describe("\u753B\u677F\u540D\u79F0"),
2559
+ page_y: z.number().describe("Y \u8D77\u59CB\u5750\u6807").default(0),
2560
+ page_height: z.number().describe("Y \u8303\u56F4\u9AD8\u5EA6").default(4e3),
2561
+ max_nodes: z.number().describe("\u6700\u5927\u8282\u70B9\u6570").default(900),
2562
+ max_components: z.number().describe("\u6700\u5927\u7EC4\u4EF6\u5019\u9009\u6570").default(500),
2563
+ offset: z.number().describe("components \u5206\u9875\u8D77\u59CB\u504F\u79FB").default(0),
2564
+ limit: z.number().describe("components \u5206\u9875\u5927\u5C0F").default(200),
2565
+ cursor: z.string().optional().describe("\u5206\u9875\u6E38\u6807\uFF0C\u683C\u5F0F\u793A\u4F8B: offset:200"),
2566
+ include_interactions: z.boolean().describe("\u662F\u5426\u5728 blueprint \u4E2D\u5305\u542B interactions\uFF08false \u65F6\u53EF\u5355\u72EC\u8C03 get_interaction_graph\uFF09").default(true)
2567
+ },
2568
+ async ({
2569
+ url,
2570
+ artboard,
2571
+ page_y = 0,
2572
+ page_height = 4e3,
2573
+ max_nodes = 900,
2574
+ max_components = 500,
2575
+ offset = 0,
2576
+ limit = 200,
2577
+ cursor,
2578
+ include_interactions = true
2579
+ }) => {
2580
+ const data = await toolGetBlueprint(
2581
+ url,
2582
+ artboard,
2583
+ page_y,
2584
+ page_height,
2585
+ max_nodes,
2586
+ max_components,
2587
+ offset,
2588
+ limit,
2589
+ cursor,
2590
+ include_interactions
2591
+ );
2592
+ return { content: [{ type: "text", text: data }] };
2593
+ }
2594
+ );
2595
+ server.tool(
2596
+ "get_interaction_graph",
2597
+ "V3\u63A8\u8350\uFF1A\u7ED3\u6784\u5316\u4EA4\u4E92\u56FE\uFF08trigger/condition/action + target \u7ED1\u5B9A\uFF09\uFF0C\u652F\u6301\u5206\u9875\u3002",
2598
+ {
2599
+ url: z.string().describe("\u8BBE\u8BA1\u7A3F\u9875\u9762 URL"),
2600
+ artboard: z.string().describe("\u753B\u677F\u540D\u79F0"),
2601
+ page_y: z.number().describe("Y \u8D77\u59CB\u5750\u6807").default(0),
2602
+ page_height: z.number().describe("Y \u8303\u56F4\u9AD8\u5EA6").default(5e3),
2603
+ offset: z.number().describe("interactions \u5206\u9875\u8D77\u59CB\u504F\u79FB").default(0),
2604
+ limit: z.number().describe("interactions \u5206\u9875\u5927\u5C0F").default(200),
2605
+ cursor: z.string().optional().describe("\u5206\u9875\u6E38\u6807\uFF0C\u683C\u5F0F\u793A\u4F8B: offset:200")
2606
+ },
2607
+ async ({
2608
+ url,
2609
+ artboard,
2610
+ page_y = 0,
2611
+ page_height = 5e3,
2612
+ offset = 0,
2613
+ limit = 200,
2614
+ cursor
2615
+ }) => {
2616
+ const data = await toolGetInteractionGraph(
2617
+ url,
2618
+ artboard,
2619
+ page_y,
2620
+ page_height,
2621
+ offset,
2622
+ limit,
2623
+ cursor
2624
+ );
2625
+ return { content: [{ type: "text", text: data }] };
2626
+ }
2627
+ );
2628
+ server.tool(
2629
+ "validate_implementation",
2630
+ "V3\u95ED\u73AF\uFF1A\u5BF9\u6BD4\u671F\u671B blueprint \u4E0E\u5B9E\u73B0\u5FEB\u7167\uFF0C\u8F93\u51FA\u5E03\u5C40/\u6837\u5F0F/\u4EA4\u4E92\u8BC4\u5206\u4E0E\u4FEE\u590D\u5EFA\u8BAE\u3002",
2631
+ {
2632
+ expected_json: z.string().describe("\u671F\u671B blueprint JSON \u5B57\u7B26\u4E32"),
2633
+ actual_json: z.string().describe("\u5B9E\u73B0\u5FEB\u7167 JSON \u5B57\u7B26\u4E32")
2634
+ },
2635
+ async ({ expected_json, actual_json }) => {
2636
+ const data = await toolValidateImplementation(expected_json, actual_json);
2637
+ return { content: [{ type: "text", text: data }] };
2638
+ }
2639
+ );
2640
+ var transport = new StdioServerTransport();
2641
+ await server.connect(transport);
2642
+ //# sourceMappingURL=index.js.map