11import { z } from "zod" ;
22import { BaseTool } from "./base-tool" ;
33import { httpUtilInstance } from "../utils/api" ;
4+ import axios from "axios" ;
5+ import path from "path" ;
6+ import { existsSync , mkdirSync } from "fs" ;
7+ import { writeFile } from "fs/promises" ;
48
59const D2C_TOOL_NAME = "mcp__getD2c" ;
610const D2C_TOOL_DESCRIPTION = `
7- 使用此工具从 MasterGo 获取 D2C 数据。
8- 返回结果是一个 JSON 字符串,示例结构为:
9- {
10- "code": "00000",
11- "message": "✅ 请求成功",
12- "status": 200,
13- "data": {
14- "payload": {
15- "code": "code string",
16- "contentId": "contentId string",
17- "frameType": "frameType string",
18- "image": {
19- "396102588418ac9fff2dead11ae585ce.png": "https://image-resource.mastergo.com/164019242047676/169610758561058/396102588418ac9fff2dead11ae585ce.png",
20- "397816b5a878c61c85b8dba8c05d7347.png": "https://image-resource.mastergo.com/164019242047676/169610758561058/397816b5a878c61c85b8dba8c05d7347.png"
21- },
22- "svg": {
23- "396102588418ac9fff2dead11ae585ce.svg": "<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100'><circle cx='50' cy='50' r='40' fill='red'/></svg>",
24- },
25- "resourcePath": {
26- "image": "./asset/images/",
27- "svg": "./asset/icons/",
28- "shape": ""
29- },
11+ 使用此工具从 MasterGo 获取 D2C 数据,并在本地落盘:
12+ 1)将返回的 code 写入 html;
13+ 2)将返回的 svg / image 资源按 resourcePath 落盘到对应目录;
14+ 3)返回落盘摘要,避免把大体积资源塞进上下文。
15+ ` ;
16+
17+ type ResourcePathMap = Record < "image" | "svg" , string > ;
18+
19+ type SaveResult = {
20+ targetDir : string ;
21+ htmlFileName : string ;
22+ htmlPath : string ;
23+ svgCount : number ;
24+ imageCount : number ;
25+ resourcePathMap : ResourcePathMap ;
26+ } ;
27+
28+ type WriteResourceResult = {
29+ savedCount : number ;
30+ attemptedCount : number ;
31+ errorCount : number ;
32+ } ;
33+
34+ function isEmpty ( value : any ) : boolean {
35+ if ( value === null || value === undefined ) return true ;
36+ if ( typeof value === "string" ) return value . trim ( ) . length === 0 ;
37+ if ( Array . isArray ( value ) ) return value . length === 0 ;
38+ if ( typeof value === "object" ) return Object . keys ( value ) . length === 0 ;
39+ return false ;
40+ }
41+
42+ function hasContent ( value : any ) : boolean {
43+ if ( value === null || value === undefined ) return false ;
44+ if ( typeof value === "string" ) return value . trim ( ) . length > 0 ;
45+ if ( Array . isArray ( value ) ) return value . length > 0 ;
46+ if ( typeof value === "object" ) return Object . keys ( value ) . length > 0 ;
47+ return false ;
48+ }
49+
50+ function pickFirstWithContent ( values : any [ ] ) : any {
51+ for ( const v of values ) {
52+ if ( hasContent ( v ) ) return v ;
53+ }
54+ return undefined ;
55+ }
56+
57+ function parseResourcePath ( resourcePath : any ) : ResourcePathMap {
58+ const map : ResourcePathMap = {
59+ image : "asset/images" ,
60+ svg : "asset/icons" ,
61+ } ;
62+ if ( ! isEmpty ( resourcePath ) ) {
63+ try {
64+ const parsed = typeof resourcePath === "string" ? JSON . parse ( resourcePath ) : resourcePath ;
65+ if ( parsed . image ) {
66+ map . image = String ( parsed . image )
67+ . replace ( / ^ ( \. \/ | \/ ) / , "" )
68+ . replace ( / \/ + $ / , "" ) ;
69+ }
70+ if ( parsed . svg ) {
71+ map . svg = String ( parsed . svg ) . replace ( / ^ ( \. \/ | \/ ) / , "" ) . replace ( / \/ + $ / , "" ) ;
72+ }
73+ } catch {
74+ return map ;
75+ }
76+ }
77+ return map ;
78+ }
79+
80+ async function writeResource (
81+ resData : any ,
82+ targetDir : string ,
83+ folderName : string ,
84+ ext : string
85+ ) : Promise < WriteResourceResult > {
86+ if ( isEmpty ( resData ) ) return { savedCount : 0 , attemptedCount : 0 , errorCount : 0 } ;
87+
88+ let parsed : any ;
89+ try {
90+ parsed = typeof resData === "string" ? JSON . parse ( resData ) : resData ;
91+ } catch {
92+ return { savedCount : 0 , attemptedCount : 0 , errorCount : 1 } ;
93+ }
94+
95+ if ( ! parsed || typeof parsed !== "object" ) {
96+ return { savedCount : 0 , attemptedCount : 0 , errorCount : 1 } ;
97+ }
98+
99+ const keys = Object . keys ( parsed ) ;
100+ if ( keys . length === 0 ) {
101+ return { savedCount : 0 , attemptedCount : 0 , errorCount : 0 } ;
102+ }
103+
104+ const resDir = path . join ( targetDir , folderName ) ;
105+ if ( ! existsSync ( resDir ) ) mkdirSync ( resDir , { recursive : true } ) ;
106+
107+ let successCount = 0 ;
108+ let errorCount = 0 ;
109+
110+ await Promise . all (
111+ Object . entries ( parsed ) . map ( async ( [ key , value ] ) => {
112+ const match = String ( key ) . match ( / ( .+ ) \. ( [ a - z A - Z 0 - 9 ] + ) $ / ) ;
113+ const safeKey = ( match ? match [ 1 ] : key ) . replace ( / [ ^ a - z A - Z 0 - 9 _ - ] / g, "_" ) ;
114+ const finalExt = match ? match [ 2 ] : ext ;
115+ const filePath = path . join ( resDir , `${ safeKey } .${ finalExt } ` ) ;
116+
117+ const content = value as any ;
118+
119+ try {
120+ if ( typeof content === "string" && content . startsWith ( "http" ) ) {
121+ try {
122+ const response = await axios . get ( content , {
123+ responseType : "arraybuffer" ,
124+ timeout : 30000 ,
125+ } ) ;
126+ await writeFile ( filePath , response . data ) ;
127+ successCount += 1 ;
128+ return ;
129+ } catch ( err : any ) {
130+ const isWrongSsl =
131+ err ?. code === "EPROTO" ||
132+ String ( err ?. message ?? "" ) . includes ( "wrong version number" ) ;
133+
134+ // 有些资源链接会误把 http 服务包装成 https,导致 EPROTO:
135+ // 尝试回退到 http 再请求一次。
136+ if ( isWrongSsl && content . startsWith ( "https://" ) ) {
137+ const httpUrl = content . replace ( / ^ h t t p s : \/ \/ / , "http://" ) ;
138+ const response = await axios . get ( httpUrl , {
139+ responseType : "arraybuffer" ,
140+ timeout : 30000 ,
141+ } ) ;
142+ await writeFile ( filePath , response . data ) ;
143+ successCount += 1 ;
144+ return ;
145+ }
146+
147+ errorCount += 1 ;
148+ return ;
149+ }
150+ }
151+
152+ if ( typeof content === "string" && content . startsWith ( "data:image/" ) ) {
153+ const parts = content . split ( ";base64," ) ;
154+ if ( parts . length === 2 ) {
155+ await writeFile ( filePath , parts [ 1 ] , "base64" ) ;
156+ successCount += 1 ;
157+ }
158+ return ;
30159 }
160+
161+ const dataToWrite =
162+ typeof content === "object" ? JSON . stringify ( content , null , 2 ) : String ( content ?? "" ) ;
163+ const encoding : BufferEncoding =
164+ finalExt === "png" || finalExt === "jpg" || finalExt === "jpeg" ? "base64" : "utf8" ;
165+ await writeFile ( filePath , dataToWrite , encoding ) ;
166+ successCount += 1 ;
167+ } catch {
168+ errorCount += 1 ;
169+ return ;
31170 }
171+ } )
172+ ) ;
173+
174+ return { savedCount : successCount , attemptedCount : keys . length , errorCount } ;
175+ }
176+
177+ async function saveCodeAndResources ( params : {
178+ outDir ?: string ;
179+ contentId : string ;
180+ code : string ;
181+ resourcePath ?: any ;
182+ svg ?: any ;
183+ image ?: any ;
184+ } ) : Promise < SaveResult > {
185+ const { outDir, contentId, code, resourcePath, svg, image } = params ;
186+
187+ const targetDir = outDir
188+ ? path . isAbsolute ( outDir )
189+ ? path . join ( outDir )
190+ : path . join ( process . cwd ( ) , outDir )
191+ : process . cwd ( ) ;
192+
193+ if ( ! existsSync ( targetDir ) ) mkdirSync ( targetDir , { recursive : true } ) ;
194+
195+ const htmlFileName = `${ contentId || "index" } .html` ;
196+ const htmlPath = path . join ( targetDir , htmlFileName ) ;
197+
198+ if ( ! isEmpty ( code ) ) {
199+ await writeFile ( htmlPath , code , "utf8" ) ;
32200 }
201+
202+ const resPathMap = parseResourcePath ( resourcePath ) ;
203+
204+ // 即使资源为空,也确保目录按 resourcePath 规划创建出来,便于后续排查
205+ const ensureResDir = ( folderName : string ) => {
206+ const dirPath = path . join ( targetDir , folderName ) ;
207+ if ( ! existsSync ( dirPath ) ) mkdirSync ( dirPath , { recursive : true } ) ;
208+ } ;
209+ ensureResDir ( resPathMap . image ) ;
210+ ensureResDir ( resPathMap . svg ) ;
211+
212+
213+ const [ svgWrite , imageWrite ] = await Promise . all ( [
214+ writeResource ( svg , targetDir , resPathMap . svg , "svg" ) ,
215+ writeResource ( image , targetDir , resPathMap . image , "png" ) ,
216+ ] ) ;
217+
218+ return {
219+ targetDir,
220+ htmlFileName,
221+ htmlPath,
222+ svgCount : svgWrite . savedCount ,
223+ imageCount : imageWrite . savedCount ,
224+ resourcePathMap : resPathMap ,
225+ } ;
226+ }
227+
228+ function extractPayload ( d2c : any ) : {
229+ contentId : string ;
230+ frameType ?: string ;
231+ code : string ;
232+ resourcePath ?: any ;
233+ shape ?: any ;
234+ svg ?: any ;
235+ image ?: any ;
236+ } {
237+ const data = d2c ?. data ;
238+ const firstItem = Array . isArray ( data ) ? data [ 0 ] : undefined ;
239+
240+ const payload =
241+ firstItem ?. payload ??
242+ d2c ?. payload ??
243+ d2c ?. data ?. payload ??
244+ firstItem ?. payload ?. payload ??
245+ data ?. payload ??
246+ { } ;
247+
248+ const codeCandidate =
249+ payload ?. code ?? payload ?. html ?? payload ?. content ?? d2c ?. code ?? "" ;
250+
251+ const resourcePath = pickFirstWithContent ( [
252+ payload ?. resourcePath ,
253+ d2c ?. resourcePath ,
254+ firstItem ?. resourcePath ,
255+ ] ) ;
256+
257+ const image = pickFirstWithContent ( [ payload ?. image , firstItem ?. image ] ) ;
258+ const svg = pickFirstWithContent ( [ payload ?. svg , firstItem ?. svg ] ) ;
259+ const shape = pickFirstWithContent ( [ payload ?. shape , firstItem ?. shape ] ) ;
260+
261+ return {
262+ contentId : String ( firstItem ?. contentId ?? payload ?. contentId ?? d2c ?. contentId ?? "" ) ,
263+ frameType : payload ?. frameType ?? firstItem ?. frameType ?? d2c ?. frameType ,
264+ code : String ( codeCandidate ?? "" ) ,
265+ resourcePath,
266+ shape,
267+ svg,
268+ image,
269+ } ;
33270}
34- 使用此工具后,请按以下 todo 执行:
35- 1)为每个资源(返回的svg和image)在本地按 resourcePath 落盘:
36- - 确保 resourcePath 对应目录存在;
37- - 将返回的真实svg按照resourcePath的svg目录落盘;
38- - 将返回的真实image按照resourcePath的image目录落盘;
39- 2)使用 code 字段进行代码生成;
40- ` ;
41271
42272export class GetD2cTool extends BaseTool {
43273 name = D2C_TOOL_NAME ;
@@ -58,24 +288,34 @@ export class GetD2cTool extends BaseTool {
58288 . describe (
59289 "MasterGo 文档 ID,通常为 contentId 的第一段,例如 contentId 为 176452330285910-2-9032 时 documentId 为 176452330285910。"
60290 ) ,
291+ outDir : z
292+ . string ( )
293+ . optional ( )
294+ . describe ( "可选,输出目录(绝对路径或相对当前工作目录)。" ) ,
61295 } ) ;
62296
63297 async execute ( {
64298 contentId,
65299 documentId,
300+ outDir,
66301 } : z . infer < typeof this . schema > ) {
67302 try {
68- if ( ! contentId ) {
69- throw new Error ( "contentId 不能为空" ) ;
70- }
71- if ( ! documentId ) {
72- throw new Error ( "documentId 不能为空" ) ;
73- }
303+ if ( ! contentId ) throw new Error ( "contentId 不能为空" ) ;
304+ if ( ! documentId ) throw new Error ( "documentId 不能为空" ) ;
74305
75- const d2c = await httpUtilInstance . getD2c (
76- contentId ,
77- documentId
78- ) ;
306+ const d2c = await httpUtilInstance . getD2c ( contentId , documentId ) ;
307+
308+ const payloadExtracted = extractPayload ( d2c ) ;
309+ const finalContentId = payloadExtracted . contentId || contentId ;
310+
311+ await saveCodeAndResources ( {
312+ outDir,
313+ contentId : finalContentId ,
314+ code : payloadExtracted . code ,
315+ resourcePath : payloadExtracted . resourcePath ,
316+ svg : payloadExtracted . svg ,
317+ image : payloadExtracted . image ,
318+ } ) ;
79319
80320 return {
81321 content : [
@@ -95,6 +335,7 @@ export class GetD2cTool extends BaseTool {
95335 text : JSON . stringify ( errorMessage ) ,
96336 } ,
97337 ] ,
98- } ; }
338+ } ;
339+ }
99340 }
100341}
0 commit comments