@dyno181cm.nexsoft/zentao_mcp 1.2.8 → 1.2.10
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/build/tools.js +204 -177
- package/build/zentaoClient.js +28 -0
- package/package.json +1 -1
package/build/tools.js
CHANGED
|
@@ -4,19 +4,18 @@ import * as path from "path";
|
|
|
4
4
|
import fs from "fs";
|
|
5
5
|
import { ZentaoClient } from "./zentaoClient.js";
|
|
6
6
|
import TurndownService from "turndown";
|
|
7
|
+
// ─── Module-level singletons ────────────────────────────────────────────────
|
|
7
8
|
const client = new ZentaoClient();
|
|
8
9
|
const activeDownloads = new Map();
|
|
9
10
|
const turndownService = new TurndownService();
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
* @param server The MCP Server instance where the tools will be registered.
|
|
19
|
-
*/
|
|
11
|
+
// ─── Internal helpers (Single Responsibility) ────────────────────────────────
|
|
12
|
+
/** Convert HTML to Markdown for better AI readability. */
|
|
13
|
+
function htmlToMarkdown(html) {
|
|
14
|
+
if (!html)
|
|
15
|
+
return '';
|
|
16
|
+
return turndownService.turndown(html);
|
|
17
|
+
}
|
|
18
|
+
/** Normalize the ZenTao `files` field (can be an array or object) into a flat array. */
|
|
20
19
|
function parseFiles(files) {
|
|
21
20
|
if (!files)
|
|
22
21
|
return [];
|
|
@@ -24,89 +23,58 @@ function parseFiles(files) {
|
|
|
24
23
|
id: f.id,
|
|
25
24
|
title: f.title || f.name,
|
|
26
25
|
extension: f.extension,
|
|
27
|
-
size: f.size
|
|
26
|
+
size: f.size,
|
|
28
27
|
});
|
|
29
|
-
if (Array.isArray(files))
|
|
28
|
+
if (Array.isArray(files))
|
|
30
29
|
return files.map(mapper);
|
|
31
|
-
|
|
32
|
-
if (typeof files === 'object') {
|
|
30
|
+
if (typeof files === 'object')
|
|
33
31
|
return Object.values(files).map(mapper);
|
|
34
|
-
}
|
|
35
32
|
return [];
|
|
36
33
|
}
|
|
34
|
+
/** Format a ZenTao user field (object or string) into a human-readable string. */
|
|
35
|
+
function formatUser(user) {
|
|
36
|
+
if (!user)
|
|
37
|
+
return '';
|
|
38
|
+
if (typeof user === 'object') {
|
|
39
|
+
const name = user.realname || user.account || '';
|
|
40
|
+
const account = user.account ? ` (${user.account})` : '';
|
|
41
|
+
return `${name}${account}`;
|
|
42
|
+
}
|
|
43
|
+
return String(user);
|
|
44
|
+
}
|
|
37
45
|
/**
|
|
38
|
-
*
|
|
46
|
+
* Find all <img src="..."> URLs in an HTML string, download each image
|
|
47
|
+
* to the local tmp directory using the authenticated ZenTao client,
|
|
48
|
+
* and replace the remote src with the local file path.
|
|
49
|
+
* Images already cached on disk are not re-downloaded.
|
|
39
50
|
*/
|
|
40
|
-
function
|
|
51
|
+
async function localizeImages(html) {
|
|
41
52
|
if (!html)
|
|
42
|
-
return
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
realname: task.assignedTo.realname
|
|
65
|
-
} : undefined,
|
|
66
|
-
finishedBy: task.finishedBy ? {
|
|
67
|
-
account: task.finishedBy.account,
|
|
68
|
-
realname: task.finishedBy.realname
|
|
69
|
-
} : undefined,
|
|
70
|
-
closedBy: task.closedBy ? {
|
|
71
|
-
account: task.closedBy.account,
|
|
72
|
-
realname: task.closedBy.realname
|
|
73
|
-
} : undefined,
|
|
74
|
-
closedReason: task.closedReason,
|
|
75
|
-
files: parseFiles(task.files)
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
export function cleanBug(bug) {
|
|
79
|
-
if (!bug)
|
|
80
|
-
return null;
|
|
81
|
-
return {
|
|
82
|
-
id: bug.id,
|
|
83
|
-
title: bug.title,
|
|
84
|
-
status: bug.status,
|
|
85
|
-
severity: bug.severity,
|
|
86
|
-
pri: bug.pri,
|
|
87
|
-
type: bug.type,
|
|
88
|
-
steps: htmlToMarkdown(bug.steps),
|
|
89
|
-
openedBy: bug.openedBy ? {
|
|
90
|
-
account: bug.openedBy.account,
|
|
91
|
-
realname: bug.openedBy.realname
|
|
92
|
-
} : undefined,
|
|
93
|
-
assignedTo: bug.assignedTo ? {
|
|
94
|
-
account: bug.assignedTo.account,
|
|
95
|
-
realname: bug.assignedTo.realname
|
|
96
|
-
} : undefined,
|
|
97
|
-
resolvedBy: bug.resolvedBy ? {
|
|
98
|
-
account: bug.resolvedBy.account,
|
|
99
|
-
realname: bug.resolvedBy.realname
|
|
100
|
-
} : undefined,
|
|
101
|
-
resolution: bug.resolution,
|
|
102
|
-
closedBy: bug.closedBy ? {
|
|
103
|
-
account: bug.closedBy.account,
|
|
104
|
-
realname: bug.closedBy.realname
|
|
105
|
-
} : undefined,
|
|
106
|
-
files: parseFiles(bug.files)
|
|
107
|
-
};
|
|
53
|
+
return html;
|
|
54
|
+
const imgRegex = /<img[^>]+src=["']([^"']+)["']/gi;
|
|
55
|
+
const matches = [...html.matchAll(imgRegex)];
|
|
56
|
+
if (matches.length === 0)
|
|
57
|
+
return html;
|
|
58
|
+
let result = html;
|
|
59
|
+
await Promise.all(matches.map(async (match) => {
|
|
60
|
+
const remoteUrl = match[1];
|
|
61
|
+
// Derive a stable local filename from the last path segment of the URL
|
|
62
|
+
const filename = `zentao_img_${path.basename(remoteUrl.split('?')[0])}`;
|
|
63
|
+
const localPath = path.join(os.tmpdir(), filename);
|
|
64
|
+
try {
|
|
65
|
+
if (!fs.existsSync(localPath)) {
|
|
66
|
+
await client.downloadImageToLocal(remoteUrl, localPath);
|
|
67
|
+
}
|
|
68
|
+
result = result.split(remoteUrl).join(localPath);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// If download fails, keep the original remote URL
|
|
72
|
+
}
|
|
73
|
+
}));
|
|
74
|
+
return result;
|
|
108
75
|
}
|
|
109
|
-
|
|
76
|
+
/** Format a file size in bytes to a human-readable string (B / KB / MB). */
|
|
77
|
+
function formatSize(bytes) {
|
|
110
78
|
if (bytes === undefined || bytes === null || bytes === '')
|
|
111
79
|
return 'unknown size';
|
|
112
80
|
const numBytes = typeof bytes === 'string' ? parseInt(bytes, 10) : bytes;
|
|
@@ -118,132 +86,191 @@ export function formatSize(bytes) {
|
|
|
118
86
|
return `${(numBytes / 1024).toFixed(2)} KB`;
|
|
119
87
|
return `${(numBytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
120
88
|
}
|
|
121
|
-
|
|
122
|
-
|
|
89
|
+
/** Build a standard MCP text content response. */
|
|
90
|
+
function mcpText(text) {
|
|
91
|
+
return { content: [{ type: "text", text }] };
|
|
92
|
+
}
|
|
93
|
+
/** Download all attachments to local tmp dir and render them as a Markdown section with AI hints. */
|
|
94
|
+
async function renderAttachments(files) {
|
|
95
|
+
if (!files || files.length === 0)
|
|
96
|
+
return '';
|
|
97
|
+
const IMAGE_EXTS = new Set(['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'svg']);
|
|
98
|
+
const VIDEO_EXTS = new Set(['mp4', 'mov', 'avi', 'mkv', 'webm']);
|
|
99
|
+
const enriched = await Promise.all(files.map(async (f) => {
|
|
100
|
+
const ext = (f.extension || '').toLowerCase();
|
|
101
|
+
const targetPath = path.join(os.tmpdir(), `zentao_file_${f.id}.${ext}`);
|
|
102
|
+
let localPath = null;
|
|
103
|
+
try {
|
|
104
|
+
localPath = fs.existsSync(targetPath)
|
|
105
|
+
? targetPath
|
|
106
|
+
: await client.downloadFile(f.id, targetPath);
|
|
107
|
+
}
|
|
108
|
+
catch { /* keep null, show download failed below */ }
|
|
109
|
+
return { title: f.title, size: f.size, localPath, ext };
|
|
110
|
+
}));
|
|
111
|
+
// Build section header hint based on media types present
|
|
112
|
+
const hasImage = enriched.some(f => f.localPath && IMAGE_EXTS.has(f.ext));
|
|
113
|
+
const hasVideo = enriched.some(f => f.localPath && VIDEO_EXTS.has(f.ext));
|
|
114
|
+
let headerHint = '';
|
|
115
|
+
if (hasImage && hasVideo)
|
|
116
|
+
headerHint = ' *(AI: Please view the images and videos below)*';
|
|
117
|
+
else if (hasImage)
|
|
118
|
+
headerHint = ' *(AI: Please view the images below)*';
|
|
119
|
+
else if (hasVideo)
|
|
120
|
+
headerHint = ' *(AI: Please view the videos below)*';
|
|
121
|
+
const lines = enriched.map((f) => {
|
|
122
|
+
if (!f.localPath)
|
|
123
|
+
return `- 📎 ${f.title} — *(download failed, Size: ${formatSize(f.size)})*`;
|
|
124
|
+
if (IMAGE_EXTS.has(f.ext))
|
|
125
|
+
return `- 📎  *(Size: ${formatSize(f.size)})*`;
|
|
126
|
+
return `- 📎 [${f.title}](${f.localPath}) *(Size: ${formatSize(f.size)})*`;
|
|
127
|
+
});
|
|
128
|
+
return `\n## Files 📎${headerHint}\n${lines.join('\n')}\n`;
|
|
129
|
+
}
|
|
130
|
+
// ─── Public formatters ───────────────────────────────────────────────────────
|
|
131
|
+
/**
|
|
132
|
+
* Format a raw ZenTao task object into a human-readable Markdown document.
|
|
133
|
+
*/
|
|
134
|
+
export async function taskToMarkdown(rawTask) {
|
|
135
|
+
if (!rawTask)
|
|
123
136
|
return "Task not found.";
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (
|
|
135
|
-
|
|
136
|
-
if (
|
|
137
|
-
|
|
138
|
-
if (
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
md += `\n## Description\n${task.desc || '*No description provided.*'}\n`;
|
|
145
|
-
if (task.files && task.files.length > 0) {
|
|
146
|
-
md += `\n## Attachments\n`;
|
|
147
|
-
task.files.forEach((f) => {
|
|
148
|
-
md += `- 📎 **${f.title}** (ID: ${f.id}, Extension: ${f.extension}, Size: ${formatSize(f.size)})\n`;
|
|
149
|
-
});
|
|
137
|
+
const est = rawTask.estimate ?? 0;
|
|
138
|
+
const cons = rawTask.consumed ?? 0;
|
|
139
|
+
const left = rawTask.left ?? 0;
|
|
140
|
+
const prog = rawTask.progress ?? 0;
|
|
141
|
+
// ── Compact metadata row ──
|
|
142
|
+
const meta = [
|
|
143
|
+
`**Status:** ${rawTask.status || 'N/A'}`,
|
|
144
|
+
`**Priority:** ${rawTask.pri || 'N/A'}`,
|
|
145
|
+
`**Estimate/Consumed/Left:** ${est}h / ${cons}h / ${left}h (${prog}%)`,
|
|
146
|
+
];
|
|
147
|
+
if (rawTask.openedBy)
|
|
148
|
+
meta.push(`**Opened by:** ${formatUser(rawTask.openedBy)}`);
|
|
149
|
+
if (rawTask.assignedTo)
|
|
150
|
+
meta.push(`**Assigned to:** ${formatUser(rawTask.assignedTo)}`);
|
|
151
|
+
if (rawTask.finishedBy)
|
|
152
|
+
meta.push(`**Finished by:** ${formatUser(rawTask.finishedBy)}`);
|
|
153
|
+
if (rawTask.closedBy) {
|
|
154
|
+
const reason = rawTask.closedReason ? ` (${rawTask.closedReason})` : '';
|
|
155
|
+
meta.push(`**Closed by:** ${formatUser(rawTask.closedBy)}${reason}`);
|
|
150
156
|
}
|
|
151
|
-
|
|
157
|
+
const localizedDesc = await localizeImages(rawTask.desc);
|
|
158
|
+
const desc = htmlToMarkdown(localizedDesc);
|
|
159
|
+
const attachments = await renderAttachments(parseFiles(rawTask.files));
|
|
160
|
+
return [
|
|
161
|
+
`# Task #${rawTask.id}: ${rawTask.name}`,
|
|
162
|
+
'',
|
|
163
|
+
meta.join(' | '),
|
|
164
|
+
'',
|
|
165
|
+
'## Description',
|
|
166
|
+
desc || '*No description provided.*',
|
|
167
|
+
attachments,
|
|
168
|
+
].join('\n');
|
|
152
169
|
}
|
|
153
|
-
|
|
154
|
-
|
|
170
|
+
/**
|
|
171
|
+
* Format a raw ZenTao bug object into a human-readable Markdown document.
|
|
172
|
+
*/
|
|
173
|
+
export async function bugToMarkdown(rawBug) {
|
|
174
|
+
if (!rawBug)
|
|
155
175
|
return "Bug not found.";
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
if (
|
|
164
|
-
|
|
165
|
-
if (
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
176
|
+
// ── Compact metadata row ──
|
|
177
|
+
const meta = [
|
|
178
|
+
`**Status:** ${rawBug.status || 'N/A'}`,
|
|
179
|
+
`**Severity:** ${rawBug.severity || 'N/A'}`,
|
|
180
|
+
`**Priority:** ${rawBug.pri || 'N/A'}`,
|
|
181
|
+
`**Type:** ${rawBug.type || 'N/A'}`,
|
|
182
|
+
];
|
|
183
|
+
if (rawBug.openedBy)
|
|
184
|
+
meta.push(`**Opened by:** ${formatUser(rawBug.openedBy)}`);
|
|
185
|
+
if (rawBug.assignedTo)
|
|
186
|
+
meta.push(`**Assigned to:** ${formatUser(rawBug.assignedTo)}`);
|
|
187
|
+
if (rawBug.resolvedBy) {
|
|
188
|
+
const resolution = rawBug.resolution ? ` (${rawBug.resolution})` : '';
|
|
189
|
+
meta.push(`**Resolved by:** ${formatUser(rawBug.resolvedBy)}${resolution}`);
|
|
170
190
|
}
|
|
171
|
-
if (
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
191
|
+
if (rawBug.closedBy)
|
|
192
|
+
meta.push(`**Closed by:** ${formatUser(rawBug.closedBy)}`);
|
|
193
|
+
const localizedSteps = await localizeImages(rawBug.steps);
|
|
194
|
+
const steps = htmlToMarkdown(localizedSteps);
|
|
195
|
+
const attachments = await renderAttachments(parseFiles(rawBug.files));
|
|
196
|
+
return [
|
|
197
|
+
`# Bug #${rawBug.id}: ${rawBug.title}`,
|
|
198
|
+
'',
|
|
199
|
+
meta.join(' | '),
|
|
200
|
+
'',
|
|
201
|
+
'## Repro Steps',
|
|
202
|
+
steps || '*No steps provided.*',
|
|
203
|
+
attachments,
|
|
204
|
+
].join('\n');
|
|
205
|
+
}
|
|
206
|
+
// ─── Download handler (extracted for Single Responsibility) ──────────────────
|
|
207
|
+
/**
|
|
208
|
+
* Download a ZenTao attachment to a local path.
|
|
209
|
+
* Deduplicates concurrent requests for the same file.
|
|
210
|
+
*/
|
|
211
|
+
async function downloadAttachment(fileId, targetPath) {
|
|
212
|
+
if (fs.existsSync(targetPath) && !activeDownloads.has(targetPath)) {
|
|
213
|
+
return `File already exists locally at: ${targetPath}`;
|
|
214
|
+
}
|
|
215
|
+
if (activeDownloads.has(targetPath)) {
|
|
216
|
+
await activeDownloads.get(targetPath);
|
|
217
|
+
return `File downloaded successfully to: ${targetPath}`;
|
|
218
|
+
}
|
|
219
|
+
const downloadPromise = client.downloadFile(fileId, targetPath);
|
|
220
|
+
activeDownloads.set(targetPath, downloadPromise);
|
|
221
|
+
try {
|
|
222
|
+
const savedPath = await downloadPromise;
|
|
223
|
+
return `File downloaded successfully to: ${savedPath}`;
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
activeDownloads.delete(targetPath);
|
|
179
227
|
}
|
|
180
|
-
return md;
|
|
181
228
|
}
|
|
229
|
+
// ─── Tool registration (Open/Closed: add tools without touching existing ones) ─
|
|
230
|
+
/**
|
|
231
|
+
* Registers Zentao-specific tools to the Model Context Protocol (MCP) server.
|
|
232
|
+
*
|
|
233
|
+
* Available Tools:
|
|
234
|
+
* - `zentao_get_task_details`: Fetch full details of a specific task by ID.
|
|
235
|
+
* - `zentao_get_bug_details`: Fetch full details of a specific bug by ID.
|
|
236
|
+
* - `zentao_download_attachment`: Download attachments and save locally.
|
|
237
|
+
*
|
|
238
|
+
* @param server The MCP Server instance where the tools will be registered.
|
|
239
|
+
*/
|
|
182
240
|
export function registerTools(server) {
|
|
183
241
|
server.registerTool("zentao_get_task_details", {
|
|
184
242
|
description: "Get detailed information of a task",
|
|
185
243
|
inputSchema: {
|
|
186
244
|
taskId: z.union([z.string(), z.number()]).describe("Task ID"),
|
|
187
|
-
}
|
|
245
|
+
},
|
|
188
246
|
}, async ({ taskId }) => {
|
|
189
247
|
const data = await client.getTaskDetails(taskId);
|
|
190
|
-
|
|
191
|
-
return {
|
|
192
|
-
content: [{ type: "text", text: taskToMarkdown(cleaned) }],
|
|
193
|
-
};
|
|
248
|
+
return mcpText(await taskToMarkdown(data));
|
|
194
249
|
});
|
|
195
250
|
server.registerTool("zentao_get_bug_details", {
|
|
196
251
|
description: "Get detailed information of a bug",
|
|
197
252
|
inputSchema: {
|
|
198
253
|
bugId: z.union([z.string(), z.number()]).describe("Bug ID"),
|
|
199
|
-
}
|
|
254
|
+
},
|
|
200
255
|
}, async ({ bugId }) => {
|
|
201
256
|
const data = await client.getBugDetails(bugId);
|
|
202
|
-
|
|
203
|
-
return {
|
|
204
|
-
content: [{ type: "text", text: bugToMarkdown(cleaned) }],
|
|
205
|
-
};
|
|
257
|
+
return mcpText(await bugToMarkdown(data));
|
|
206
258
|
});
|
|
207
259
|
server.registerTool("zentao_download_attachment", {
|
|
208
260
|
description: "Download a file attachment from ZenTao and save it locally",
|
|
209
261
|
inputSchema: {
|
|
210
262
|
fileId: z.union([z.string(), z.number()]).describe("File ID to download"),
|
|
211
263
|
extension: z.string().optional().describe("Optional file extension (e.g., mp4, png)"),
|
|
212
|
-
}
|
|
264
|
+
},
|
|
213
265
|
}, async ({ fileId, extension }) => {
|
|
214
266
|
const ext = extension ? (extension.startsWith('.') ? extension : `.${extension}`) : '';
|
|
215
267
|
const targetPath = path.join(os.tmpdir(), `zentao_file_${fileId}${ext}`);
|
|
216
268
|
try {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
return {
|
|
220
|
-
content: [{ type: "text", text: `File already exists locally at: ${targetPath}` }],
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
// If it is currently being downloaded, await the active promise
|
|
224
|
-
if (activeDownloads.has(targetPath)) {
|
|
225
|
-
await activeDownloads.get(targetPath);
|
|
226
|
-
return {
|
|
227
|
-
content: [{ type: "text", text: `File downloaded successfully to: ${targetPath}` }],
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
// Start new download and track it
|
|
231
|
-
const downloadPromise = client.downloadFile(fileId, targetPath);
|
|
232
|
-
activeDownloads.set(targetPath, downloadPromise);
|
|
233
|
-
try {
|
|
234
|
-
const savedPath = await downloadPromise;
|
|
235
|
-
return {
|
|
236
|
-
content: [{ type: "text", text: `File downloaded successfully to: ${savedPath}` }],
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
finally {
|
|
240
|
-
activeDownloads.delete(targetPath);
|
|
241
|
-
}
|
|
269
|
+
const message = await downloadAttachment(fileId, targetPath);
|
|
270
|
+
return mcpText(message);
|
|
242
271
|
}
|
|
243
272
|
catch (error) {
|
|
244
|
-
return {
|
|
245
|
-
content: [{ type: "text", text: `Failed to download file: ${error.message}` }],
|
|
246
|
-
};
|
|
273
|
+
return mcpText(`Failed to download file: ${error.message}`);
|
|
247
274
|
}
|
|
248
275
|
});
|
|
249
276
|
}
|
package/build/zentaoClient.js
CHANGED
|
@@ -145,4 +145,32 @@ export class ZentaoClient {
|
|
|
145
145
|
});
|
|
146
146
|
});
|
|
147
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* Downloads an image from a full authenticated URL and saves it to a local path.
|
|
150
|
+
* Uses the same authenticated Axios client so ZenTao session token is injected automatically.
|
|
151
|
+
*
|
|
152
|
+
* @param imageUrl The full image URL to download (e.g. https://zentao.../file-read-xxx.png).
|
|
153
|
+
* @param targetPath The local file path where the image should be saved.
|
|
154
|
+
* @returns A promise that resolves to the targetPath upon success, or rejects with an error.
|
|
155
|
+
*/
|
|
156
|
+
async downloadImageToLocal(imageUrl, targetPath) {
|
|
157
|
+
const writer = fs.createWriteStream(targetPath);
|
|
158
|
+
const res = await this.client.get(imageUrl, {
|
|
159
|
+
baseURL: '', // override baseURL so the full URL is used as-is
|
|
160
|
+
responseType: 'stream',
|
|
161
|
+
});
|
|
162
|
+
return new Promise((resolve, reject) => {
|
|
163
|
+
res.data.pipe(writer);
|
|
164
|
+
let error = null;
|
|
165
|
+
writer.on('error', err => {
|
|
166
|
+
error = err;
|
|
167
|
+
writer.close();
|
|
168
|
+
reject(err);
|
|
169
|
+
});
|
|
170
|
+
writer.on('close', () => {
|
|
171
|
+
if (!error)
|
|
172
|
+
resolve(targetPath);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
}
|
|
148
176
|
}
|