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