@dyno181cm.nexsoft/zentao_mcp 1.4.0 → 1.4.1
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/zentaoClient.js +152 -2
- package/package.json +1 -1
package/build/zentaoClient.js
CHANGED
|
@@ -113,25 +113,175 @@ export class ZentaoClient {
|
|
|
113
113
|
throw new Error('Login failed: Token not found in response');
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Helper to generate standard action descriptions when they are not present in classic API.
|
|
118
|
+
*/
|
|
119
|
+
generateActionDesc(act, userMap) {
|
|
120
|
+
const actorName = userMap[act.actor] || act.actor || 'User';
|
|
121
|
+
let extraName = act.extra || '';
|
|
122
|
+
if (userMap[extraName]) {
|
|
123
|
+
extraName = userMap[extraName];
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
const resMap = {
|
|
127
|
+
'fixed': '已解决',
|
|
128
|
+
'design': '设计如此',
|
|
129
|
+
'duplicate': '重复Bug',
|
|
130
|
+
'external': '外部原因',
|
|
131
|
+
'notrepro': '无法重现',
|
|
132
|
+
'postponed': '延期处理',
|
|
133
|
+
'willnotfix': '不予解决',
|
|
134
|
+
'tostory': '转为需求',
|
|
135
|
+
};
|
|
136
|
+
if (resMap[extraName]) {
|
|
137
|
+
extraName = resMap[extraName];
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
switch (act.action) {
|
|
141
|
+
case 'opened':
|
|
142
|
+
return `由 <strong>${actorName}</strong> 创建。`;
|
|
143
|
+
case 'assigned':
|
|
144
|
+
return `由 <strong>${actorName}</strong> 指派给 <strong>${extraName}</strong>。`;
|
|
145
|
+
case 'started':
|
|
146
|
+
if (act.extra === 'autobychild') {
|
|
147
|
+
return `由 <strong>${actorName}</strong> 启动子任务,该任务自动启动。`;
|
|
148
|
+
}
|
|
149
|
+
return `由 <strong>${actorName}</strong> 启动。`;
|
|
150
|
+
case 'finished':
|
|
151
|
+
return `由 <strong>${actorName}</strong> 完成。`;
|
|
152
|
+
case 'resolved':
|
|
153
|
+
return `由 <strong>${actorName}</strong> 解决,方案为 <strong>${extraName}</strong>。`;
|
|
154
|
+
case 'closed':
|
|
155
|
+
return `由 <strong>${actorName}</strong> 关闭。`;
|
|
156
|
+
case 'edited':
|
|
157
|
+
return `由 <strong>${actorName}</strong> 编辑。`;
|
|
158
|
+
case 'bugconfirmed':
|
|
159
|
+
case 'confirmed':
|
|
160
|
+
return `由 <strong>${actorName}</strong> 确认Bug。`;
|
|
161
|
+
case 'activated':
|
|
162
|
+
return `由 <strong>${actorName}</strong> 激活。`;
|
|
163
|
+
case 'commented':
|
|
164
|
+
return `由 <strong>${actorName}</strong> 备注。`;
|
|
165
|
+
case 'createchildren':
|
|
166
|
+
return `由 <strong>${actorName}</strong> 创建子任务 ${act.extra || ''}。`;
|
|
167
|
+
default:
|
|
168
|
+
if (act.action && act.action.startsWith('subtask')) {
|
|
169
|
+
return `由 <strong>${actorName}</strong> ${act.action}。`;
|
|
170
|
+
}
|
|
171
|
+
return `由 <strong>${actorName}</strong> 执行了 <strong>${act.action || '未知'}</strong> 操作。`;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Fetches classic JSON API fallback data when REST API returns empty/errors.
|
|
176
|
+
*/
|
|
177
|
+
async getFallbackClassicData(type, id) {
|
|
178
|
+
const webBaseUrl = this.baseUrl.split('/api.php/v1')[0];
|
|
179
|
+
const url = `${webBaseUrl}/${type}-view-${id}.json`;
|
|
180
|
+
const res = await this.client.get(url, {
|
|
181
|
+
baseURL: '', // override baseURL
|
|
182
|
+
});
|
|
183
|
+
const responseData = typeof res.data === 'string' ? JSON.parse(res.data) : res.data;
|
|
184
|
+
if (responseData && responseData.status === 'success') {
|
|
185
|
+
const detailData = typeof responseData.data === 'string' ? JSON.parse(responseData.data) : responseData.data;
|
|
186
|
+
if (detailData) {
|
|
187
|
+
const entity = detailData[type];
|
|
188
|
+
if (entity) {
|
|
189
|
+
const userMap = detailData.users || {};
|
|
190
|
+
// Normalize user fields
|
|
191
|
+
const userFields = ['openedBy', 'assignedTo', 'resolvedBy', 'closedBy', 'finishedBy', 'canceledBy', 'lastEditedBy'];
|
|
192
|
+
for (const field of userFields) {
|
|
193
|
+
const val = entity[field];
|
|
194
|
+
if (val && typeof val === 'string') {
|
|
195
|
+
entity[field] = {
|
|
196
|
+
account: val,
|
|
197
|
+
realname: userMap[val] || val
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (entity.assignedTo?.account === 'closed') {
|
|
202
|
+
entity.assignedTo = null;
|
|
203
|
+
}
|
|
204
|
+
// Normalize actions list to sorted array
|
|
205
|
+
let actionsArray = [];
|
|
206
|
+
if (detailData.actions) {
|
|
207
|
+
if (Array.isArray(detailData.actions)) {
|
|
208
|
+
actionsArray = detailData.actions;
|
|
209
|
+
}
|
|
210
|
+
else if (typeof detailData.actions === 'object') {
|
|
211
|
+
actionsArray = Object.values(detailData.actions);
|
|
212
|
+
actionsArray.sort((a, b) => (Number(a.id) || 0) - (Number(b.id) || 0));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// Generate descriptions if missing
|
|
216
|
+
for (const act of actionsArray) {
|
|
217
|
+
if (!act.desc) {
|
|
218
|
+
act.desc = `${act.date || ''}, ` + this.generateActionDesc(act, userMap);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
entity.actions = actionsArray;
|
|
222
|
+
return entity;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
throw new Error(`Failed to fetch ${type} details from classic fallback API for ID ${id}`);
|
|
227
|
+
}
|
|
116
228
|
/**
|
|
117
229
|
* Retrieves details of a specific task.
|
|
118
230
|
* Checks the cache first, otherwise fetches from API.
|
|
231
|
+
* Falls back to classic JSON API if REST API returns empty/invalid response.
|
|
119
232
|
*
|
|
120
233
|
* @param taskId The ID of the task.
|
|
121
234
|
* @returns Resolves with the task details.
|
|
122
235
|
*/
|
|
123
236
|
async getTaskDetails(taskId) {
|
|
124
|
-
|
|
237
|
+
try {
|
|
238
|
+
const task = await this.get(`/tasks/${taskId}`);
|
|
239
|
+
if (!task || task === "") {
|
|
240
|
+
const fallbackTask = await this.getFallbackClassicData('task', taskId);
|
|
241
|
+
this.getCache.set(`/tasks/${taskId}`, { data: fallbackTask, timestamp: Date.now() });
|
|
242
|
+
return fallbackTask;
|
|
243
|
+
}
|
|
244
|
+
return task;
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
try {
|
|
248
|
+
const fallbackTask = await this.getFallbackClassicData('task', taskId);
|
|
249
|
+
this.getCache.set(`/tasks/${taskId}`, { data: fallbackTask, timestamp: Date.now() });
|
|
250
|
+
return fallbackTask;
|
|
251
|
+
}
|
|
252
|
+
catch (fallbackError) {
|
|
253
|
+
throw error; // throw original error if fallback also fails
|
|
254
|
+
}
|
|
255
|
+
}
|
|
125
256
|
}
|
|
126
257
|
/**
|
|
127
258
|
* Retrieves details of a specific bug.
|
|
128
259
|
* Checks the cache first, otherwise fetches from API.
|
|
260
|
+
* Falls back to classic JSON API if REST API returns empty/invalid response.
|
|
129
261
|
*
|
|
130
262
|
* @param bugId The ID of the bug.
|
|
131
263
|
* @returns Resolves with the bug details.
|
|
132
264
|
*/
|
|
133
265
|
async getBugDetails(bugId) {
|
|
134
|
-
|
|
266
|
+
try {
|
|
267
|
+
const bug = await this.get(`/bugs/${bugId}`);
|
|
268
|
+
if (!bug || bug === "") {
|
|
269
|
+
const fallbackBug = await this.getFallbackClassicData('bug', bugId);
|
|
270
|
+
this.getCache.set(`/bugs/${bugId}`, { data: fallbackBug, timestamp: Date.now() });
|
|
271
|
+
return fallbackBug;
|
|
272
|
+
}
|
|
273
|
+
return bug;
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
try {
|
|
277
|
+
const fallbackBug = await this.getFallbackClassicData('bug', bugId);
|
|
278
|
+
this.getCache.set(`/bugs/${bugId}`, { data: fallbackBug, timestamp: Date.now() });
|
|
279
|
+
return fallbackBug;
|
|
280
|
+
}
|
|
281
|
+
catch (fallbackError) {
|
|
282
|
+
throw error; // throw original error if fallback also fails
|
|
283
|
+
}
|
|
284
|
+
}
|
|
135
285
|
}
|
|
136
286
|
/**
|
|
137
287
|
* Downloads a file from Zentao API and pipes it to a local file path.
|