@wallacewen/tapd-mcp-server
Version:
Model Context Protocol server for TAPD (Tencent Agile Product Development) - Provides professional weekly report generation and timesheet analysis
504 lines • 25.3 kB
JavaScript
import axios from 'axios';
export class TAPDClient {
httpClient;
workflowClient;
baseUrl;
workflowBaseUrl;
workflowToken;
constructor(baseUrl, workflowBaseUrl, workflowToken) {
this.baseUrl =
baseUrl || process.env.TAPD_BASE_URL || 'http://porsche-tapd-inc.chinahuanong.com.cn';
this.workflowBaseUrl =
workflowBaseUrl || process.env.TAPD_WORKFLOW_URL || 'https://faq-flow.chinahuanong.com.cn';
this.workflowToken =
workflowToken || process.env.TAPD_WORKFLOW_API_KEY || 'app-Q9fnctG4oQhyIdxCWyALtlwE';
this.httpClient = axios.create({
baseURL: this.baseUrl,
timeout: 30000,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': 'TAPD-MCP-Server/1.0.0',
},
});
this.workflowClient = axios.create({
baseURL: this.workflowBaseUrl,
timeout: 30000,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.workflowToken}`,
'User-Agent': 'TAPD-MCP-Server/1.0.0',
},
});
}
/**
* Query time sheets for a specific user and date range
* 完全匹配test.http中的POST请求格式
* @param params Parameters for querying time sheets
* @returns Promise containing the time sheet data
*/
async queryTimeSheets(params) {
try {
const { name, startDate, endDate } = params;
// Validate date format (expected: YYYY-MM-DD)
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
if (!dateRegex.test(startDate) || !dateRegex.test(endDate)) {
throw new Error('Date format should be YYYY-MM-DD');
}
// Validate that startDate is not after endDate
if (new Date(startDate) > new Date(endDate)) {
throw new Error('Start date should not be after end date');
}
// 构造请求体,完全匹配test.http中的参数顺序和格式
// 参数顺序:name, endDate, startDate (与test.http保持一致)
const formData = new URLSearchParams();
formData.append('name', String(name));
formData.append('endDate', String(endDate));
formData.append('startDate', String(startDate));
console.log('发送TAPD请求:', {
url: `${this.baseUrl}/ai/queryTimeSheets`,
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
data: formData.toString(),
});
const response = await this.httpClient.post('/ai/queryTimeSheets', formData.toString());
console.log('TAPD响应:', {
status: response.status,
data: response.data,
});
// Handle different response formats
if (response.data) {
// TAPD API返回的数据格式: { work_logs: [...], total_hours: 56, name: '潘明哲' }
let dataArray = [];
let total = 0;
if (response.data.work_logs && Array.isArray(response.data.work_logs)) {
// 展平嵌套的工时数据
const flattenedData = [];
response.data.work_logs.forEach((dayLog) => {
const date = dayLog.date;
dayLog.tasks?.forEach((story) => {
story.tasks?.forEach((task, index) => {
flattenedData.push({
id: `${story.story_id}_${index}`,
name: name,
date: date,
hours: task.hours,
project: task.project,
task: task.tasksTitle,
description: task.remark,
storyTitle: story.title,
storyId: story.story_id,
taskType: task.taskType,
taskClassify: task.taskClassify,
storyStatus: story.storyStatus,
storyIteration: story.storyIteration,
totalHours: story.totalHours,
taskCompletion: story.taskCompletion,
});
});
});
});
dataArray = flattenedData;
total = dataArray.length;
}
else if (Array.isArray(response.data)) {
dataArray = response.data;
total = dataArray.length;
}
else if (response.data.data && Array.isArray(response.data.data)) {
dataArray = response.data.data;
total = response.data.total || dataArray.length;
}
return {
success: true,
data: dataArray,
message: response.data.message || 'Success',
total: response.data.total_hours || total,
};
}
return {
success: true,
data: [],
message: 'No data returned',
total: 0,
};
}
catch (error) {
console.error('Error querying time sheets:', error);
if (axios.isAxiosError(error)) {
const errorMessage = error.response?.data?.message || error.message || 'Unknown error';
const statusCode = error.response?.status || 0;
const responseData = error.response?.data || {};
console.error('TAPD API错误详情:', {
status: statusCode,
message: errorMessage,
data: responseData,
url: error.config?.url || 'unknown',
method: error.config?.method || 'unknown',
});
return {
success: false,
data: [],
message: `Request failed (${statusCode}): ${errorMessage}`,
total: 0,
};
}
return {
success: false,
data: [],
message: error instanceof Error ? error.message : 'Unknown error occurred',
total: 0,
};
}
}
/**
* Select time sheet data for mantis operation analysis
* 完全匹配Untitled-1.http中的POST请求格式
* @param params Parameters for selecting time sheet data
* @returns Promise containing the mantis analysis data
*/
async selectTimeSheet(params) {
try {
const { startDate, endDate } = params;
// Validate date format (expected: YYYYMMDD)
const dateRegex = /^\d{8}$/;
if (!dateRegex.test(startDate) || !dateRegex.test(endDate)) {
throw new Error('Date format should be YYYYMMDD');
}
// Validate that startDate is not after endDate
const startDateObj = new Date(parseInt(startDate.substring(0, 4)), parseInt(startDate.substring(4, 6)) - 1, parseInt(startDate.substring(6, 8)));
const endDateObj = new Date(parseInt(endDate.substring(0, 4)), parseInt(endDate.substring(4, 6)) - 1, parseInt(endDate.substring(6, 8)));
if (startDateObj > endDateObj) {
throw new Error('Start date should not be after end date');
}
// 构造请求体,完全匹配Untitled-1.http中的参数顺序和格式
// 参数顺序:endDate, startDate (与Untitled-1.http保持一致)
const formData = new URLSearchParams();
formData.append('endDate', String(endDate));
formData.append('startDate', String(startDate));
console.log('发送TAPD selectTimeSheet请求:', {
url: `${this.baseUrl}/ai/selectTimeSheet`,
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
data: formData.toString(),
});
const response = await this.httpClient.post('/ai/selectTimeSheet', formData.toString());
console.log('TAPD selectTimeSheet响应:', {
status: response.status,
data: response.data,
});
// Handle different response formats
if (response.data) {
// TAPD API返回的数据格式处理
let dataArray = [];
let total = 0;
if (response.data.analysis_results && Array.isArray(response.data.analysis_results)) {
// 处理mantis运维分析结果数据
const flattenedData = [];
response.data.analysis_results.forEach((result, index) => {
flattenedData.push({
id: `analysis_${index}`,
date: result.date,
hours: result.hours,
project: result.project,
task: result.task,
description: result.description,
analysisType: result.analysis_type || 'mantis',
severity: result.severity,
status: result.status,
...result,
});
});
dataArray = flattenedData;
total = dataArray.length;
}
else if (Array.isArray(response.data)) {
dataArray = response.data;
total = dataArray.length;
}
else if (response.data.data && Array.isArray(response.data.data)) {
dataArray = response.data.data;
total = response.data.total || dataArray.length;
}
else if (typeof response.data === 'object' && response.data !== null) {
// 处理mantis运维分析的实际返回格式:以系统名称为键的对象结构
const flattenedData = [];
let index = 0;
Object.entries(response.data).forEach(([systemName, systemData]) => {
if (systemData && typeof systemData === 'object') {
// 创建系统概览条目 - 包含所有原始数据
flattenedData.push({
id: `system_${index++}`,
project: systemName,
analysisType: 'mantis_system_overview',
description: `${systemName} 运维统计概览`,
// 本周数据
thisWeekNonModifiedCount: systemData.thisWeekNonModifiedDataCount || 0,
thisWeekNonModifiedWithin8h: systemData.thisWeekNonModifiedDataWithin8HoursCount || 0,
thisWeekNonModifiedExceeding8h: systemData.thisWeekNonModifiedDataExceeding8HoursCount || 0,
thisWeekModifiedCount: systemData.thisWeekModifiedDataCount || 0,
// 今年数据
thisYearNonModifiedCount: systemData.thisYearNonModifiedDataCount || 0,
thisYearModifiedCount: systemData.thisYearModifiedDataCount || 0,
thisYearNonModifiedTimeOutCount: systemData.thisYearNonModifiedDataTimeOutCount || 0,
// 去年数据
lastYearNonModifiedCount: systemData.lastYearNonModifiedDataCount || 0,
lastYearModifiedCount: systemData.lastYearModifiedDataCount || 0,
lastYearNonModifiedTimeOutCount: systemData.lastYearNonModifiedDataTimeOutCount || 0,
// 保留所有原始数据字段
...systemData,
});
// 处理12周修改数据趋势
if (systemData.lastTwelveWeeksModifiedDataVolumeTrend &&
typeof systemData.lastTwelveWeeksModifiedDataVolumeTrend === 'object') {
Object.entries(systemData.lastTwelveWeeksModifiedDataVolumeTrend).forEach(([weekKey, weekValue]) => {
flattenedData.push({
id: `trend_modified_${index}_${weekKey}`,
project: systemName,
analysisType: 'mantis_trend_modified',
task: `${weekKey}修改数据趋势`,
description: `${systemName} - ${weekKey}修改数据量: ${weekValue}`,
weekKey,
weekValue: Number(weekValue),
trendType: 'modified',
period: weekKey,
count: Number(weekValue),
});
});
}
// 处理12周未修改数据趋势
if (systemData.lastTwelveWeeksNonModifiedDataVolumeTrend &&
typeof systemData.lastTwelveWeeksNonModifiedDataVolumeTrend === 'object') {
Object.entries(systemData.lastTwelveWeeksNonModifiedDataVolumeTrend).forEach(([weekKey, weekValue]) => {
flattenedData.push({
id: `trend_nonmodified_${index}_${weekKey}`,
project: systemName,
analysisType: 'mantis_trend_nonmodified',
task: `${weekKey}未修改数据趋势`,
description: `${systemName} - ${weekKey}未修改数据量: ${weekValue}`,
weekKey,
weekValue: Number(weekValue),
trendType: 'nonmodified',
period: weekKey,
count: Number(weekValue),
});
});
}
// 处理本周已修改数据的详细信息(8小时内处理)
if (systemData.thisWeekModifiedDataProcessingTimeWithin8HoursMantisDetails &&
Array.isArray(systemData.thisWeekModifiedDataProcessingTimeWithin8HoursMantisDetails)) {
systemData.thisWeekModifiedDataProcessingTimeWithin8HoursMantisDetails.forEach((detail, detailIndex) => {
flattenedData.push({
id: `within8h_${index}_${detailIndex}`,
project: systemName,
analysisType: 'mantis_within8h_detail',
task: detail.summary || 'mantis问题处理',
description: `8小时内处理完成 - ${detail.summary}`,
severity: '正常',
status: detail.status || '已解决',
processingTime: detail.processingTime,
mantisId: detail.mantisId,
currentHandler: detail.currentHandler,
submitter: detail.submitter,
dateFixed: detail.dateFixed,
lastReplyTime: detail.lastReplyTime,
submitTime: detail.submitTime,
summary: detail.summary,
projectName: detail.projectName,
category: detail.category,
knowledgeBaseCovered: detail.knowledgeBaseCovered,
// 保留所有原始字段
...detail,
});
});
}
// 处理超过8小时的详细信息
if (systemData.thisWeekModifiedDataProcessingTimeExceeding8HoursMantisDetails &&
Array.isArray(systemData.thisWeekModifiedDataProcessingTimeExceeding8HoursMantisDetails)) {
systemData.thisWeekModifiedDataProcessingTimeExceeding8HoursMantisDetails.forEach((detail, detailIndex) => {
flattenedData.push({
id: `exceeding8h_${index}_${detailIndex}`,
project: systemName,
analysisType: 'mantis_exceeding8h_detail',
task: detail.summary || 'mantis问题处理',
description: `处理时间超过8小时 - ${detail.summary}`,
severity: '高',
status: detail.status || '已解决',
processingTime: detail.processingTime,
mantisId: detail.mantisId,
currentHandler: detail.currentHandler,
submitter: detail.submitter,
dateFixed: detail.dateFixed,
lastReplyTime: detail.lastReplyTime,
submitTime: detail.submitTime,
summary: detail.summary,
projectName: detail.projectName,
category: detail.category,
knowledgeBaseCovered: detail.knowledgeBaseCovered,
// 保留所有原始字段
...detail,
});
});
}
}
});
dataArray = flattenedData;
total = dataArray.length;
}
return {
success: true,
data: dataArray,
message: response.data.message || 'Success',
total: response.data.total || total,
};
}
return {
success: true,
data: [],
message: 'No data returned',
total: 0,
};
}
catch (error) {
console.error('Error selecting time sheet:', error);
if (axios.isAxiosError(error)) {
const errorMessage = error.response?.data?.message || error.message || 'Unknown error';
const statusCode = error.response?.status || 0;
const responseData = error.response?.data || {};
console.error('TAPD selectTimeSheet API错误详情:', {
status: statusCode,
message: errorMessage,
data: responseData,
url: error.config?.url || 'unknown',
method: error.config?.method || 'unknown',
});
return {
success: false,
data: [],
message: `Request failed (${statusCode}): ${errorMessage}`,
total: 0,
};
}
return {
success: false,
data: [],
message: error instanceof Error ? error.message : 'Unknown error occurred',
total: 0,
};
}
}
/**
* Get the base URL being used
* @returns The base URL for TAPD API
*/
/**
* Run workflow to query TAPD bug list and details
* @param params Parameters for workflow run
* @returns Promise containing the workflow run response
*/
async runWorkflow(params) {
try {
const { msg } = params;
// 构造请求体
const requestBody = {
inputs: {
msg: msg,
},
user: process.env.TAPD_WORKFLOW_USER_ID || 'squ_df9f02baf35492ac997de6155f1b08b38e6f72a2',
response_mode: 'blocking',
};
console.log('发送工作流请求:', {
url: `${this.workflowBaseUrl}/v1/workflows/run`,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.workflowToken}`,
},
data: requestBody,
});
const response = await this.workflowClient.post('/v1/workflows/run', requestBody);
console.log('工作流响应:', {
status: response.status,
data: response.data,
});
// 处理响应数据
if (response.data) {
return {
success: true,
data: response.data,
message: response.data.data?.error || 'Success',
};
}
return {
success: true,
data: response.data,
message: 'No data returned',
};
}
catch (error) {
console.error('Error running workflow:', error);
if (axios.isAxiosError(error)) {
const statusCode = error.response?.status;
const errorMessage = error.response?.data?.message || error.message || 'Unknown error';
const responseData = error.response?.data;
console.error('工作流API错误详情:', {
status: statusCode,
message: errorMessage,
data: responseData,
url: error.config?.url || 'unknown',
method: error.config?.method || 'unknown',
});
return {
success: false,
data: {
task_id: '',
workflow_run_id: '',
data: {
id: '',
workflow_id: '',
status: 'failed',
outputs: {},
error: errorMessage,
elapsed_time: 0,
total_tokens: 0,
total_steps: 0,
created_at: 0,
finished_at: 0,
},
},
message: `Request failed (${statusCode}): ${errorMessage}`,
};
}
return {
success: false,
data: {
task_id: '',
workflow_run_id: '',
data: {
id: '',
workflow_id: '',
status: 'failed',
outputs: {},
error: error instanceof Error ? error.message : 'Unknown error occurred',
elapsed_time: 0,
total_tokens: 0,
total_steps: 0,
created_at: 0,
finished_at: 0,
},
},
message: error instanceof Error ? error.message : 'Unknown error occurred',
};
}
}
getBaseUrl() {
return this.baseUrl;
}
getWorkflowBaseUrl() {
return this.workflowBaseUrl;
}
}
//# sourceMappingURL=tapd.js.map