@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.
Files changed (3) hide show
  1. package/dist/index.js +369 -128
  2. package/dist/rules.md +23 -0
  3. 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.rootNodeId) responseText += `
29
- - 根节点 ID: ${result.rootNodeId}`;
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
- nodeId,
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 && !nodeId ? "code" : "") : isSelection ? path.join(baseDir, CODIFY_OUTPUT_DIR, "selection") : path.join(baseDir, `${CODIFY_OUTPUT_DIR}${contentId ? "/" + contentId : ""}`);
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(nodeId || Date.now()).replace(/:/g, "-");
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 || nodeId || nodeName ? `${safeName}-${safeId}.html` : `${contentId || "index"}.html`;
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: '创建一个 MasterGo 母版组件或组件集(变体)。应当使用 HTML 格式并包含 data-type="component" 属性。',
214
+ description: i18n.createComponent.description,
144
215
  inputSchema: {
145
- code: z.string().describe('组件的 HTML 结构。必须包含 data-type="component" 或 "component-set"。')
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: "将代码发送到 Codify 插件转换为设计稿。若 HTML 已保存为本地文件,请通过 filePath 传入,避免大段 code 占用 Token。",
227
+ description: i18n.createPage.description,
157
228
  inputSchema: {
158
- code: z.string().optional().describe("【可选】要发送的 HTML 代码内容。小片段可直接传此参数。"),
159
- filePath: z.string().optional().describe("【可选】本地 HTML 文件的绝对路径。创建设计稿且文件已存在时,优先传此参数,工具会读取后发送并执行落盘。"),
160
- projectDir: z.string().describe("【必填】用户当前工作区的根目录绝对路径"),
161
- saveCodeToLocal: z.boolean().default(true).describe("是否将插件返回的渲染结果保存到本地 .codify 目录(落盘机制)")
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
- nodeId: data.rootNodeId,
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: "根据需求生成符合 Codify 规范的 HTML+CSS 代码",
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: "从 Codify For MasterGo 插件获取指定的代码",
309
+ description: i18n.getCode.description,
239
310
  inputSchema: {
240
- contentId: z.string().describe("从Codify插件复制图层的指令"),
241
- documentId: z.string().optional().describe("当前 MasterGo 文档 ID。"),
242
- documentPageId: z.string().optional().describe("当前 MasterGo 页面 ID。"),
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: "获取 MasterGo 中当前选中图层(或指定图层)的代码,并智能落盘到本地 .codify 目录。\n\n【强制触发场景】凡用户说「将选中的 xxx 改成 yyy」「将选中的按钮改成红色」「修改当前选中的图层」等含「选中」的修改指令,你必须首先调用此工具拉取当前画布选中节点的最新代码,再根据返回的 HTML 进行修改,禁止凭空生成或直接修改本地文件。",
374
+ description: i18n.getSelectionCode.description,
304
375
  inputSchema: {
305
- projectDir: z.string().describe("【必填】用户当前工作区的根目录绝对路径"),
306
- id: z.string().optional().describe("【选填】MasterGo图层ID (例如 123:456)。如果提供,将直接拉取该ID的代码;如果不提供,将拉取当前选中图层的代码。")
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 { content: [{ type: "text", text: "❌ 获取失败: 没有选中任何图层。请先在 MasterGo 中选中一个图层。" }], isError: true };
399
+ return {
400
+ content: [
401
+ {
402
+ type: "text",
403
+ text: "❌ 获取失败: 没有选中任何图层。请先在 MasterGo 中选中一个图层。"
404
+ }
405
+ ],
406
+ isError: true
407
+ };
315
408
  }
316
- return { content: [{ type: "text", text: `❌ 获取失败: ${error.message}` }], isError: true };
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 { rootId, documentId, documentPageId, nodeId, nodeName, parentId } = nodeInfo;
321
- if (!parentId || parentId === "null") {
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
- nodeId: rootId || nodeId,
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} (${nodeId})
342
- - 文件: ${saveResult.htmlPath}
343
-
344
- 请查看该文件了解当前结构。`
443
+ - 节点: ${nodeName} (${targetNodeId})
444
+ - 根节点: ${rootId}
445
+ - 文件: ${saveResult.htmlPath}。`
345
446
  }
346
447
  ]
347
448
  };
