UNPKG

@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
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