@codify-ai/mcp-client 1.0.24 → 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 +371 -129
- package/dist/rules.md +23 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -17,15 +17,16 @@ const designPhilosophy = readFileSync(
|
|
|
17
17
|
resolve(__dirname$1, "design-philosophy.md"),
|
|
18
18
|
"utf-8"
|
|
19
19
|
);
|
|
20
|
-
const
|
|
20
|
+
const urlArg = process.argv.find((arg) => arg.startsWith("--url="));
|
|
21
|
+
const SERVER_URL = urlArg ? urlArg.slice("--url=".length) : process.env.CODIFY_SERVER_URL || "https://mcp.codify-api.com";
|
|
21
22
|
const ACCESS_KEY = process.env.CODIFY_ACCESS_KEY;
|
|
22
23
|
const CODIFY_DOC_DIR = ".codify";
|
|
23
24
|
const CODIFY_OUTPUT_DIR = ".codify-output";
|
|
24
25
|
const isEmpty = (value) => value === null || value === void 0 || value === "" || typeof value === "string" && value.trim() === "";
|
|
25
26
|
function buildSuccessText(result, actionName = "操作") {
|
|
26
27
|
let responseText = `✅ ${actionName}已成功完成`;
|
|
27
|
-
if (result.
|
|
28
|
-
-
|
|
28
|
+
if (result.targetNodeId) responseText += `
|
|
29
|
+
- 节点 ID: ${result.targetNodeId}`;
|
|
29
30
|
if (result.documentId) responseText += `
|
|
30
31
|
- 文档: ${result.documentName || ""} (${result.documentId})`;
|
|
31
32
|
if (result.documentPageId) responseText += `
|
|
@@ -111,7 +112,7 @@ async function saveCodeAndResources({
|
|
|
111
112
|
documentId,
|
|
112
113
|
documentPageId,
|
|
113
114
|
contentId,
|
|
114
|
-
|
|
115
|
+
targetNodeId,
|
|
115
116
|
nodeName,
|
|
116
117
|
code,
|
|
117
118
|
resourcePath,
|
|
@@ -120,11 +121,11 @@ async function saveCodeAndResources({
|
|
|
120
121
|
image,
|
|
121
122
|
isSelection = false
|
|
122
123
|
}) {
|
|
123
|
-
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 : ""}`);
|
|
124
125
|
if (!existsSync(targetDir)) mkdirSync(targetDir, { recursive: true });
|
|
125
|
-
const safeId = String(
|
|
126
|
+
const safeId = String(targetNodeId || Date.now()).replace(/:/g, "-");
|
|
126
127
|
const safeName = String(nodeName || (isSelection ? "selection" : "node")).replace(/[^a-zA-Z0-9\u4e00-\u9fa5_-]/g, "-");
|
|
127
|
-
const htmlFileName = isSelection ||
|
|
128
|
+
const htmlFileName = isSelection || targetNodeId || nodeName ? `${safeName}-${safeId}.html` : `${contentId || "index"}.html`;
|
|
128
129
|
const htmlPath = path.join(targetDir, htmlFileName);
|
|
129
130
|
if (code) {
|
|
130
131
|
await fs.writeFile(htmlPath, code, "utf8");
|
|
@@ -137,11 +138,82 @@ async function saveCodeAndResources({
|
|
|
137
138
|
]);
|
|
138
139
|
return { targetDir, htmlFileName, htmlPath, shapeCount: stats[0], svgCount: stats[1], imageCount: stats[2], resourcePathMap: resPathMap };
|
|
139
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;
|
|
140
212
|
const createComponentTool = {
|
|
141
213
|
name: "agent_create_component",
|
|
142
|
-
description:
|
|
214
|
+
description: i18n.createComponent.description,
|
|
143
215
|
inputSchema: {
|
|
144
|
-
code: z.string().describe(
|
|
216
|
+
code: z.string().describe(i18n.createComponent.code)
|
|
145
217
|
},
|
|
146
218
|
handler: async (args) => {
|
|
147
219
|
const { code } = args;
|
|
@@ -152,12 +224,12 @@ const createComponentTool = {
|
|
|
152
224
|
};
|
|
153
225
|
const createPageTool = {
|
|
154
226
|
name: "agent_create_page",
|
|
155
|
-
description:
|
|
227
|
+
description: i18n.createPage.description,
|
|
156
228
|
inputSchema: {
|
|
157
|
-
code: z.string().optional().describe(
|
|
158
|
-
filePath: z.string().optional().describe(
|
|
159
|
-
projectDir: z.string().describe(
|
|
160
|
-
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)
|
|
161
233
|
},
|
|
162
234
|
handler: async (args) => {
|
|
163
235
|
const { projectDir, saveCodeToLocal = true } = args;
|
|
@@ -183,7 +255,7 @@ const createPageTool = {
|
|
|
183
255
|
code: data.htmlCode || code,
|
|
184
256
|
documentId: data.documentId,
|
|
185
257
|
documentPageId: data.documentPageId,
|
|
186
|
-
|
|
258
|
+
targetNodeId: data.targetNodeId,
|
|
187
259
|
nodeName: data.nodeName,
|
|
188
260
|
resourcePath: data.resourcePath,
|
|
189
261
|
shape: data.shape,
|
|
@@ -202,9 +274,9 @@ const createPageTool = {
|
|
|
202
274
|
};
|
|
203
275
|
const designTool = {
|
|
204
276
|
name: "design",
|
|
205
|
-
description:
|
|
277
|
+
description: i18n.design.description,
|
|
206
278
|
inputSchema: {
|
|
207
|
-
requirement: z.string().describe(
|
|
279
|
+
requirement: z.string().describe(i18n.design.requirement)
|
|
208
280
|
},
|
|
209
281
|
handler: async (args) => {
|
|
210
282
|
const { requirement } = args;
|
|
@@ -234,13 +306,13 @@ ${generationRules}
|
|
|
234
306
|
};
|
|
235
307
|
const getCodeTool = {
|
|
236
308
|
name: "get_code",
|
|
237
|
-
description:
|
|
309
|
+
description: i18n.getCode.description,
|
|
238
310
|
inputSchema: {
|
|
239
|
-
contentId: z.string().describe(
|
|
240
|
-
documentId: z.string().optional().describe(
|
|
241
|
-
documentPageId: z.string().optional().describe(
|
|
242
|
-
projectDir: z.string().describe(
|
|
243
|
-
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)
|
|
244
316
|
},
|
|
245
317
|
handler: async (args) => {
|
|
246
318
|
const { contentId, outDir, projectDir, documentId, documentPageId } = args;
|
|
@@ -276,8 +348,8 @@ const getCodeTool = {
|
|
|
276
348
|
};
|
|
277
349
|
const getCodeListTool = {
|
|
278
350
|
name: "get_code_list",
|
|
279
|
-
description:
|
|
280
|
-
inputSchema: z.object({}).describe(
|
|
351
|
+
description: i18n.getCodeList.description,
|
|
352
|
+
inputSchema: z.object({}).describe(i18n.getCodeList.inputSchema),
|
|
281
353
|
handler: async () => {
|
|
282
354
|
const { data, error } = await callApi("GET", "/api/getCodeList");
|
|
283
355
|
if (error) return { content: [{ type: "text", text: `❌ 获取失败: ${error.message}` }], isError: true };
|
|
@@ -299,31 +371,63 @@ const getCodeListTool = {
|
|
|
299
371
|
};
|
|
300
372
|
const getSelectionCodeTool = {
|
|
301
373
|
name: "get_selection_code",
|
|
302
|
-
description:
|
|
374
|
+
description: i18n.getSelectionCode.description,
|
|
303
375
|
inputSchema: {
|
|
304
|
-
projectDir: z.string().describe(
|
|
305
|
-
|
|
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)
|
|
306
379
|
},
|
|
380
|
+
/**
|
|
381
|
+
* 工具处理器
|
|
382
|
+
* @param args.syncToBase 是否尝试将子节点代码合并到基准 HTML 文件
|
|
383
|
+
* @param args._depth 内部参数,用于防止递归获取根节点时出现无限循环
|
|
384
|
+
*/
|
|
307
385
|
handler: async (args) => {
|
|
308
|
-
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
|
+
}
|
|
309
395
|
const endpoint = id ? `/api/getSelectionCode?id=${encodeURIComponent(id)}` : "/api/getSelectionCode";
|
|
310
396
|
const { data, error } = await callApi("GET", endpoint);
|
|
311
397
|
if (error) {
|
|
312
398
|
if (error.status === 400 && error.data?.error === "NoSelection") {
|
|
313
|
-
return {
|
|
399
|
+
return {
|
|
400
|
+
content: [
|
|
401
|
+
{
|
|
402
|
+
type: "text",
|
|
403
|
+
text: "❌ 获取失败: 没有选中任何图层。请先在 MasterGo 中选中一个图层。"
|
|
404
|
+
}
|
|
405
|
+
],
|
|
406
|
+
isError: true
|
|
407
|
+
};
|
|
314
408
|
}
|
|
315
|
-
return {
|
|
409
|
+
return {
|
|
410
|
+
content: [{ type: "text", text: `❌ 获取失败: ${error.message}` }],
|
|
411
|
+
isError: true
|
|
412
|
+
};
|
|
316
413
|
}
|
|
317
414
|
const baseDir = projectDir ? path.resolve(projectDir) : process.cwd();
|
|
318
415
|
const nodeInfo = data.nodeInfo || {};
|
|
319
|
-
const {
|
|
320
|
-
|
|
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}`);
|
|
321
426
|
const saveResult = await saveCodeAndResources({
|
|
322
427
|
baseDir,
|
|
323
428
|
documentId,
|
|
324
429
|
documentPageId,
|
|
325
|
-
|
|
326
|
-
// 使用 rootId 作为文件名后缀
|
|
430
|
+
targetNodeId: rootId,
|
|
327
431
|
nodeName,
|
|
328
432
|
code: data.code,
|
|
329
433
|
resourcePath: data.resourcePath,
|
|
@@ -331,70 +435,108 @@ const getSelectionCodeTool = {
|
|
|
331
435
|
svg: data.svg,
|
|
332
436
|
image: data.image
|
|
333
437
|
});
|
|
334
|
-
console.log("saveResult", saveResult);
|
|
335
438
|
return {
|
|
336
439
|
content: [
|
|
337
440
|
{
|
|
338
441
|
type: "text",
|
|
339
442
|
text: `✅ 成功获取并保存根节点代码
|
|
340
|
-
- 节点: ${nodeName} (${
|
|
341
|
-
-
|
|
342
|
-
|
|
343
|
-
请查看该文件了解当前结构。`
|
|
443
|
+
- 节点: ${nodeName} (${targetNodeId})
|
|
444
|
+
- 根节点: ${rootId}
|
|
445
|
+
- 文件: ${saveResult.htmlPath}。`
|
|
344
446
|
}
|
|
345
447
|
]
|
|
346
448
|
};
|
|
347
449
|
} else {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
+
}
|
|
373
506
|
}
|
|
374
|
-
} else {
|
|
375
|
-
return { content: [{ type: "text", text: `❌ 更新失败: 在文件 ${rootFile} 中找不到 data-node-id="${nodeId}" 的元素,且未提供 parentId 作为后备。` }], isError: true };
|
|
376
507
|
}
|
|
377
|
-
await fs.writeFile(rootFilePath, rootNode.toString(), "utf8");
|
|
378
508
|
await saveCodeAndResources({
|
|
379
509
|
baseDir,
|
|
380
510
|
documentId,
|
|
381
511
|
documentPageId,
|
|
382
|
-
|
|
512
|
+
targetNodeId,
|
|
383
513
|
nodeName,
|
|
384
514
|
code: "",
|
|
385
|
-
//
|
|
515
|
+
// 核心改动:强制将此参数留空,彻底阻断 .html 碎片文件的生成
|
|
386
516
|
resourcePath: data.resourcePath,
|
|
387
517
|
shape: data.shape,
|
|
388
518
|
svg: data.svg,
|
|
389
519
|
image: data.image
|
|
390
|
-
})
|
|
520
|
+
});
|
|
521
|
+
let successText = `✅ 成功获取子节点代码
|
|
522
|
+
- 节点: ${nodeName} (${targetNodeId})
|
|
523
|
+
- 根节点: ${rootId}`;
|
|
524
|
+
if (mergedToFilePath) {
|
|
525
|
+
successText += `
|
|
526
|
+
- ⚠️ (自动机制) 子节点最新代码已合并备份至本地基准 HTML: ${mergedToFilePath}`;
|
|
527
|
+
} else {
|
|
528
|
+
successText += `
|
|
529
|
+
- 💡 提示: 子节点代码已提取到对话上下文中,未生成本地 HTML 碎片文件。可以直接针对返回代码进行修改。`;
|
|
530
|
+
}
|
|
391
531
|
return {
|
|
392
532
|
content: [
|
|
393
533
|
{
|
|
394
534
|
type: "text",
|
|
395
|
-
text:
|
|
396
|
-
|
|
397
|
-
|
|
535
|
+
text: `${successText}
|
|
536
|
+
|
|
537
|
+
代码内容:
|
|
538
|
+
|
|
539
|
+
${data.code}`
|
|
398
540
|
}
|
|
399
541
|
]
|
|
400
542
|
};
|
|
@@ -403,8 +545,8 @@ const getSelectionCodeTool = {
|
|
|
403
545
|
};
|
|
404
546
|
const getUserInfoTool = {
|
|
405
547
|
name: "get_user_info",
|
|
406
|
-
description:
|
|
407
|
-
inputSchema: z.object({}).describe(
|
|
548
|
+
description: i18n.getUserInfo.description,
|
|
549
|
+
inputSchema: z.object({}).describe(i18n.getUserInfo.inputSchema),
|
|
408
550
|
handler: async () => {
|
|
409
551
|
const { data, error } = await callApi("GET", "/api/getUserInfo");
|
|
410
552
|
if (error) return { content: [{ type: "text", text: `❌ 获取失败: ${error.message}` }], isError: true };
|
|
@@ -433,67 +575,95 @@ const getUserInfoTool = {
|
|
|
433
575
|
};
|
|
434
576
|
const updateNodeTool = {
|
|
435
577
|
name: "agent_update_node",
|
|
436
|
-
description:
|
|
578
|
+
description: i18n.updateNode.description,
|
|
437
579
|
inputSchema: {
|
|
438
|
-
documentId: z.string().optional().describe(
|
|
439
|
-
documentPageId: z.string().optional().describe(
|
|
440
|
-
targetNodeId: z.string().optional().describe(
|
|
441
|
-
code: z.string().
|
|
442
|
-
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)
|
|
443
584
|
},
|
|
444
585
|
handler: async (args) => {
|
|
445
|
-
const { targetNodeId, documentId, documentPageId,
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
try {
|
|
449
|
-
const absolutePath = path.resolve(filePath);
|
|
450
|
-
finalCode = await fs.readFile(absolutePath, "utf8");
|
|
451
|
-
} catch (e) {
|
|
452
|
-
return { content: [{ type: "text", text: `❌ 读取文件失败: ${e.message}` }], isError: true };
|
|
453
|
-
}
|
|
586
|
+
const { targetNodeId, documentId, documentPageId, code } = args;
|
|
587
|
+
if (!code) {
|
|
588
|
+
return { content: [{ type: "text", text: `❌ 必须提供 code` }], isError: true };
|
|
454
589
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
}
|
|
458
|
-
const { data, error } = await callApi("POST", "/api/updateNode", { code: finalCode, targetNodeId, documentId, documentPageId });
|
|
459
|
-
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 };
|
|
460
592
|
return {
|
|
461
|
-
content: [{ type: "text", text: `✅
|
|
462
|
-
-
|
|
463
|
-
|
|
464
|
-
⚠️ 强烈建议:为了保证本地 .codify 目录中的基准 HTML 与设计稿保持一致,请立即调用 get_selection_code 工具(传入对应的根节点 ID),拉取最新的纯净 HTML 并覆盖本地基准文件。` }]
|
|
593
|
+
content: [{ type: "text", text: `✅ [局部修改] 指令已发送${data.targetNodeId ? `
|
|
594
|
+
- 节点 ID: ${data.targetNodeId}` : ""}` }]
|
|
465
595
|
};
|
|
466
596
|
}
|
|
467
597
|
};
|
|
468
598
|
const getDesignDiffTool = {
|
|
469
599
|
name: "get_design_diff",
|
|
470
|
-
description:
|
|
600
|
+
description: i18n.getDesignDiff.description,
|
|
471
601
|
inputSchema: {
|
|
472
|
-
projectDir: z.string().describe(
|
|
473
|
-
|
|
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)
|
|
474
605
|
},
|
|
475
606
|
handler: async (args) => {
|
|
476
|
-
const { projectDir,
|
|
607
|
+
const { projectDir, targetNodeId, filePath } = args;
|
|
477
608
|
const baseDir = projectDir ? path.resolve(projectDir) : process.cwd();
|
|
609
|
+
let currentTargetNodeId = targetNodeId;
|
|
478
610
|
let oldHtml = "";
|
|
479
|
-
let localFilePath = "";
|
|
480
|
-
if (
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
+
}
|
|
491
663
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
const codifyDir = path.join(baseDir, CODIFY_DOC_DIR);
|
|
496
|
-
if (existsSync(codifyDir)) {
|
|
664
|
+
return null;
|
|
665
|
+
};
|
|
666
|
+
const codifyDir = path.join(baseDir, CODIFY_DOC_DIR);
|
|
497
667
|
localFilePath = await findFile(codifyDir) || "";
|
|
498
668
|
if (localFilePath) {
|
|
499
669
|
oldHtml = await fs.readFile(localFilePath, "utf8");
|
|
@@ -502,21 +672,32 @@ const getDesignDiffTool = {
|
|
|
502
672
|
}
|
|
503
673
|
if (!oldHtml) {
|
|
504
674
|
return {
|
|
505
|
-
content: [
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
675
|
+
content: [
|
|
676
|
+
{
|
|
677
|
+
type: "text",
|
|
678
|
+
text: `❌ 在本地 .codify 目录中找不到 ID 为 ${finalRootId || currentTargetNodeId} 的基准代码文件。请确保您之前已使用 get_selection_code 拉取过该图层或其根节点的代码。`
|
|
679
|
+
}
|
|
680
|
+
],
|
|
509
681
|
isError: true
|
|
510
682
|
};
|
|
511
683
|
}
|
|
512
684
|
const { data, error } = await callApi("POST", "/api/getDesignDiff", {
|
|
513
685
|
code: oldHtml,
|
|
514
|
-
|
|
686
|
+
targetNodeId: currentTargetNodeId
|
|
515
687
|
});
|
|
516
688
|
if (error) {
|
|
517
|
-
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;
|
|
518
700
|
}
|
|
519
|
-
const diffs = data.diffs || [];
|
|
520
701
|
if (diffs.length === 0) {
|
|
521
702
|
return {
|
|
522
703
|
content: [
|
|
@@ -543,6 +724,65 @@ ${JSON.stringify(diffs, null, 2)}
|
|
|
543
724
|
};
|
|
544
725
|
}
|
|
545
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
|
+
};
|
|
546
786
|
const allTools = [
|
|
547
787
|
designTool,
|
|
548
788
|
getCodeTool,
|
|
@@ -552,7 +792,9 @@ const allTools = [
|
|
|
552
792
|
createComponentTool,
|
|
553
793
|
getSelectionCodeTool,
|
|
554
794
|
updateNodeTool,
|
|
555
|
-
|
|
795
|
+
syncToDesignTool,
|
|
796
|
+
getDesignDiffTool,
|
|
797
|
+
removeNodeTool
|
|
556
798
|
];
|
|
557
799
|
function registerAllTools(mcpServer2) {
|
|
558
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 大黄金标准:
|