@maxenlin/mcp-zentao-11-3 1.0.0-patch.1 → 1.0.2
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/README.md +26 -57
- package/dist/index.js +75 -6
- package/dist/zentaoLegacyApi.d.ts +43 -0
- package/dist/zentaoLegacyApi.js +177 -23
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @maxenlin/mcp-zentao-11-3
|
|
2
2
|
|
|
3
|
-
禅道 11.3 Legacy 版 MCP
|
|
3
|
+
禅道 11.3 Legacy 版 MCP 服务器,支持所有兼容 MCP 协议的 IDE 和工具(如 Cursor IDE、Claude Desktop、Continue 等),只支持旧版 Session API。
|
|
4
4
|
|
|
5
5
|
## ✨ 特性
|
|
6
6
|
|
|
@@ -15,33 +15,13 @@
|
|
|
15
15
|
|
|
16
16
|
## 📦 安装
|
|
17
17
|
|
|
18
|
-
### 方法 1
|
|
19
|
-
|
|
20
|
-
在 Cursor IDE 配置文件中添加:
|
|
21
|
-
|
|
22
|
-
```json
|
|
23
|
-
{
|
|
24
|
-
"mcpServers": {
|
|
25
|
-
"zentao-11-3": {
|
|
26
|
-
"command": "npx",
|
|
27
|
-
"args": ["-y", "@maxenlin/mcp-zentao-11-3"],
|
|
28
|
-
"env": {
|
|
29
|
-
"ZENTAO_URL": "http://your-zentao-url/zentao",
|
|
30
|
-
"ZENTAO_USERNAME": "your-username",
|
|
31
|
-
"ZENTAO_PASSWORD": "your-password"
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
### 方法 2:本地安装
|
|
18
|
+
### 方法 1:本地安装
|
|
39
19
|
|
|
40
20
|
```bash
|
|
41
21
|
npm install -g @maxenlin/mcp-zentao-11-3
|
|
42
22
|
```
|
|
43
23
|
|
|
44
|
-
|
|
24
|
+
然后在支持 MCP 的 IDE/工具配置文件中添加(以 Cursor IDE 为例):
|
|
45
25
|
|
|
46
26
|
```json
|
|
47
27
|
{
|
|
@@ -59,18 +39,21 @@ npm install -g @maxenlin/mcp-zentao-11-3
|
|
|
59
39
|
}
|
|
60
40
|
```
|
|
61
41
|
|
|
62
|
-
|
|
42
|
+
**配置说明:**
|
|
43
|
+
- `ZENTAO_URL`: 禅道服务器地址(必须包含 `/zentao` 路径)
|
|
44
|
+
- `ZENTAO_USERNAME`: 禅道用户名
|
|
45
|
+
- `ZENTAO_PASSWORD`: 禅道密码
|
|
63
46
|
|
|
64
|
-
### 方法
|
|
47
|
+
### 方法 2:使用 npx
|
|
65
48
|
|
|
66
|
-
|
|
49
|
+
在支持 MCP 的 IDE/工具配置文件中添加(以 Cursor IDE 为例):
|
|
67
50
|
|
|
68
51
|
```json
|
|
69
52
|
{
|
|
70
53
|
"mcpServers": {
|
|
71
54
|
"zentao-11-3": {
|
|
72
|
-
"command": "
|
|
73
|
-
"args": [],
|
|
55
|
+
"command": "npx",
|
|
56
|
+
"args": ["-y", "@maxenlin/mcp-zentao-11-3"],
|
|
74
57
|
"env": {
|
|
75
58
|
"ZENTAO_URL": "http://your-zentao-url/zentao",
|
|
76
59
|
"ZENTAO_USERNAME": "your-username",
|
|
@@ -86,36 +69,9 @@ npm install -g @maxenlin/mcp-zentao-11-3
|
|
|
86
69
|
- `ZENTAO_USERNAME`: 禅道用户名
|
|
87
70
|
- `ZENTAO_PASSWORD`: 禅道密码
|
|
88
71
|
|
|
89
|
-
### 方法 2:使用配置文件(备选)
|
|
90
|
-
|
|
91
|
-
如果不想在 Cursor 配置中直接填写密码,可以创建配置文件:
|
|
92
|
-
|
|
93
|
-
**Windows:**
|
|
94
|
-
```
|
|
95
|
-
C:\Users\你的用户名\.zentao\config.json
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
**macOS/Linux:**
|
|
99
|
-
```
|
|
100
|
-
~/.zentao/config.json
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
配置文件内容:
|
|
104
|
-
|
|
105
|
-
```json
|
|
106
|
-
{
|
|
107
|
-
"url": "http://your-zentao-url/zentao",
|
|
108
|
-
"username": "your-username",
|
|
109
|
-
"password": "your-password",
|
|
110
|
-
"apiVersion": "legacy"
|
|
111
|
-
}
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
**注意:** 环境变量配置优先于配置文件。如果同时设置了环境变量和配置文件,将使用环境变量中的配置。
|
|
115
|
-
|
|
116
72
|
## 🚀 使用
|
|
117
73
|
|
|
118
|
-
|
|
74
|
+
配置完成后,重启您的 IDE/工具即可使用。
|
|
119
75
|
|
|
120
76
|
### 基础功能
|
|
121
77
|
|
|
@@ -176,70 +132,84 @@ C:\Users\你的用户名\.zentao\config.json
|
|
|
176
132
|
## 📋 可用工具
|
|
177
133
|
|
|
178
134
|
### 配置管理
|
|
135
|
+
|
|
179
136
|
- `initZentao` - 初始化禅道连接
|
|
180
137
|
- `getConfig` - 查看配置信息
|
|
181
138
|
|
|
182
139
|
### 任务管理
|
|
140
|
+
|
|
183
141
|
- `getMyTasks` - 获取我的任务列表
|
|
184
142
|
- `getTaskDetail` - 获取任务详情
|
|
185
143
|
- `updateTask` - 更新任务
|
|
186
144
|
- `finishTask` - 完成任务
|
|
187
145
|
|
|
188
146
|
### Bug 管理
|
|
147
|
+
|
|
189
148
|
- `getMyBugs` - 获取我的Bug列表
|
|
190
149
|
- `getBugDetail` - 获取Bug详情
|
|
191
150
|
- `resolveBug` - 解决Bug
|
|
192
151
|
|
|
193
152
|
### 产品管理
|
|
153
|
+
|
|
194
154
|
- `getProducts` - 获取产品列表
|
|
195
155
|
|
|
196
156
|
### 需求管理
|
|
157
|
+
|
|
197
158
|
- `getProductStories` - 获取产品的需求列表
|
|
198
159
|
- `getStoryDetail` - 获取需求详情
|
|
199
160
|
- `searchStories` - 搜索需求
|
|
200
161
|
- `searchStoriesByProductName` - 按产品名称搜索需求
|
|
201
162
|
|
|
202
163
|
### 测试用例管理
|
|
164
|
+
|
|
203
165
|
- `getProductTestCases` - 获取产品的测试用例
|
|
204
166
|
- `getTestCaseDetail` - 获取测试用例详情
|
|
205
167
|
- `createTestCase` - 创建测试用例
|
|
206
168
|
- `getStoryTestCases` - 获取需求的测试用例
|
|
207
169
|
|
|
208
170
|
### 测试单管理
|
|
171
|
+
|
|
209
172
|
- `getTestTasks` - 获取测试单列表
|
|
210
173
|
- `getTestTaskDetail` - 获取测试单详情
|
|
211
174
|
- `getTestTaskResults` - 获取测试单的测试结果
|
|
212
175
|
- `runTestCase` - 执行测试用例
|
|
213
176
|
|
|
214
177
|
### 关联关系查询
|
|
178
|
+
|
|
215
179
|
- `getStoryRelatedBugs` - 获取需求关联的 Bug 列表
|
|
216
180
|
- `getBugRelatedStory` - 获取 Bug 关联的需求
|
|
217
181
|
|
|
218
182
|
### 批量操作
|
|
183
|
+
|
|
219
184
|
- `batchUpdateTasks` - 批量更新任务
|
|
220
185
|
- `batchResolveBugs` - 批量解决 Bug
|
|
221
186
|
|
|
222
187
|
### 数据统计
|
|
188
|
+
|
|
223
189
|
- `getMyTaskStatistics` - 获取我的任务统计信息
|
|
224
190
|
- `getMyBugStatistics` - 获取我的 Bug 统计信息
|
|
225
191
|
|
|
226
192
|
### AI 编程辅助功能
|
|
193
|
+
|
|
227
194
|
- `getDevelopmentContext` - 获取需求/Bug 的完整开发上下文(包含关联信息)
|
|
228
195
|
- `generateStorySummary` - 生成需求摘要(支持 JSON/Markdown/文本格式)
|
|
229
196
|
- `generateBugSummary` - 生成 Bug 摘要(支持 JSON/Markdown/文本格式)
|
|
230
197
|
- `formatTaskAsMarkdown` - 将任务格式化为 Markdown
|
|
231
198
|
|
|
232
199
|
### 智能分析功能
|
|
200
|
+
|
|
233
201
|
- `analyzeStoryComplexity` - 分析需求复杂度(评分、工时估算、优先级建议)
|
|
234
202
|
- `analyzeBugPriority` - 分析 Bug 优先级(评分、优先级建议)
|
|
235
203
|
- `analyzeTaskWorkload` - 分析任务工作量(工时估算、难度评估)
|
|
236
204
|
|
|
237
205
|
### 代码生成提示
|
|
206
|
+
|
|
238
207
|
- `generateCodePromptFromStory` - 根据需求生成代码框架提示
|
|
239
208
|
- `generateTestPromptFromBug` - 根据 Bug 生成测试用例提示
|
|
240
209
|
- `generateCodeReviewChecklist` - 生成代码审查检查清单
|
|
241
210
|
|
|
242
211
|
### 根据需求/Bug创建任务
|
|
212
|
+
|
|
243
213
|
- `createTaskFromStory` - 根据需求创建任务(提供手动操作指南)
|
|
244
214
|
- `createTaskFromBug` - 根据Bug创建修复任务(提供手动操作指南)
|
|
245
215
|
|
|
@@ -251,4 +221,3 @@ MIT
|
|
|
251
221
|
|
|
252
222
|
- [禅道开源版 GitHub](https://github.com/easysoft/zentaopms) - 禅道官方 GitHub 仓库
|
|
253
223
|
- [禅道官网](https://www.zentao.net/)
|
|
254
|
-
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,66 @@ import { ZentaoError, ErrorCode, createError } from './errors.js';
|
|
|
9
9
|
import { formatStoryAsMarkdown, formatBugAsMarkdown, formatTaskAsMarkdown, generateStorySummary, generateBugSummary } from './utils/formatter.js';
|
|
10
10
|
import { analyzeStoryComplexity, analyzeBugPriority, analyzeTaskWorkload } from './utils/analyzer.js';
|
|
11
11
|
import { suggestNextActionsForStory, suggestNextActionsForBug, suggestNextActionsForTask, formatSuggestionsAsMarkdown } from './utils/suggestions.js';
|
|
12
|
+
/**
|
|
13
|
+
* 解析自然语言时间表达式
|
|
14
|
+
* 支持:今年、今年1月、最近3个月、今天、昨天、上个月等
|
|
15
|
+
*/
|
|
16
|
+
function parseNaturalDate(dateStr) {
|
|
17
|
+
if (!dateStr) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
// 如果已经是标准格式(YYYY-MM-DD),直接返回
|
|
21
|
+
if (/^\d{4}-\d{2}-\d{2}/.test(dateStr)) {
|
|
22
|
+
return dateStr;
|
|
23
|
+
}
|
|
24
|
+
const now = new Date();
|
|
25
|
+
const year = now.getFullYear();
|
|
26
|
+
const month = now.getMonth(); // 0-11
|
|
27
|
+
const date = now.getDate();
|
|
28
|
+
const lowerStr = dateStr.toLowerCase().trim();
|
|
29
|
+
// 今年
|
|
30
|
+
if (lowerStr === '今年' || lowerStr === 'this year') {
|
|
31
|
+
return `${year}-01-01`;
|
|
32
|
+
}
|
|
33
|
+
// 今年X月
|
|
34
|
+
const monthMatch = lowerStr.match(/今年(\d+)月/);
|
|
35
|
+
if (monthMatch) {
|
|
36
|
+
const m = parseInt(monthMatch[1]);
|
|
37
|
+
if (m >= 1 && m <= 12) {
|
|
38
|
+
return `${year}-${String(m).padStart(2, '0')}-01`;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// 最近N个月
|
|
42
|
+
const monthsMatch = lowerStr.match(/最近(\d+)个月/);
|
|
43
|
+
if (monthsMatch) {
|
|
44
|
+
const months = parseInt(monthsMatch[1]);
|
|
45
|
+
const targetDate = new Date(year, month - months, date);
|
|
46
|
+
return targetDate.toISOString().split('T')[0];
|
|
47
|
+
}
|
|
48
|
+
// 最近N天
|
|
49
|
+
const daysMatch = lowerStr.match(/最近(\d+)天/);
|
|
50
|
+
if (daysMatch) {
|
|
51
|
+
const days = parseInt(daysMatch[1]);
|
|
52
|
+
const targetDate = new Date(year, month, date - days);
|
|
53
|
+
return targetDate.toISOString().split('T')[0];
|
|
54
|
+
}
|
|
55
|
+
// 今天
|
|
56
|
+
if (lowerStr === '今天' || lowerStr === 'today') {
|
|
57
|
+
return `${year}-${String(month + 1).padStart(2, '0')}-${String(date).padStart(2, '0')}`;
|
|
58
|
+
}
|
|
59
|
+
// 昨天
|
|
60
|
+
if (lowerStr === '昨天' || lowerStr === 'yesterday') {
|
|
61
|
+
const yesterday = new Date(year, month, date - 1);
|
|
62
|
+
return yesterday.toISOString().split('T')[0];
|
|
63
|
+
}
|
|
64
|
+
// 上个月
|
|
65
|
+
if (lowerStr === '上个月' || lowerStr === 'last month') {
|
|
66
|
+
const lastMonth = new Date(year, month - 1, 1);
|
|
67
|
+
return lastMonth.toISOString().split('T')[0];
|
|
68
|
+
}
|
|
69
|
+
// 如果无法解析,返回原字符串(让后续的日期解析函数处理)
|
|
70
|
+
return dateStr;
|
|
71
|
+
}
|
|
12
72
|
// Create an MCP server
|
|
13
73
|
const server = new McpServer({
|
|
14
74
|
name: "Zentao 11.3 Legacy API",
|
|
@@ -379,17 +439,26 @@ server.tool("getStoryDetail", {
|
|
|
379
439
|
});
|
|
380
440
|
// Add searchStories tool
|
|
381
441
|
server.tool("searchStories", {
|
|
382
|
-
keyword: z.string(),
|
|
383
|
-
productId: z.number().optional(),
|
|
384
|
-
status: z.enum(['draft', 'active', 'closed', 'changed', 'all']).optional(),
|
|
385
|
-
limit: z.number().optional().default(20)
|
|
386
|
-
|
|
442
|
+
keyword: z.string().describe("搜索关键字,支持中英文,会在需求标题和描述中搜索。例如:'大R促活'、'音视频'、'每日任务'等"),
|
|
443
|
+
productId: z.number().optional().describe("产品ID(可选),如果指定则只搜索该产品的需求"),
|
|
444
|
+
status: z.enum(['draft', 'active', 'closed', 'changed', 'all']).optional().describe("需求状态(可选):draft(草稿)、active(激活)、closed(已关闭)、changed(已变更)、all(全部)"),
|
|
445
|
+
limit: z.number().optional().default(20).describe("返回结果数量限制(默认20条)"),
|
|
446
|
+
deepSearch: z.boolean().optional().default(false).describe("是否启用深度搜索(获取需求详情以获取完整描述,速度较慢但结果更准确)"),
|
|
447
|
+
startDate: z.string().optional().describe("开始时间(可选),格式:YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss。用于过滤需求的创建时间,例如:'2024-01-01' 表示2024年1月1日之后的需求。支持自然语言理解:'今年'、'今年1月'、'最近3个月'等"),
|
|
448
|
+
endDate: z.string().optional().describe("结束时间(可选),格式:YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss。用于过滤需求的创建时间,例如:'2024-12-31' 表示2024年12月31日之前的需求。支持自然语言理解:'今年'、'今年12月'、'今天'等")
|
|
449
|
+
}, async ({ keyword, productId, status, limit = 20, deepSearch = false, startDate, endDate }) => {
|
|
387
450
|
await ensureInitialized();
|
|
388
451
|
try {
|
|
452
|
+
// 解析自然语言时间表达式(如果 AI 传递的是自然语言)
|
|
453
|
+
const parsedStartDate = startDate ? parseNaturalDate(startDate) : undefined;
|
|
454
|
+
const parsedEndDate = endDate ? parseNaturalDate(endDate) : undefined;
|
|
389
455
|
const stories = await zentaoApi.searchStories(keyword, {
|
|
390
456
|
productId,
|
|
391
457
|
status: status,
|
|
392
|
-
limit
|
|
458
|
+
limit,
|
|
459
|
+
deepSearch,
|
|
460
|
+
startDate: parsedStartDate || startDate,
|
|
461
|
+
endDate: parsedEndDate || endDate
|
|
393
462
|
});
|
|
394
463
|
return {
|
|
395
464
|
content: [{ type: "text", text: JSON.stringify(stories, null, 2) }]
|
|
@@ -90,12 +90,55 @@ export declare class ZentaoLegacyAPI {
|
|
|
90
90
|
/**
|
|
91
91
|
* 搜索需求(通过关键字)
|
|
92
92
|
* 由于禅道11.3的搜索API权限限制,我们通过获取所有产品的需求然后本地过滤
|
|
93
|
+
*
|
|
94
|
+
* 搜索范围:
|
|
95
|
+
* - 如果指定 productId:搜索该产品的所有需求(全量)
|
|
96
|
+
* - 如果未指定 productId:搜索所有产品的所有需求(全量)
|
|
97
|
+
*
|
|
98
|
+
* 优化:
|
|
99
|
+
* 1. 支持分词搜索(将关键字拆分为多个词进行匹配)
|
|
100
|
+
* 2. 增强匹配逻辑(标题、描述、模块名、产品名)
|
|
101
|
+
* 3. 智能排序(匹配度评分:标题完全匹配 > 标题包含 > 描述匹配 > 其他字段匹配)
|
|
102
|
+
* 4. 如果列表接口的spec不完整,对标题匹配的需求进行深度搜索(获取详情)
|
|
103
|
+
* 5. 支持时间范围过滤(按创建时间 openedDate)
|
|
93
104
|
*/
|
|
94
105
|
searchStories(keyword: string, options?: {
|
|
95
106
|
productId?: number;
|
|
96
107
|
status?: StoryStatus;
|
|
97
108
|
limit?: number;
|
|
109
|
+
deepSearch?: boolean;
|
|
110
|
+
startDate?: string;
|
|
111
|
+
endDate?: string;
|
|
98
112
|
}): Promise<Story[]>;
|
|
113
|
+
/**
|
|
114
|
+
* 分词:将关键字拆分为多个词
|
|
115
|
+
* 支持中英文混合,中文按字符拆分,英文按单词拆分
|
|
116
|
+
*/
|
|
117
|
+
private splitKeywords;
|
|
118
|
+
/**
|
|
119
|
+
* 按时间范围过滤需求
|
|
120
|
+
* @param stories 需求列表
|
|
121
|
+
* @param startDate 开始时间(可选,格式:YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss)
|
|
122
|
+
* @param endDate 结束时间(可选,格式:YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss)
|
|
123
|
+
* @returns 过滤后的需求列表
|
|
124
|
+
*/
|
|
125
|
+
private filterByDateRange;
|
|
126
|
+
/**
|
|
127
|
+
* 解析日期字符串
|
|
128
|
+
* 支持格式:YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss
|
|
129
|
+
*/
|
|
130
|
+
private parseDate;
|
|
131
|
+
/**
|
|
132
|
+
* 计算匹配度评分
|
|
133
|
+
* 评分规则:
|
|
134
|
+
* - 标题完全匹配:100分
|
|
135
|
+
* - 标题包含关键字:80分
|
|
136
|
+
* - 标题包含部分关键字(分词匹配):60分
|
|
137
|
+
* - 描述包含关键字:40分
|
|
138
|
+
* - 描述包含部分关键字:20分
|
|
139
|
+
* - 模块名/产品名匹配:10分
|
|
140
|
+
*/
|
|
141
|
+
private calculateMatchScore;
|
|
99
142
|
/**
|
|
100
143
|
* 按产品名称搜索需求
|
|
101
144
|
*/
|
package/dist/zentaoLegacyApi.js
CHANGED
|
@@ -450,9 +450,20 @@ export class ZentaoLegacyAPI {
|
|
|
450
450
|
/**
|
|
451
451
|
* 搜索需求(通过关键字)
|
|
452
452
|
* 由于禅道11.3的搜索API权限限制,我们通过获取所有产品的需求然后本地过滤
|
|
453
|
+
*
|
|
454
|
+
* 搜索范围:
|
|
455
|
+
* - 如果指定 productId:搜索该产品的所有需求(全量)
|
|
456
|
+
* - 如果未指定 productId:搜索所有产品的所有需求(全量)
|
|
457
|
+
*
|
|
458
|
+
* 优化:
|
|
459
|
+
* 1. 支持分词搜索(将关键字拆分为多个词进行匹配)
|
|
460
|
+
* 2. 增强匹配逻辑(标题、描述、模块名、产品名)
|
|
461
|
+
* 3. 智能排序(匹配度评分:标题完全匹配 > 标题包含 > 描述匹配 > 其他字段匹配)
|
|
462
|
+
* 4. 如果列表接口的spec不完整,对标题匹配的需求进行深度搜索(获取详情)
|
|
463
|
+
* 5. 支持时间范围过滤(按创建时间 openedDate)
|
|
453
464
|
*/
|
|
454
465
|
async searchStories(keyword, options) {
|
|
455
|
-
const { productId, status, limit = 50 } = options || {};
|
|
466
|
+
const { productId, status, limit = 50, deepSearch = false, startDate, endDate } = options || {};
|
|
456
467
|
try {
|
|
457
468
|
let allStories = [];
|
|
458
469
|
if (productId) {
|
|
@@ -460,47 +471,190 @@ export class ZentaoLegacyAPI {
|
|
|
460
471
|
allStories = await this.getProductStories(productId, status);
|
|
461
472
|
}
|
|
462
473
|
else {
|
|
463
|
-
//
|
|
474
|
+
// 搜索所有产品的需求(全量搜索)
|
|
464
475
|
const products = await this.getProducts();
|
|
465
|
-
//
|
|
466
|
-
const
|
|
467
|
-
for (const product of searchProducts) {
|
|
476
|
+
// 全量搜索:遍历所有产品
|
|
477
|
+
for (const product of products) {
|
|
468
478
|
try {
|
|
469
479
|
const stories = await this.getProductStories(product.id, status);
|
|
470
480
|
allStories.push(...stories);
|
|
471
481
|
}
|
|
472
482
|
catch (error) {
|
|
483
|
+
// 某个产品获取失败,继续处理其他产品
|
|
484
|
+
console.warn(`获取产品 ${product.id} (${product.name}) 的需求失败:`, error);
|
|
473
485
|
continue;
|
|
474
486
|
}
|
|
475
487
|
}
|
|
476
488
|
}
|
|
477
|
-
//
|
|
489
|
+
// 时间范围过滤(如果指定了时间范围,先过滤再搜索,提高性能)
|
|
490
|
+
if (startDate || endDate) {
|
|
491
|
+
allStories = this.filterByDateRange(allStories, startDate, endDate);
|
|
492
|
+
}
|
|
493
|
+
// 分词:将关键字拆分为多个词(支持中英文)
|
|
494
|
+
const keywords = this.splitKeywords(keyword);
|
|
478
495
|
const keyword_lower = keyword.toLowerCase();
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
return
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
496
|
+
// 计算匹配度评分
|
|
497
|
+
const scoredStories = allStories.map(story => {
|
|
498
|
+
const score = this.calculateMatchScore(story, keyword_lower, keywords);
|
|
499
|
+
return { story, score };
|
|
500
|
+
}).filter(item => item.score > 0); // 只保留有匹配的
|
|
501
|
+
// 如果启用深度搜索,对标题匹配但描述可能不完整的需求获取详情
|
|
502
|
+
if (deepSearch) {
|
|
503
|
+
const titleMatchedButLowScore = scoredStories
|
|
504
|
+
.filter(item => {
|
|
505
|
+
const titleMatch = item.story.title.toLowerCase().includes(keyword_lower);
|
|
506
|
+
const specMatch = item.story.spec && item.story.spec.toLowerCase().includes(keyword_lower);
|
|
507
|
+
return titleMatch && !specMatch && item.score < 50; // 标题匹配但描述不匹配,且评分较低
|
|
508
|
+
})
|
|
509
|
+
.slice(0, 10); // 最多深度搜索10个
|
|
510
|
+
for (const item of titleMatchedButLowScore) {
|
|
511
|
+
try {
|
|
512
|
+
const detail = await this.getStoryDetail(item.story.id);
|
|
513
|
+
// 使用完整描述重新计算评分
|
|
514
|
+
const newScore = this.calculateMatchScore(detail, keyword_lower, keywords);
|
|
515
|
+
if (newScore > item.score) {
|
|
516
|
+
item.story = detail;
|
|
517
|
+
item.score = newScore;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
catch (error) {
|
|
521
|
+
// 忽略获取详情失败的情况
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
// 按评分降序排序
|
|
527
|
+
scoredStories.sort((a, b) => {
|
|
528
|
+
if (b.score !== a.score) {
|
|
529
|
+
return b.score - a.score;
|
|
530
|
+
}
|
|
531
|
+
// 评分相同,按ID倒序(新的在前)
|
|
532
|
+
return b.story.id - a.story.id;
|
|
495
533
|
});
|
|
496
534
|
// 限制返回数量
|
|
497
|
-
return
|
|
535
|
+
return scoredStories.slice(0, limit).map(item => item.story);
|
|
498
536
|
}
|
|
499
537
|
catch (error) {
|
|
500
538
|
console.error('搜索需求失败:', error);
|
|
501
539
|
throw new Error(`搜索需求失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
502
540
|
}
|
|
503
541
|
}
|
|
542
|
+
/**
|
|
543
|
+
* 分词:将关键字拆分为多个词
|
|
544
|
+
* 支持中英文混合,中文按字符拆分,英文按单词拆分
|
|
545
|
+
*/
|
|
546
|
+
splitKeywords(keyword) {
|
|
547
|
+
const keywords = [];
|
|
548
|
+
const lowerKeyword = keyword.toLowerCase();
|
|
549
|
+
// 英文单词(字母、数字、连字符)
|
|
550
|
+
const englishWords = lowerKeyword.match(/[a-z0-9]+(?:-[a-z0-9]+)*/g) || [];
|
|
551
|
+
keywords.push(...englishWords);
|
|
552
|
+
// 中文字符(每个字符作为一个词)
|
|
553
|
+
const chineseChars = lowerKeyword.match(/[\u4e00-\u9fa5]/g) || [];
|
|
554
|
+
keywords.push(...chineseChars);
|
|
555
|
+
// 如果分词后没有结果,返回原始关键字
|
|
556
|
+
if (keywords.length === 0) {
|
|
557
|
+
keywords.push(lowerKeyword);
|
|
558
|
+
}
|
|
559
|
+
return keywords;
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* 按时间范围过滤需求
|
|
563
|
+
* @param stories 需求列表
|
|
564
|
+
* @param startDate 开始时间(可选,格式:YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss)
|
|
565
|
+
* @param endDate 结束时间(可选,格式:YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss)
|
|
566
|
+
* @returns 过滤后的需求列表
|
|
567
|
+
*/
|
|
568
|
+
filterByDateRange(stories, startDate, endDate) {
|
|
569
|
+
if (!startDate && !endDate) {
|
|
570
|
+
return stories;
|
|
571
|
+
}
|
|
572
|
+
const start = startDate ? this.parseDate(startDate) : null;
|
|
573
|
+
const end = endDate ? this.parseDate(endDate) : null;
|
|
574
|
+
return stories.filter(story => {
|
|
575
|
+
if (!story.openedDate) {
|
|
576
|
+
return false; // 没有创建时间的需求不包含在时间范围内
|
|
577
|
+
}
|
|
578
|
+
const storyDate = this.parseDate(story.openedDate);
|
|
579
|
+
if (!storyDate) {
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
// 检查是否在时间范围内
|
|
583
|
+
if (start && storyDate < start) {
|
|
584
|
+
return false;
|
|
585
|
+
}
|
|
586
|
+
if (end && storyDate > end) {
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
return true;
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* 解析日期字符串
|
|
594
|
+
* 支持格式:YYYY-MM-DD 或 YYYY-MM-DD HH:mm:ss
|
|
595
|
+
*/
|
|
596
|
+
parseDate(dateStr) {
|
|
597
|
+
if (!dateStr) {
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
// 尝试解析常见格式
|
|
601
|
+
// 格式1: YYYY-MM-DD
|
|
602
|
+
// 格式2: YYYY-MM-DD HH:mm:ss
|
|
603
|
+
// 格式3: YYYY-MM-DDTHH:mm:ss (ISO格式)
|
|
604
|
+
const date = new Date(dateStr);
|
|
605
|
+
if (isNaN(date.getTime())) {
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
return date;
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* 计算匹配度评分
|
|
612
|
+
* 评分规则:
|
|
613
|
+
* - 标题完全匹配:100分
|
|
614
|
+
* - 标题包含关键字:80分
|
|
615
|
+
* - 标题包含部分关键字(分词匹配):60分
|
|
616
|
+
* - 描述包含关键字:40分
|
|
617
|
+
* - 描述包含部分关键字:20分
|
|
618
|
+
* - 模块名/产品名匹配:10分
|
|
619
|
+
*/
|
|
620
|
+
calculateMatchScore(story, keyword, keywords) {
|
|
621
|
+
let score = 0;
|
|
622
|
+
const title_lower = story.title.toLowerCase();
|
|
623
|
+
const spec_lower = (story.spec || '').toLowerCase();
|
|
624
|
+
const moduleName_lower = (story.moduleName || '').toLowerCase();
|
|
625
|
+
const productName_lower = (story.productName || '').toLowerCase();
|
|
626
|
+
// 标题完全匹配(最高优先级)
|
|
627
|
+
if (title_lower === keyword) {
|
|
628
|
+
score += 100;
|
|
629
|
+
}
|
|
630
|
+
// 标题包含完整关键字
|
|
631
|
+
else if (title_lower.includes(keyword)) {
|
|
632
|
+
score += 80;
|
|
633
|
+
}
|
|
634
|
+
// 标题包含部分关键字(分词匹配)
|
|
635
|
+
else {
|
|
636
|
+
const titleKeywordMatches = keywords.filter(k => title_lower.includes(k)).length;
|
|
637
|
+
if (titleKeywordMatches > 0) {
|
|
638
|
+
score += 60 * (titleKeywordMatches / keywords.length); // 按匹配比例计算
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
// 描述包含完整关键字
|
|
642
|
+
if (spec_lower.includes(keyword)) {
|
|
643
|
+
score += 40;
|
|
644
|
+
}
|
|
645
|
+
// 描述包含部分关键字
|
|
646
|
+
else if (spec_lower) {
|
|
647
|
+
const specKeywordMatches = keywords.filter(k => spec_lower.includes(k)).length;
|
|
648
|
+
if (specKeywordMatches > 0) {
|
|
649
|
+
score += 20 * (specKeywordMatches / keywords.length);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
// 模块名/产品名匹配(加分项)
|
|
653
|
+
if (moduleName_lower.includes(keyword) || productName_lower.includes(keyword)) {
|
|
654
|
+
score += 10;
|
|
655
|
+
}
|
|
656
|
+
return score;
|
|
657
|
+
}
|
|
504
658
|
/**
|
|
505
659
|
* 按产品名称搜索需求
|
|
506
660
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maxenlin/mcp-zentao-11-3",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Zentao 11.3 legacy 版 MCP 服务器,只支持旧版 Session API,纯净无 REST v1",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -25,13 +25,14 @@
|
|
|
25
25
|
"11.3",
|
|
26
26
|
"legacy",
|
|
27
27
|
"session-api",
|
|
28
|
-
"
|
|
28
|
+
"mcp-server",
|
|
29
|
+
"model-context-protocol"
|
|
29
30
|
],
|
|
30
31
|
"author": "maxenlin",
|
|
31
32
|
"license": "MIT",
|
|
32
33
|
"repository": {
|
|
33
34
|
"type": "git",
|
|
34
|
-
"url": "https://github.com/
|
|
35
|
+
"url": "https://github.com/MaxenLin/mcp-zentao-11-3.git"
|
|
35
36
|
},
|
|
36
37
|
"dependencies": {
|
|
37
38
|
"@modelcontextprotocol/sdk": "^1.6.1",
|
|
@@ -49,4 +50,3 @@
|
|
|
49
50
|
"access": "public"
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
|
-
|