@codify-ai/mcp-client 1.0.25 → 1.0.27
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 +369 -128
- package/dist/rules.md +23 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -25,8 +25,8 @@ const CODIFY_OUTPUT_DIR = ".codify-output";
|
|
|
25
25
|
const isEmpty = (value) => value === null || value === void 0 || value === "" || typeof value === "string" && value.trim() === "";
|
|
26
26
|
function buildSuccessText(result, actionName = "操作") {
|
|
27
27
|
let responseText = `✅ ${actionName}已成功完成`;
|
|
28
|
-
if (result.
|
|
29
|
-
-
|
|
28
|
+
if (result.targetNodeId) responseText += `
|
|
29
|
+
- 节点 ID: ${result.targetNodeId}`;
|
|
30
30
|
if (result.documentId) responseText += `
|
|
31
31
|
- 文档: ${result.documentName || ""} (${result.documentId})`;
|
|
32
32
|
if (result.documentPageId) responseText += `
|
|
@@ -112,7 +112,7 @@ async function saveCodeAndResources({
|
|
|
112
112
|
documentId,
|
|
113
113
|
documentPageId,
|
|
114
114
|
contentId,
|
|
115
|
-
|
|
115
|
+
targetNodeId,
|
|
116
116
|
nodeName,
|
|
117
117
|
code,
|
|
118
118
|
resourcePath,
|
|
@@ -121,11 +121,11 @@ async function saveCodeAndResources({
|
|
|
121
121
|
image,
|
|
122
122
|
isSelection = false
|
|
123
123
|
}) {
|
|
124
|
-
let targetDir = outDir ? path.resolve(baseDir, outDir) : documentId && documentPageId ? path.join(baseDir, CODIFY_DOC_DIR, String(documentId).replace(/:/g, "-"), String(documentPageId).replace(/:/g, "-"), contentId && !isSelection && !
|
|
124
|
+
let targetDir = outDir ? path.resolve(baseDir, outDir) : documentId && documentPageId ? path.join(baseDir, CODIFY_DOC_DIR, String(documentId).replace(/:/g, "-"), String(documentPageId).replace(/:/g, "-"), contentId && !isSelection && !targetNodeId ? "code" : "") : isSelection ? path.join(baseDir, CODIFY_OUTPUT_DIR, "selection") : path.join(baseDir, `${CODIFY_OUTPUT_DIR}${contentId ? "/" + contentId : ""}`);
|
|
125
125
|
if (!existsSync(targetDir)) mkdirSync(targetDir, { recursive: true });
|
|
126
|
-
const safeId = String(
|
|
126
|
+
const safeId = String(targetNodeId || Date.now()).replace(/:/g, "-");
|
|
127
127
|
const safeName = String(nodeName || (isSelection ? "selection" : "node")).replace(/[^a-zA-Z0-9\u4e00-\u9fa5_-]/g, "-");
|
|
128
|
-
const htmlFileName = isSelection ||
|
|
128
|
+
const htmlFileName = isSelection || targetNodeId || nodeName ? `${safeName}-${safeId}.html` : `${contentId || "index"}.html`;
|
|
129
129
|
const htmlPath = path.join(targetDir, htmlFileName);
|
|
130
130
|
if (code) {
|
|
131
131
|
await fs.writeFile(htmlPath, code, "utf8");
|
|
@@ -138,11 +138,82 @@ async function saveCodeAndResources({
|
|
|
138
138
|
]);
|
|
139
139
|
return { targetDir, htmlFileName, htmlPath, shapeCount: stats[0], svgCount: stats[1], imageCount: stats[2], resourcePathMap: resPathMap };
|
|
140
140
|
}
|
|
141
|
+
const zh = {
|
|
142
|
+
createPage: {
|
|
143
|
+
description: "将代码发送到 Codify 插件转换为设计稿。\n\n【极致性能要求】若纯 HTML 已保存为本地文件,你必须且只能通过 filePath 传入该文件的绝对路径。严禁使用 Read 工具读取文件内容,严禁将大段代码写入 code 参数。工具底层会自动读取并传输,以节省 Token。只有当代码是你在当前对话中临时生成且尚未写入文件时,才允许使用 code 参数。\n\n⚠️ 逆向转译警告:严禁直接发送 Vue/React 等包含动态绑定 ({{}}) 或框架指令的业务代码!若要将业务代码转为设计稿,必须先执行【逆向转译】(参考 codify://generation-rules),将其转换为带静态假数据的纯 HTML 后再发送。",
|
|
144
|
+
code: "【可选】要发送的 HTML 代码内容。仅当代码是临时生成且未保存为文件时使用。大段代码严禁使用此参数。",
|
|
145
|
+
filePath: "【可选】本地 HTML 文件的绝对路径。若文件已存在本地,必须传此参数,工具会自动读取并执行落盘。",
|
|
146
|
+
projectDir: "【必填】用户当前工作区的根目录绝对路径",
|
|
147
|
+
saveCodeToLocal: "是否将插件返回的渲染结果保存到本地 .codify 目录(落盘机制)"
|
|
148
|
+
},
|
|
149
|
+
updateNode: {
|
|
150
|
+
description: "将修改后的 HTML 代码发回 MasterGo 画布进行【局部修改】(如改颜色、加文字、改间距等)。完成代码修改后,你必须同步调用此工具。注意:此工具一次只能同步一个节点及其关联子节点,必须传入 code。",
|
|
151
|
+
documentId: "当前 MasterGo 文档 ID。",
|
|
152
|
+
documentPageId: "当前 MasterGo 页面 ID。",
|
|
153
|
+
targetNodeId: "【选填】目标图层 ID (例如 123:456)。如果不传,则默认更新 MasterGo 中当前选中的图层。",
|
|
154
|
+
code: "【必填】修改后的 HTML 代码片段。必须包含 data-node-id。"
|
|
155
|
+
},
|
|
156
|
+
syncToDesign: {
|
|
157
|
+
description: "将本地完整的静态 HTML 文件内容同步覆盖到 MasterGo 画布进行【全量同步】(如保存整个页面、复杂的模块同步)。必须传入根节点 ID (rootId) 以确保层级正确。",
|
|
158
|
+
documentId: "当前 MasterGo 文档 ID。",
|
|
159
|
+
documentPageId: "当前 MasterGo 页面 ID。",
|
|
160
|
+
targetNodeId: "【必填】页面的根节点 ID (rootId)。",
|
|
161
|
+
filePath: "【必填】本地静态 HTML 文件的绝对路径(通常位于 .codify/... 目录下)。工具会自动读取内容。严禁传入 .vue/.tsx 业务代码路径!"
|
|
162
|
+
},
|
|
163
|
+
getSelectionCode: {
|
|
164
|
+
description: "获取 MasterGo 中当前选中图层(或指定图层)的代码。如果你获取的是子节点,工具只会将代码作为纯文本返回给你进行局部修改上下文,绝对不会在本地生成烦人的 HTML 碎片文件!若是根节点则会自动将完整页面代码同步保存到 .codify 目录。",
|
|
165
|
+
projectDir: "【必填】用户当前工作区的根目录绝对路径",
|
|
166
|
+
targetNodeId: "【选填】MasterGo图层ID (例如 123:456)。如果提供,将直接拉取该ID的代码;如果不提供,将拉取当前选中图层的代码。",
|
|
167
|
+
syncToBase: "【选填】是否将获取到的子图层代码同步回本地 .codify 目录下的基准 HTML 文件(合并更新)。默认为 true。"
|
|
168
|
+
},
|
|
169
|
+
createComponent: {
|
|
170
|
+
description: '创建一个 MasterGo 母版组件或组件集(变体)。应当使用 HTML 格式并包含 data-type="component" 属性。',
|
|
171
|
+
code: '组件的 HTML 结构。必须包含 data-type="component" 或 "component-set"。'
|
|
172
|
+
},
|
|
173
|
+
getDesignDiff: {
|
|
174
|
+
description: `获取设计稿与本地代码的差异。
|
|
175
|
+
【标准双向同步规程】:
|
|
176
|
+
1. 准备:将当前项目业务代码(Vue/React)按 rules.md 规范物理逆推为纯静态 HTML。
|
|
177
|
+
2. 刷新:将转译产物覆盖写入 .codify 目录对应的基准 HTML 文件。
|
|
178
|
+
3. 比对:调用此工具比对“基准文件”与“画布现状”获得差异列表。
|
|
179
|
+
4. 决策:展示差异,由用户决定执行“全量同步到设计稿”或“按差异局部同步到项目”。`,
|
|
180
|
+
projectDir: "【必填】用户当前工作区的根目录绝对路径",
|
|
181
|
+
targetNodeId: "【选填】MasterGo图层ID (例如 123:456)。如果不传,则默认获取当前选中图层的代码进行对比。",
|
|
182
|
+
filePath: "【选填】本地基准 HTML 文件的绝对路径。如果已通过 write_to_file 更新了基准文件,请直接传此路径。"
|
|
183
|
+
},
|
|
184
|
+
getCodeList: {
|
|
185
|
+
description: "获取所有可用的代码列表",
|
|
186
|
+
inputSchema: "无需参数获取代码列表"
|
|
187
|
+
},
|
|
188
|
+
design: {
|
|
189
|
+
description: "根据需求生成符合 Codify 规范的 HTML+CSS 代码。生成完成后,应调用 agent_create_page 将代码发送到画布。",
|
|
190
|
+
requirement: '界面需求描述,例如:"一个美观的登录页面"、"现代化的仪表盘界面"等。'
|
|
191
|
+
},
|
|
192
|
+
getUserInfo: {
|
|
193
|
+
description: "获取当前登录用户的信息,包括配额、团队等",
|
|
194
|
+
inputSchema: "获取当前用户信息"
|
|
195
|
+
},
|
|
196
|
+
getCode: {
|
|
197
|
+
description: '【特定场景】通过 contentId 从 Codify 插件获取代码。常规的"获取选中代码"请优先使用 get_selection_code 工具。',
|
|
198
|
+
contentId: "从Codify插件复制图层的指令 (contentId)",
|
|
199
|
+
documentId: "当前 MasterGo 文档 ID。",
|
|
200
|
+
documentPageId: "当前 MasterGo 页面 ID。",
|
|
201
|
+
projectDir: "【必填】用户当前工作区的根目录绝对路径",
|
|
202
|
+
outDir: "【必填】保存代码和资源的绝对路径"
|
|
203
|
+
},
|
|
204
|
+
removeNode: {
|
|
205
|
+
description: "在 MasterGo 画布中执行删除节点操作。支持通过 targetNodeId 指定 ID,或在不传 ID 时默认删除当前选中图层。",
|
|
206
|
+
documentId: "当前 MasterGo 文档 ID。",
|
|
207
|
+
documentPageId: "当前 MasterGo 页面 ID。",
|
|
208
|
+
targetNodeId: "【选填】要删除的目标图层 ID (例如 123:456)。如果不传,则默认删除 MasterGo 中当前选中的图层。"
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
const i18n = zh;
|
|
141
212
|
const createComponentTool = {
|
|
142
213
|
name: "agent_create_component",
|
|
143
|
-
description:
|
|
214
|
+
description: i18n.createComponent.description,
|
|
144
215
|
inputSchema: {
|
|
145
|
-
code: z.string().describe(
|
|
216
|
+
code: z.string().describe(i18n.createComponent.code)
|
|
146
217
|
},
|
|
147
218
|
handler: async (args) => {
|
|
148
219
|
const { code } = args;
|
|
@@ -153,12 +224,12 @@ const createComponentTool = {
|
|
|
153
224
|
};
|
|
154
225
|
const createPageTool = {
|
|
155
226
|
name: "agent_create_page",
|
|
156
|
-
description:
|
|
227
|
+
description: i18n.createPage.description,
|
|
157
228
|
inputSchema: {
|
|
158
|
-
code: z.string().optional().describe(
|
|
159
|
-
filePath: z.string().optional().describe(
|
|
160
|
-
projectDir: z.string().describe(
|
|
161
|
-
saveCodeToLocal: z.boolean().default(true).describe(
|
|
229
|
+
code: z.string().optional().describe(i18n.createPage.code),
|
|
230
|
+
filePath: z.string().optional().describe(i18n.createPage.filePath),
|
|
231
|
+
projectDir: z.string().describe(i18n.createPage.projectDir),
|
|
232
|
+
saveCodeToLocal: z.boolean().default(true).describe(i18n.createPage.saveCodeToLocal)
|
|
162
233
|
},
|
|
163
234
|
handler: async (args) => {
|
|
164
235
|
const { projectDir, saveCodeToLocal = true } = args;
|
|
@@ -184,7 +255,7 @@ const createPageTool = {
|
|
|
184
255
|
code: data.htmlCode || code,
|
|
185
256
|
documentId: data.documentId,
|
|
186
257
|
documentPageId: data.documentPageId,
|
|
187
|
-
|
|
258
|
+
targetNodeId: data.targetNodeId,
|
|
188
259
|
nodeName: data.nodeName,
|
|
189
260
|
resourcePath: data.resourcePath,
|
|
190
261
|
shape: data.shape,
|
|
@@ -203,9 +274,9 @@ const createPageTool = {
|
|
|
203
274
|
};
|
|
204
275
|
const designTool = {
|
|
205
276
|
name: "design",
|
|
206
|
-
description:
|
|
277
|
+
description: i18n.design.description,
|
|
207
278
|
inputSchema: {
|
|
208
|
-
requirement: z.string().describe(
|
|
279
|
+
requirement: z.string().describe(i18n.design.requirement)
|
|
209
280
|
},
|
|
210
281
|
handler: async (args) => {
|
|
211
282
|
const { requirement } = args;
|
|
@@ -235,13 +306,13 @@ ${generationRules}
|
|
|
235
306
|
};
|
|
236
307
|
const getCodeTool = {
|
|
237
308
|
name: "get_code",
|
|
238
|
-
description:
|
|
309
|
+
description: i18n.getCode.description,
|
|
239
310
|
inputSchema: {
|
|
240
|
-
contentId: z.string().describe(
|
|
241
|
-
documentId: z.string().optional().describe(
|
|
242
|
-
documentPageId: z.string().optional().describe(
|
|
243
|
-
projectDir: z.string().describe(
|
|
244
|
-
outDir: z.string().describe(
|
|
311
|
+
contentId: z.string().describe(i18n.getCode.contentId),
|
|
312
|
+
documentId: z.string().optional().describe(i18n.getCode.documentId),
|
|
313
|
+
documentPageId: z.string().optional().describe(i18n.getCode.documentPageId),
|
|
314
|
+
projectDir: z.string().describe(i18n.getCode.projectDir),
|
|
315
|
+
outDir: z.string().describe(i18n.getCode.outDir)
|
|
245
316
|
},
|
|
246
317
|
handler: async (args) => {
|
|
247
318
|
const { contentId, outDir, projectDir, documentId, documentPageId } = args;
|
|
@@ -277,8 +348,8 @@ const getCodeTool = {
|
|
|
277
348
|
};
|
|
278
349
|
const getCodeListTool = {
|
|
279
350
|
name: "get_code_list",
|
|
280
|
-
description:
|
|
281
|
-
inputSchema: z.object({}).describe(
|
|
351
|
+
description: i18n.getCodeList.description,
|
|
352
|
+
inputSchema: z.object({}).describe(i18n.getCodeList.inputSchema),
|
|
282
353
|
handler: async () => {
|
|
283
354
|
const { data, error } = await callApi("GET", "/api/getCodeList");
|
|
284
355
|
if (error) return { content: [{ type: "text", text: `❌ 获取失败: ${error.message}` }], isError: true };
|
|
@@ -300,31 +371,63 @@ const getCodeListTool = {
|
|
|
300
371
|
};
|
|
301
372
|
const getSelectionCodeTool = {
|
|
302
373
|
name: "get_selection_code",
|
|
303
|
-
description:
|
|
374
|
+
description: i18n.getSelectionCode.description,
|
|
304
375
|
inputSchema: {
|
|
305
|
-
projectDir: z.string().describe(
|
|
306
|
-
|
|
376
|
+
projectDir: z.string().describe(i18n.getSelectionCode.projectDir),
|
|
377
|
+
targetNodeId: z.string().optional().describe(i18n.getSelectionCode.targetNodeId),
|
|
378
|
+
syncToBase: z.boolean().default(true).describe(i18n.getSelectionCode.syncToBase)
|
|
307
379
|
},
|
|
380
|
+
/**
|
|
381
|
+
* 工具处理器
|
|
382
|
+
* @param args.syncToBase 是否尝试将子节点代码合并到基准 HTML 文件
|
|
383
|
+
* @param args._depth 内部参数,用于防止递归获取根节点时出现无限循环
|
|
384
|
+
*/
|
|
308
385
|
handler: async (args) => {
|
|
309
|
-
const { projectDir, id } = args;
|
|
386
|
+
const { projectDir, targetNodeId: id, syncToBase = true, _depth = 0 } = args;
|
|
387
|
+
if (_depth > 2) {
|
|
388
|
+
return {
|
|
389
|
+
content: [
|
|
390
|
+
{ type: "text", text: "❌ 递归获取根节点深度过深,已停止。" }
|
|
391
|
+
],
|
|
392
|
+
isError: true
|
|
393
|
+
};
|
|
394
|
+
}
|
|
310
395
|
const endpoint = id ? `/api/getSelectionCode?id=${encodeURIComponent(id)}` : "/api/getSelectionCode";
|
|
311
396
|
const { data, error } = await callApi("GET", endpoint);
|
|
312
397
|
if (error) {
|
|
313
398
|
if (error.status === 400 && error.data?.error === "NoSelection") {
|
|
314
|
-
return {
|
|
399
|
+
return {
|
|
400
|
+
content: [
|
|
401
|
+
{
|
|
402
|
+
type: "text",
|
|
403
|
+
text: "❌ 获取失败: 没有选中任何图层。请先在 MasterGo 中选中一个图层。"
|
|
404
|
+
}
|
|
405
|
+
],
|
|
406
|
+
isError: true
|
|
407
|
+
};
|
|
315
408
|
}
|
|
316
|
-
return {
|
|
409
|
+
return {
|
|
410
|
+
content: [{ type: "text", text: `❌ 获取失败: ${error.message}` }],
|
|
411
|
+
isError: true
|
|
412
|
+
};
|
|
317
413
|
}
|
|
318
414
|
const baseDir = projectDir ? path.resolve(projectDir) : process.cwd();
|
|
319
415
|
const nodeInfo = data.nodeInfo || {};
|
|
320
|
-
const {
|
|
321
|
-
|
|
416
|
+
const {
|
|
417
|
+
rootId,
|
|
418
|
+
documentId,
|
|
419
|
+
documentPageId,
|
|
420
|
+
targetNodeId,
|
|
421
|
+
nodeName,
|
|
422
|
+
parentId
|
|
423
|
+
} = nodeInfo;
|
|
424
|
+
if (targetNodeId === rootId) {
|
|
425
|
+
console.log(`[getSelectionCode] 当前节点ID: ${targetNodeId}, 根节点ID: ${rootId}`);
|
|
322
426
|
const saveResult = await saveCodeAndResources({
|
|
323
427
|
baseDir,
|
|
324
428
|
documentId,
|
|
325
429
|
documentPageId,
|
|
326
|
-
|
|
327
|
-
// 使用 rootId 作为文件名后缀
|
|
430
|
+
targetNodeId: rootId,
|
|
328
431
|
nodeName,
|
|
329
432
|
code: data.code,
|
|
330
433
|
resourcePath: data.resourcePath,
|
|
@@ -332,70 +435,108 @@ const getSelectionCodeTool = {
|
|
|
332
435
|
svg: data.svg,
|
|
333
436
|
image: data.image
|
|
334
437
|
});
|
|
335
|
-
console.log("saveResult", saveResult);
|
|
336
438
|
return {
|
|
337
439
|
content: [
|
|
338
440
|
{
|
|
339
441
|
type: "text",
|
|
340
442
|
text: `✅ 成功获取并保存根节点代码
|
|
341
|
-
- 节点: ${nodeName} (${
|
|
342
|
-
-
|
|
343
|
-
|
|
344
|
-
请查看该文件了解当前结构。`
|
|
443
|
+
- 节点: ${nodeName} (${targetNodeId})
|
|
444
|
+
- 根节点: ${rootId}
|
|
445
|
+
- 文件: ${saveResult.htmlPath}。`
|
|
345
446
|
}
|
|
346
447
|
]
|
|
347
448
|
};
|
|
348
449
|
} else {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
450
|
+
console.log(`[getSelectionCode] 当前节点ID: ${targetNodeId}, 根节点ID: ${rootId}`);
|
|
451
|
+
let mergedToFilePath = "";
|
|
452
|
+
if (syncToBase && documentId && documentPageId) {
|
|
453
|
+
const targetPageDir = path.join(
|
|
454
|
+
baseDir,
|
|
455
|
+
CODIFY_DOC_DIR,
|
|
456
|
+
String(documentId).replace(/:/g, "-"),
|
|
457
|
+
String(documentPageId).replace(/:/g, "-")
|
|
458
|
+
);
|
|
459
|
+
const safeRootId = String(rootId).replace(/:/g, "-");
|
|
460
|
+
let rootFile = "";
|
|
461
|
+
const findRootFile = async () => {
|
|
462
|
+
if (existsSync(targetPageDir)) {
|
|
463
|
+
const files = await fs.readdir(targetPageDir);
|
|
464
|
+
return files.find((f) => f.endsWith(`-${safeRootId}.html`)) || "";
|
|
465
|
+
}
|
|
466
|
+
return "";
|
|
467
|
+
};
|
|
468
|
+
rootFile = await findRootFile();
|
|
469
|
+
if (!rootFile && _depth === 0) {
|
|
470
|
+
console.log(
|
|
471
|
+
`[getSelectionCode] 基准文件不存在,自动拉取根节点: ${rootId}`
|
|
472
|
+
);
|
|
473
|
+
await getSelectionCodeTool.handler({
|
|
474
|
+
projectDir,
|
|
475
|
+
targetNodeId: rootId,
|
|
476
|
+
syncToBase: true,
|
|
477
|
+
_depth: _depth + 1
|
|
478
|
+
});
|
|
479
|
+
rootFile = await findRootFile();
|
|
480
|
+
}
|
|
481
|
+
if (rootFile) {
|
|
482
|
+
const rootFilePath = path.join(targetPageDir, rootFile);
|
|
483
|
+
try {
|
|
484
|
+
const htmlContent = await fs.readFile(rootFilePath, "utf8");
|
|
485
|
+
const rootNode = parse(htmlContent);
|
|
486
|
+
const targetElement = rootNode.querySelector(
|
|
487
|
+
`[data-node-id="${targetNodeId}"]`
|
|
488
|
+
);
|
|
489
|
+
if (targetElement) {
|
|
490
|
+
targetElement.replaceWith(data.code);
|
|
491
|
+
} else if (parentId) {
|
|
492
|
+
const parentElement = rootNode.querySelector(
|
|
493
|
+
`[data-node-id="${parentId}"]`
|
|
494
|
+
);
|
|
495
|
+
if (parentElement) {
|
|
496
|
+
parentElement.insertAdjacentHTML("beforeend", data.code);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
if (targetElement || parentId && rootNode.querySelector(`[data-node-id="${parentId}"]`)) {
|
|
500
|
+
await fs.writeFile(rootFilePath, rootNode.toString(), "utf8");
|
|
501
|
+
mergedToFilePath = rootFilePath;
|
|
502
|
+
}
|
|
503
|
+
} catch (err) {
|
|
504
|
+
console.error("合并到基准文件操作出错:", err);
|
|
505
|
+
}
|
|
374
506
|
}
|
|
375
|
-
} else {
|
|
376
|
-
return { content: [{ type: "text", text: `❌ 更新失败: 在文件 ${rootFile} 中找不到 data-node-id="${nodeId}" 的元素,且未提供 parentId 作为后备。` }], isError: true };
|
|
377
507
|
}
|
|
378
|
-
await fs.writeFile(rootFilePath, rootNode.toString(), "utf8");
|
|
379
508
|
await saveCodeAndResources({
|
|
380
509
|
baseDir,
|
|
381
510
|
documentId,
|
|
382
511
|
documentPageId,
|
|
383
|
-
|
|
512
|
+
targetNodeId,
|
|
384
513
|
nodeName,
|
|
385
514
|
code: "",
|
|
386
|
-
//
|
|
515
|
+
// 核心改动:强制将此参数留空,彻底阻断 .html 碎片文件的生成
|
|
387
516
|
resourcePath: data.resourcePath,
|
|
388
517
|
shape: data.shape,
|
|
389
518
|
svg: data.svg,
|
|
390
519
|
image: data.image
|
|
391
|
-
})
|
|
520
|
+
});
|
|
521
|
+
let successText = `✅ 成功获取子节点代码
|
|
522
|
+
- 节点: ${nodeName} (${targetNodeId})
|
|
523
|
+
- 根节点: ${rootId}`;
|
|
524
|
+
if (mergedToFilePath) {
|
|
525
|
+
successText += `
|
|
526
|
+
- ⚠️ (自动机制) 子节点最新代码已合并备份至本地基准 HTML: ${mergedToFilePath}`;
|
|
527
|
+
} else {
|
|
528
|
+
successText += `
|
|
529
|
+
- 💡 提示: 子节点代码已提取到对话上下文中,未生成本地 HTML 碎片文件。可以直接针对返回代码进行修改。`;
|
|
530
|
+
}
|
|
392
531
|
return {
|
|
393
532
|
content: [
|
|
394
533
|
{
|
|
395
534
|
type: "text",
|
|
396
|
-
text:
|
|
397
|
-
|
|
398
|
-
|
|
535
|
+
text: `${successText}
|
|
536
|
+
|
|
537
|
+
代码内容:
|
|
538
|
+
|
|
539
|
+
${data.code}`
|
|
399
540
|
}
|
|
400
541
|
]
|
|
401
542
|
};
|
|
@@ -404,8 +545,8 @@ const getSelectionCodeTool = {
|
|
|
404
545
|
};
|
|
405
546
|
const getUserInfoTool = {
|
|
406
547
|
name: "get_user_info",
|
|
407
|
-
description:
|
|
408
|
-
inputSchema: z.object({}).describe(
|
|
548
|
+
description: i18n.getUserInfo.description,
|
|
549
|
+
inputSchema: z.object({}).describe(i18n.getUserInfo.inputSchema),
|
|
409
550
|
handler: async () => {
|
|
410
551
|
const { data, error } = await callApi("GET", "/api/getUserInfo");
|
|
411
552
|
if (error) return { content: [{ type: "text", text: `❌ 获取失败: ${error.message}` }], isError: true };
|
|
@@ -434,67 +575,95 @@ const getUserInfoTool = {
|
|
|
434
575
|
};
|
|
435
576
|
const updateNodeTool = {
|
|
436
577
|
name: "agent_update_node",
|
|
437
|
-
description:
|
|
578
|
+
description: i18n.updateNode.description,
|
|
438
579
|
inputSchema: {
|
|
439
|
-
documentId: z.string().optional().describe(
|
|
440
|
-
documentPageId: z.string().optional().describe(
|
|
441
|
-
targetNodeId: z.string().optional().describe(
|
|
442
|
-
code: z.string().
|
|
443
|
-
filePath: z.string().optional().describe("【可选】如果需要发送整个文件或大量代码,请提供该文件的绝对路径,工具会自动读取并发送,绝对不要把整个文件的代码塞进 code 参数里!")
|
|
580
|
+
documentId: z.string().optional().describe(i18n.updateNode.documentId),
|
|
581
|
+
documentPageId: z.string().optional().describe(i18n.updateNode.documentPageId),
|
|
582
|
+
targetNodeId: z.string().optional().describe(i18n.updateNode.targetNodeId),
|
|
583
|
+
code: z.string().describe(i18n.updateNode.code)
|
|
444
584
|
},
|
|
445
585
|
handler: async (args) => {
|
|
446
|
-
const { targetNodeId, documentId, documentPageId,
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
try {
|
|
450
|
-
const absolutePath = path.resolve(filePath);
|
|
451
|
-
finalCode = await fs.readFile(absolutePath, "utf8");
|
|
452
|
-
} catch (e) {
|
|
453
|
-
return { content: [{ type: "text", text: `❌ 读取文件失败: ${e.message}` }], isError: true };
|
|
454
|
-
}
|
|
586
|
+
const { targetNodeId, documentId, documentPageId, code } = args;
|
|
587
|
+
if (!code) {
|
|
588
|
+
return { content: [{ type: "text", text: `❌ 必须提供 code` }], isError: true };
|
|
455
589
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
}
|
|
459
|
-
const { data, error } = await callApi("POST", "/api/updateNode", { code: finalCode, targetNodeId, documentId, documentPageId });
|
|
460
|
-
if (error) return { content: [{ type: "text", text: `❌ 更新失败: ${error.message}` }], isError: true };
|
|
590
|
+
const { data, error } = await callApi("POST", "/api/updateNode", { code, targetNodeId, documentId, documentPageId });
|
|
591
|
+
if (error) return { content: [{ type: "text", text: `❌ 局部更新失败: ${error.message}` }], isError: true };
|
|
461
592
|
return {
|
|
462
|
-
content: [{ type: "text", text: `✅
|
|
463
|
-
-
|
|
464
|
-
|
|
465
|
-
⚠️ 强烈建议:为了保证本地 .codify 目录中的基准 HTML 与设计稿保持一致,请立即调用 get_selection_code 工具(传入对应的根节点 ID),拉取最新的纯净 HTML 并覆盖本地基准文件。` }]
|
|
593
|
+
content: [{ type: "text", text: `✅ [局部修改] 指令已发送${data.targetNodeId ? `
|
|
594
|
+
- 节点 ID: ${data.targetNodeId}` : ""}` }]
|
|
466
595
|
};
|
|
467
596
|
}
|
|
468
597
|
};
|
|
469
598
|
const getDesignDiffTool = {
|
|
470
599
|
name: "get_design_diff",
|
|
471
|
-
description:
|
|
600
|
+
description: i18n.getDesignDiff.description,
|
|
472
601
|
inputSchema: {
|
|
473
|
-
projectDir: z.string().describe(
|
|
474
|
-
|
|
602
|
+
projectDir: z.string().describe(i18n.getDesignDiff.projectDir),
|
|
603
|
+
targetNodeId: z.string().optional().describe(i18n.getDesignDiff.targetNodeId),
|
|
604
|
+
filePath: z.string().optional().describe(i18n.getDesignDiff.filePath)
|
|
475
605
|
},
|
|
476
606
|
handler: async (args) => {
|
|
477
|
-
const { projectDir,
|
|
607
|
+
const { projectDir, targetNodeId, filePath } = args;
|
|
478
608
|
const baseDir = projectDir ? path.resolve(projectDir) : process.cwd();
|
|
609
|
+
let currentTargetNodeId = targetNodeId;
|
|
479
610
|
let oldHtml = "";
|
|
480
|
-
let localFilePath = "";
|
|
481
|
-
if (
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
611
|
+
let localFilePath = filePath || "";
|
|
612
|
+
if (localFilePath) {
|
|
613
|
+
try {
|
|
614
|
+
const absolutePath = path.isAbsolute(localFilePath) ? localFilePath : path.resolve(baseDir, localFilePath);
|
|
615
|
+
oldHtml = await fs.readFile(absolutePath, "utf8");
|
|
616
|
+
} catch (e) {
|
|
617
|
+
return {
|
|
618
|
+
content: [
|
|
619
|
+
{ type: "text", text: `❌ 无法读取指定的 filePath: ${e.message}` }
|
|
620
|
+
],
|
|
621
|
+
isError: true
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
let finalRootId = "";
|
|
626
|
+
if (!oldHtml || !currentTargetNodeId) {
|
|
627
|
+
const selectionEndpoint = currentTargetNodeId ? `/api/getSelectionCode?id=${encodeURIComponent(currentTargetNodeId)}` : "/api/getSelectionCode";
|
|
628
|
+
const { data: selectionData, error: selectionError } = await callApi(
|
|
629
|
+
"GET",
|
|
630
|
+
selectionEndpoint
|
|
631
|
+
);
|
|
632
|
+
if (selectionError || !selectionData?.nodeInfo?.targetNodeId) {
|
|
633
|
+
return {
|
|
634
|
+
content: [
|
|
635
|
+
{
|
|
636
|
+
type: "text",
|
|
637
|
+
text: `❌ 无法获取图层信息,请确保已在 MasterGo 中选中图层。`
|
|
638
|
+
}
|
|
639
|
+
],
|
|
640
|
+
isError: true
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
const {
|
|
644
|
+
rootId,
|
|
645
|
+
targetNodeId: finalTargetId,
|
|
646
|
+
nodeName: finalNodeName
|
|
647
|
+
} = selectionData.nodeInfo;
|
|
648
|
+
finalRootId = rootId;
|
|
649
|
+
if (!currentTargetNodeId) currentTargetNodeId = finalTargetId;
|
|
650
|
+
if (!oldHtml && rootId) {
|
|
651
|
+
const safeRootId = String(rootId).replace(/:/g, "-");
|
|
652
|
+
const findFile = async (dir) => {
|
|
653
|
+
if (!existsSync(dir)) return null;
|
|
654
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
655
|
+
for (const entry of entries) {
|
|
656
|
+
const fullPath = path.join(dir, entry.name);
|
|
657
|
+
if (entry.isDirectory()) {
|
|
658
|
+
const found = await findFile(fullPath);
|
|
659
|
+
if (found) return found;
|
|
660
|
+
} else if (entry.name.endsWith(`-${safeRootId}.html`)) {
|
|
661
|
+
return fullPath;
|
|
662
|
+
}
|
|
492
663
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
const codifyDir = path.join(baseDir, CODIFY_DOC_DIR);
|
|
497
|
-
if (existsSync(codifyDir)) {
|
|
664
|
+
return null;
|
|
665
|
+
};
|
|
666
|
+
const codifyDir = path.join(baseDir, CODIFY_DOC_DIR);
|
|
498
667
|
localFilePath = await findFile(codifyDir) || "";
|
|
499
668
|
if (localFilePath) {
|
|
500
669
|
oldHtml = await fs.readFile(localFilePath, "utf8");
|
|
@@ -503,21 +672,32 @@ const getDesignDiffTool = {
|
|
|
503
672
|
}
|
|
504
673
|
if (!oldHtml) {
|
|
505
674
|
return {
|
|
506
|
-
content: [
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
675
|
+
content: [
|
|
676
|
+
{
|
|
677
|
+
type: "text",
|
|
678
|
+
text: `❌ 在本地 .codify 目录中找不到 ID 为 ${finalRootId || currentTargetNodeId} 的基准代码文件。请确保您之前已使用 get_selection_code 拉取过该图层或其根节点的代码。`
|
|
679
|
+
}
|
|
680
|
+
],
|
|
510
681
|
isError: true
|
|
511
682
|
};
|
|
512
683
|
}
|
|
513
684
|
const { data, error } = await callApi("POST", "/api/getDesignDiff", {
|
|
514
685
|
code: oldHtml,
|
|
515
|
-
|
|
686
|
+
targetNodeId: currentTargetNodeId
|
|
516
687
|
});
|
|
517
688
|
if (error) {
|
|
518
|
-
return {
|
|
689
|
+
return {
|
|
690
|
+
content: [
|
|
691
|
+
{ type: "text", text: `❌ 获取设计稿差异失败: ${error.message}` }
|
|
692
|
+
],
|
|
693
|
+
isError: true
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
let diffs = data.diffs || [];
|
|
697
|
+
if (diffs && !Array.isArray(diffs) && Array.isArray(diffs.diffs)) {
|
|
698
|
+
diffs.nodeInfo;
|
|
699
|
+
diffs = diffs.diffs;
|
|
519
700
|
}
|
|
520
|
-
const diffs = data.diffs || [];
|
|
521
701
|
if (diffs.length === 0) {
|
|
522
702
|
return {
|
|
523
703
|
content: [
|
|
@@ -544,6 +724,65 @@ ${JSON.stringify(diffs, null, 2)}
|
|
|
544
724
|
};
|
|
545
725
|
}
|
|
546
726
|
};
|
|
727
|
+
const removeNodeTool = {
|
|
728
|
+
name: "agent_remove_node",
|
|
729
|
+
description: i18n.removeNode.description,
|
|
730
|
+
inputSchema: {
|
|
731
|
+
documentId: z.string().optional().describe(i18n.removeNode.documentId),
|
|
732
|
+
documentPageId: z.string().optional().describe(i18n.removeNode.documentPageId),
|
|
733
|
+
targetNodeId: z.string().optional().describe(i18n.removeNode.targetNodeId)
|
|
734
|
+
},
|
|
735
|
+
handler: async (args) => {
|
|
736
|
+
const { targetNodeId, documentId, documentPageId } = args;
|
|
737
|
+
const { data, error } = await callApi("POST", "/api/removeNode", { targetNodeId, documentId, documentPageId });
|
|
738
|
+
if (error) {
|
|
739
|
+
return {
|
|
740
|
+
content: [{ type: "text", text: `❌ 删除失败: ${error.message}` }],
|
|
741
|
+
isError: true
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
return {
|
|
745
|
+
content: [{
|
|
746
|
+
type: "text",
|
|
747
|
+
text: `✅ 已成功发送删除指令
|
|
748
|
+
- 目标节点 ID: ${data.targetNodeId || targetNodeId || "当前选中图层"}`
|
|
749
|
+
}]
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
const syncToDesignTool = {
|
|
754
|
+
name: "agent_sync_design",
|
|
755
|
+
description: i18n.syncToDesign.description,
|
|
756
|
+
inputSchema: {
|
|
757
|
+
documentId: z.string().optional().describe(i18n.syncToDesign.documentId),
|
|
758
|
+
documentPageId: z.string().optional().describe(i18n.syncToDesign.documentPageId),
|
|
759
|
+
targetNodeId: z.string().optional().describe(i18n.syncToDesign.targetNodeId),
|
|
760
|
+
filePath: z.string().describe(i18n.syncToDesign.filePath)
|
|
761
|
+
},
|
|
762
|
+
handler: async (args) => {
|
|
763
|
+
const { targetNodeId, documentId, documentPageId, filePath } = args;
|
|
764
|
+
let finalCode = "";
|
|
765
|
+
if (filePath) {
|
|
766
|
+
try {
|
|
767
|
+
const absolutePath = path.resolve(filePath);
|
|
768
|
+
finalCode = await fs.readFile(absolutePath, "utf8");
|
|
769
|
+
} catch (e) {
|
|
770
|
+
return { content: [{ type: "text", text: `❌ 读取本地文件失败: ${e.message}` }], isError: true };
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
if (!finalCode) {
|
|
774
|
+
return { content: [{ type: "text", text: `❌ 同步失败: 未能从指定路径读取到有效代码` }], isError: true };
|
|
775
|
+
}
|
|
776
|
+
const { data, error } = await callApi("POST", "/api/syncToDesign", { code: finalCode, targetNodeId, documentId, documentPageId });
|
|
777
|
+
if (error) return { content: [{ type: "text", text: `❌ [全量同步] 失败: ${error.message}` }], isError: true };
|
|
778
|
+
return {
|
|
779
|
+
content: [{ type: "text", text: `✅ [全量同步] 已成功推送至画布${data.targetNodeId ? `
|
|
780
|
+
- 根节点 ID: ${data.targetNodeId}` : ""}
|
|
781
|
+
|
|
782
|
+
💡 建议:同步完成后,请立即通过 get_selection_code (传入 rootId) 重新拉取一次纯净 HTML,以刷新本地 .codify 基准。` }]
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
};
|
|
547
786
|
const allTools = [
|
|
548
787
|
designTool,
|
|
549
788
|
getCodeTool,
|
|
@@ -553,7 +792,9 @@ const allTools = [
|
|
|
553
792
|
createComponentTool,
|
|
554
793
|
getSelectionCodeTool,
|
|
555
794
|
updateNodeTool,
|
|
556
|
-
|
|
795
|
+
syncToDesignTool,
|
|
796
|
+
getDesignDiffTool,
|
|
797
|
+
removeNodeTool
|
|
557
798
|
];
|
|
558
799
|
function registerAllTools(mcpServer2) {
|
|
559
800
|
allTools.forEach((tool) => {
|
package/dist/rules.md
CHANGED
|
@@ -10,6 +10,29 @@
|
|
|
10
10
|
**核心二:严苛的 Figma 协议编译器(负责“格式化”)**
|
|
11
11
|
一旦视觉方案定型,你必须将这个绝美的界面,**100% 严格地“降维、压缩、翻译”**成符合底层规范的代码。你不再是设计师,而是一个没有感情的机器,确保每一行代码都能被程序完美逆向解析为 Figma 图层。任何非标代码都会导致转换失败系统崩溃。
|
|
12
12
|
|
|
13
|
+
## 🔄 标准双向同步协议 (Standard Bi-directional Sync Protocol)
|
|
14
|
+
|
|
15
|
+
当需要将本地工程与 MasterGo 设计稿进行比对或同步时,必须严格遵守以下“三步走”规程,严禁跳步:
|
|
16
|
+
|
|
17
|
+
### Step 1: 准备与逆向转译 (Prepare & Reverse Transpile)
|
|
18
|
+
你必须读取当前项目对应的业务代码(如 `.vue`, `.tsx`),并将其物理“解构”为符合本协议规范的纯静态 HTML。
|
|
19
|
+
- **清除所有框架指令**:删除 `v-for`, `@click`, `v-if`, `props` 等所有动态逻辑。
|
|
20
|
+
- **展开静态数据**:将变量替换为高逼真的静态假数据,将循环展开为重复的 HTML 结构。
|
|
21
|
+
- **验证样式**:确保所有类名已转换为本协议要求的 Tailwind 任意值语法。
|
|
22
|
+
|
|
23
|
+
### Step 2: 刷新本地基准 (Refresh Local Base)
|
|
24
|
+
将 Step 1 产出的纯静态 HTML 代码,通过 `write_to_file` 覆盖写入到 `.codify` 目录下对应的基准 `.html` 文件中。
|
|
25
|
+
- **理由**:通过此步骤,我们确保 `.codify` 目录实时映射了当前代码的真实视觉状态,使得后续的 Diff 能够准确反映“代码”与“设计”的差异,而非过期的快照。
|
|
26
|
+
|
|
27
|
+
### Step 3: 比对与决策 (Diff & Decision)
|
|
28
|
+
调用 `get_design_diff` 工具,获取基准文件与画布现状的差异 JSON。
|
|
29
|
+
- **展示差异**:向用户展示变更列表(修改、插入、删除)。
|
|
30
|
+
- **用户决策**:
|
|
31
|
+
- **A. 同步到设计稿**:用户确认代码是正确的,调用 `agent_update_node` (带 `filePath`) 将文件强行覆盖到画布根节点。
|
|
32
|
+
- **B. 同步到代码**:用户确认设计是正确的,你根据 Diff JSON 精准修补业务代码中的样式和结构。
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
13
36
|
## 📜 最终产出总纲 (Final Output Standards)
|
|
14
37
|
|
|
15
38
|
你所有的工作产出,必须无条件满足以下 7 大黄金标准:
|