348
449
  } else {
349
- if (!documentId || !documentPageId) {
350
- return { content: [{ type: "text", text: `❌ 更新失败: 缺少 documentId 或 documentPageId 信息。` }], isError: true };
351
- }
352
- const targetDir = path.join(baseDir, CODIFY_DOC_DIR, String(documentId).replace(/:/g, "-"), String(documentPageId).replace(/:/g, "-"));
353
- if (!existsSync(targetDir)) {
354
- return { content: [{ type: "text", text: `❌ 更新失败: 找不到根节点所在目录 ${targetDir}。请先选中并获取根节点的代码。` }], isError: true };
355
- }
356
- const files = await fs.readdir(targetDir);
357
- const safeRootId = String(rootId).replace(/:/g, "-");
358
- const rootFile = files.find((f) => f.endsWith(`-${safeRootId}.html`));
359
- if (!rootFile) {
360
- return { content: [{ type: "text", text: `❌ 更新失败: 在目录中找不到包含根节点 ID (${rootId}) 的文件。请先选中并获取根节点的代码。` }], isError: true };
361
- }
362
- const rootFilePath = path.join(targetDir, rootFile);
363
- const htmlContent = await fs.readFile(rootFilePath, "utf8");
364
- const rootNode = parse(htmlContent);
365
- const targetElement = rootNode.querySelector(`[data-node-id="${nodeId}"]`);
366
- if (targetElement) {
367
- targetElement.replaceWith(data.code);
368
- } else if (nodeInfo.parentId) {
369
- const parentElement = rootNode.querySelector(`[data-node-id="${nodeInfo.parentId}"]`);
370
- if (parentElement) {
371
- parentElement.insertAdjacentHTML("beforeend", data.code);
372
- } else {
373
- return { content: [{ type: "text", text: `❌ 更新失败: 在文件 ${rootFile} 中找不到当前节点(${nodeId}),也找不到父节点(${nodeInfo.parentId})。` }], isError: true };
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
- nodeId,
512
+ targetNodeId,
384
513
  nodeName,
385
514
  code: "",
386
- // 我们已经手动更新了 HTML,这里传空字符串,主要是为了利用它去保存资源
515
+ // 核心改动:强制将此参数留空,彻底阻断 .html 碎片文件的生成
387
516
  resourcePath: data.resourcePath,
388
517
  shape: data.shape,
389
518
  svg: data.svg,
390
519
  image: data.image
391
- }).catch((e) => console.error("保存子节点资源失败:", e));
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
- - 节点: ${nodeName} (${nodeId})
398
- - 已更新文件: ${rootFilePath}`
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: '将修改后的 HTML 代码发回 MasterGo 画布进行更新。\n\n【调用前置条件】若用户说的是「将选中的 xxx 改成 yyy」等含「选中」的修改指令,你必须先调用 get_selection_code 拉取当前选中节点代码,修改完成后再调用本工具,不得跳过前置步骤。\n\n【局部修改(改文字/颜色/某处)】只传你改动的那一个节点(含 data-node-id 及子节点)的 HTML 片段到 code 参数,严禁传整个文件。\n\n【全量同步(用户明确说"同步到设计稿")】才用 filePath 传本地文件绝对路径,禁止用 Read 工具读大文件再塞进 code。\n\n若是从 Vue/React 业务代码同步,请剔除框架指令(v-for、@click 等),只传纯 HTML。',
578
+ description: i18n.updateNode.description,
438
579
  inputSchema: {
439
- documentId: z.string().optional().describe("当前 MasterGo 文档 ID。"),
440
- documentPageId: z.string().optional().describe("当前 MasterGo 页面 ID。"),
441
- targetNodeId: z.string().optional().describe("【选填】目标图层 ID (例如 123:456)。如果不传,则默认更新 MasterGo 中当前选中的图层。"),
442
- code: z.string().optional().describe("【可选】修改后的 HTML 代码片段。如果只是局部小修改,可以直接传代码。"),
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, filePath } = args;
447
- let finalCode = args.code || "";
448
- if (filePath) {
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
- if (!finalCode) {
457
- return { content: [{ type: "text", text: `❌ 必须提供 code 或 filePath` }], isError: true };
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: `✅ 已成功发送更新指令${data.nodeId ? `
463
- - 结果节点 ID: ${data.nodeId}` : ""}
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: "获取最新设计稿代码与本地 .codify 目录中旧代码的差异 (Diff)。当用户要求“同步设计稿到业务代码”或“对比最新设计稿”时使用。注意:拿到差异后,请根据 data-node-id 精准修改用户的业务代码,绝对不要破坏原有的框架逻辑(如 Vue/React 指令)。",
600
+ description: i18n.getDesignDiff.description,
472
601
  inputSchema: {
473
- projectDir: z.string().describe("【必填】用户当前工作区的根目录绝对路径"),
474
- id: z.string().optional().describe("【选填】MasterGo图层ID (例如 123:456)。如果不传,则默认获取当前选中图层的代码进行对比。")
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, id } = args;
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 (id) {
482
- const safeId = String(id).replace(/:/g, "-");
483
- const findFile = async (dir) => {
484
- const entries = await fs.readdir(dir, { withFileTypes: true });
485
- for (const entry of entries) {
486
- const fullPath = path.join(dir, entry.name);
487
- if (entry.isDirectory()) {
488
- const found = await findFile(fullPath);
489
- if (found) return found;
490
- } else if (entry.name.endsWith(`-${safeId}.html`)) {
491
- return fullPath;
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
- return null;
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
- type: "text",
508
- text: id ? `❌ 在本地 .codify 目录中找不到 ID 为 ${id} 的代码文件。请确保已执行过拉取操作。` : `❌ 请提供图层 ID 以便从本地 .codify 目录加载基准代码进行 Diff。`
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
- id
686
+ targetNodeId: currentTargetNodeId
516
687
  });
517
688
  if (error) {
518
- return { content: [{ type: "text", text: `❌ 获取设计稿差异失败: ${error.message}` }], isError: true };
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
- getDesignDiffTool
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 大黄金标准:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codify-ai/mcp-client",
3
- "version": "1.0.25",
3
+ "version": "1.0.27",
4
4
  "description": "Codify MCP 客户端 - 连接到远程 Codify MCP 服务器,供 CLI 或 Cursor 等 IDE 使用",
5
5
  "type": "module",
6
6
  "bin": {