@hangox/pm-cli 1.1.6 → 1.2.0
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/dist/api.d.ts +462 -0
- package/dist/api.js +1218 -0
- package/dist/api.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +13 -2
package/dist/api.js
ADDED
|
@@ -0,0 +1,1218 @@
|
|
|
1
|
+
// src/utils/logger.ts
|
|
2
|
+
var currentLevel = "silent";
|
|
3
|
+
var levels = {
|
|
4
|
+
debug: 0,
|
|
5
|
+
info: 1,
|
|
6
|
+
warn: 2,
|
|
7
|
+
error: 3,
|
|
8
|
+
silent: 4
|
|
9
|
+
};
|
|
10
|
+
function shouldLog(level) {
|
|
11
|
+
return levels[level] >= levels[currentLevel];
|
|
12
|
+
}
|
|
13
|
+
function formatMessage(level, message, data) {
|
|
14
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
15
|
+
const dataStr = data ? ` ${JSON.stringify(data)}` : "";
|
|
16
|
+
return `[${timestamp}] [${level.toUpperCase()}] ${message}${dataStr}`;
|
|
17
|
+
}
|
|
18
|
+
var logger = {
|
|
19
|
+
setLevel(level) {
|
|
20
|
+
currentLevel = level;
|
|
21
|
+
},
|
|
22
|
+
getLevel() {
|
|
23
|
+
return currentLevel;
|
|
24
|
+
},
|
|
25
|
+
debug(message, data) {
|
|
26
|
+
if (shouldLog("debug")) {
|
|
27
|
+
console.error(formatMessage("debug", message, data));
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
info(message, data) {
|
|
31
|
+
if (shouldLog("info")) {
|
|
32
|
+
console.error(formatMessage("info", message, data));
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
warn(message, data) {
|
|
36
|
+
if (shouldLog("warn")) {
|
|
37
|
+
console.error(formatMessage("warn", message, data));
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
error(message, data) {
|
|
41
|
+
if (shouldLog("error")) {
|
|
42
|
+
console.error(formatMessage("error", message, data));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
function setLogLevel(level) {
|
|
47
|
+
currentLevel = level;
|
|
48
|
+
}
|
|
49
|
+
var logger_default = logger;
|
|
50
|
+
|
|
51
|
+
// src/client/api-client.ts
|
|
52
|
+
var ApiClient = class _ApiClient {
|
|
53
|
+
static BASE_URL = "http://redmineapi.nie.netease.com/api";
|
|
54
|
+
static DEFAULT_TIMEOUT = 3e4;
|
|
55
|
+
// 30秒
|
|
56
|
+
/**
|
|
57
|
+
* 发送 GET 请求
|
|
58
|
+
*/
|
|
59
|
+
async get(endpoint, params = {}) {
|
|
60
|
+
try {
|
|
61
|
+
logger_default.debug(`API GET \u8BF7\u6C42: ${endpoint}`, { params });
|
|
62
|
+
const queryString = this.buildQueryString(params);
|
|
63
|
+
const url = `${_ApiClient.BASE_URL}/${endpoint}${queryString ? `?${queryString}` : ""}`;
|
|
64
|
+
const controller = new AbortController();
|
|
65
|
+
const timeoutId = setTimeout(() => controller.abort(), _ApiClient.DEFAULT_TIMEOUT);
|
|
66
|
+
try {
|
|
67
|
+
const response = await fetch(url, {
|
|
68
|
+
method: "GET",
|
|
69
|
+
signal: controller.signal
|
|
70
|
+
});
|
|
71
|
+
clearTimeout(timeoutId);
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
logger_default.error(`HTTP \u8BF7\u6C42\u5931\u8D25: ${response.status} ${response.statusText}`);
|
|
74
|
+
return {
|
|
75
|
+
success: false,
|
|
76
|
+
message: `HTTP\u8BF7\u6C42\u5931\u8D25,\u72B6\u6001\u7801:${response.status}`
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
const data = await response.json();
|
|
80
|
+
logger_default.debug("API \u54CD\u5E94:", data);
|
|
81
|
+
return data;
|
|
82
|
+
} finally {
|
|
83
|
+
clearTimeout(timeoutId);
|
|
84
|
+
}
|
|
85
|
+
} catch (error) {
|
|
86
|
+
return this.handleError(error);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* 发送 POST 请求
|
|
91
|
+
*/
|
|
92
|
+
async post(endpoint, params = {}) {
|
|
93
|
+
try {
|
|
94
|
+
logger_default.debug(`API POST \u8BF7\u6C42: ${endpoint}`, { params });
|
|
95
|
+
const url = `${_ApiClient.BASE_URL}/${endpoint}`;
|
|
96
|
+
const formData = this.buildFormData(params);
|
|
97
|
+
const controller = new AbortController();
|
|
98
|
+
const timeoutId = setTimeout(() => controller.abort(), _ApiClient.DEFAULT_TIMEOUT);
|
|
99
|
+
try {
|
|
100
|
+
const response = await fetch(url, {
|
|
101
|
+
method: "POST",
|
|
102
|
+
headers: {
|
|
103
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
104
|
+
},
|
|
105
|
+
body: formData,
|
|
106
|
+
signal: controller.signal
|
|
107
|
+
});
|
|
108
|
+
clearTimeout(timeoutId);
|
|
109
|
+
if (!response.ok) {
|
|
110
|
+
logger_default.error(`HTTP \u8BF7\u6C42\u5931\u8D25: ${response.status} ${response.statusText}`);
|
|
111
|
+
return {
|
|
112
|
+
success: false,
|
|
113
|
+
message: `HTTP\u8BF7\u6C42\u5931\u8D25,\u72B6\u6001\u7801:${response.status}`
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const data = await response.json();
|
|
117
|
+
logger_default.debug("API \u54CD\u5E94:", data);
|
|
118
|
+
return data;
|
|
119
|
+
} finally {
|
|
120
|
+
clearTimeout(timeoutId);
|
|
121
|
+
}
|
|
122
|
+
} catch (error) {
|
|
123
|
+
return this.handleError(error);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* 处理错误
|
|
128
|
+
*/
|
|
129
|
+
handleError(error) {
|
|
130
|
+
if (error instanceof Error) {
|
|
131
|
+
if (error.name === "AbortError") {
|
|
132
|
+
logger_default.error("\u8BF7\u6C42\u8D85\u65F6");
|
|
133
|
+
return {
|
|
134
|
+
success: false,
|
|
135
|
+
message: "\u8BF7\u6C42\u8D85\u65F6"
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
logger_default.error("\u8BF7\u6C42\u5F02\u5E38:", error);
|
|
139
|
+
return {
|
|
140
|
+
success: false,
|
|
141
|
+
message: `\u8BF7\u6C42\u5F02\u5E38:${error.message}`
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
success: false,
|
|
146
|
+
message: "\u672A\u77E5\u9519\u8BEF"
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* 构建查询字符串
|
|
151
|
+
*/
|
|
152
|
+
buildQueryString(params) {
|
|
153
|
+
return Object.entries(params).filter(([, value]) => value !== null && value !== void 0).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`).join("&");
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* 构建表单数据
|
|
157
|
+
*/
|
|
158
|
+
buildFormData(params) {
|
|
159
|
+
return Object.entries(params).filter(([, value]) => value !== null && value !== void 0).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`).join("&");
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
var apiClient = new ApiClient();
|
|
163
|
+
|
|
164
|
+
// src/utils/markdown-parser.ts
|
|
165
|
+
function extractHours(text) {
|
|
166
|
+
const daysRegex = /[((][^))]*?([\d.]+)d[^))]*[))]/;
|
|
167
|
+
const daysMatch = text.match(daysRegex);
|
|
168
|
+
if (daysMatch) {
|
|
169
|
+
const value = parseFloat(daysMatch[1]);
|
|
170
|
+
return isNaN(value) ? void 0 : value;
|
|
171
|
+
}
|
|
172
|
+
const chineseDaysRegex = /[((]([\d.]+)天[))]/;
|
|
173
|
+
const chineseDaysMatch = text.match(chineseDaysRegex);
|
|
174
|
+
if (chineseDaysMatch) {
|
|
175
|
+
const value = parseFloat(chineseDaysMatch[1]);
|
|
176
|
+
return isNaN(value) ? void 0 : value;
|
|
177
|
+
}
|
|
178
|
+
const hoursRegex = /[((]([\d.]+)[))]/;
|
|
179
|
+
const hoursMatch = text.match(hoursRegex);
|
|
180
|
+
if (hoursMatch) {
|
|
181
|
+
const value = parseFloat(hoursMatch[1]);
|
|
182
|
+
return isNaN(value) ? void 0 : value;
|
|
183
|
+
}
|
|
184
|
+
return void 0;
|
|
185
|
+
}
|
|
186
|
+
function parseMarkdownLine(line) {
|
|
187
|
+
const leadingSpaces = line.match(/^(\s*)/)?.[1].length || 0;
|
|
188
|
+
const level = Math.floor(leadingSpaces / 2);
|
|
189
|
+
const cleanLine = line.trimStart().replace(/^[*-]\s*/, "").trim();
|
|
190
|
+
const estimatedHours = extractHours(cleanLine);
|
|
191
|
+
const subject = cleanLine.replace(/\s*[((][^))]*[))]/g, "").trim();
|
|
192
|
+
return { level, subject, estimatedHours };
|
|
193
|
+
}
|
|
194
|
+
function parseMarkdownToNodes(markdown) {
|
|
195
|
+
const lines = markdown.trim().split("\n").filter((line) => line.trim().length > 0).filter((line) => /^\s*[*-]/.test(line));
|
|
196
|
+
const rootNodes = [];
|
|
197
|
+
const nodeStack = [];
|
|
198
|
+
for (const line of lines) {
|
|
199
|
+
const { level, subject, estimatedHours } = parseMarkdownLine(line);
|
|
200
|
+
if (!subject) {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const node = {
|
|
204
|
+
subject,
|
|
205
|
+
estimatedHours,
|
|
206
|
+
children: []
|
|
207
|
+
};
|
|
208
|
+
while (nodeStack.length > 0 && nodeStack[nodeStack.length - 1].level >= level) {
|
|
209
|
+
nodeStack.pop();
|
|
210
|
+
}
|
|
211
|
+
if (nodeStack.length === 0) {
|
|
212
|
+
rootNodes.push(node);
|
|
213
|
+
} else {
|
|
214
|
+
nodeStack[nodeStack.length - 1].node.children.push(node);
|
|
215
|
+
}
|
|
216
|
+
nodeStack.push({ level, node });
|
|
217
|
+
}
|
|
218
|
+
return rootNodes;
|
|
219
|
+
}
|
|
220
|
+
function countTasks(nodes) {
|
|
221
|
+
let count = 0;
|
|
222
|
+
for (const node of nodes) {
|
|
223
|
+
count += 1;
|
|
224
|
+
count += countTasks(node.children);
|
|
225
|
+
}
|
|
226
|
+
return count;
|
|
227
|
+
}
|
|
228
|
+
function sumEstimatedHours(nodes) {
|
|
229
|
+
let total = 0;
|
|
230
|
+
for (const node of nodes) {
|
|
231
|
+
if (node.estimatedHours) {
|
|
232
|
+
total += node.estimatedHours;
|
|
233
|
+
}
|
|
234
|
+
total += sumEstimatedHours(node.children);
|
|
235
|
+
}
|
|
236
|
+
return total;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// src/services/issue-service.ts
|
|
240
|
+
function sleep(ms) {
|
|
241
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
242
|
+
}
|
|
243
|
+
var IssueService = class {
|
|
244
|
+
/**
|
|
245
|
+
* 获取问题详情
|
|
246
|
+
*/
|
|
247
|
+
async getIssue(token, host, project, issueId, includeChildren, includeRelations) {
|
|
248
|
+
const params = {
|
|
249
|
+
token,
|
|
250
|
+
host,
|
|
251
|
+
issue_id: issueId
|
|
252
|
+
};
|
|
253
|
+
if (project) params.project = project;
|
|
254
|
+
const includes = [];
|
|
255
|
+
if (includeChildren) includes.push("children");
|
|
256
|
+
if (includeRelations) includes.push("relations");
|
|
257
|
+
if (includes.length > 0) {
|
|
258
|
+
params.include = JSON.stringify(includes);
|
|
259
|
+
}
|
|
260
|
+
logger_default.info("\u83B7\u53D6\u95EE\u9898\u8BE6\u60C5", { host, project, issueId });
|
|
261
|
+
return await apiClient.get("issue", params);
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* 创建问题
|
|
265
|
+
*/
|
|
266
|
+
async createIssue(params) {
|
|
267
|
+
logger_default.info("\u521B\u5EFA\u95EE\u9898", { params });
|
|
268
|
+
return await apiClient.post("create_issue", params);
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* 更新问题
|
|
272
|
+
*/
|
|
273
|
+
async updateIssue(params) {
|
|
274
|
+
logger_default.info("\u66F4\u65B0\u95EE\u9898", { params });
|
|
275
|
+
return await apiClient.post("update_issue", params);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* 获取问题附件
|
|
279
|
+
*/
|
|
280
|
+
async getIssueAttachments(token, host, project, issueId) {
|
|
281
|
+
logger_default.info("\u83B7\u53D6\u95EE\u9898\u9644\u4EF6", { host, project, issueId });
|
|
282
|
+
return await apiClient.get("get_issue_attachments", {
|
|
283
|
+
token,
|
|
284
|
+
host,
|
|
285
|
+
project,
|
|
286
|
+
issue_id: issueId
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* 获取问题字段选项
|
|
291
|
+
*/
|
|
292
|
+
async getIssueFieldOptions(token, host, project) {
|
|
293
|
+
logger_default.info("\u83B7\u53D6\u95EE\u9898\u5B57\u6BB5\u9009\u9879", { host, project });
|
|
294
|
+
return await apiClient.post("get_issue_field_options", { token, host, project });
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* 自定义查询
|
|
298
|
+
*/
|
|
299
|
+
async customQuery(token, host, project, queryId, limit, offset) {
|
|
300
|
+
const params = {
|
|
301
|
+
token,
|
|
302
|
+
host,
|
|
303
|
+
project,
|
|
304
|
+
query_id: queryId
|
|
305
|
+
};
|
|
306
|
+
if (limit !== void 0) params.limit = limit;
|
|
307
|
+
if (offset !== void 0) params.offset = offset;
|
|
308
|
+
logger_default.info("\u81EA\u5B9A\u4E49\u67E5\u8BE2", { host, project, queryId });
|
|
309
|
+
return await apiClient.post("custom_query", params);
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* V6 过滤器查询
|
|
313
|
+
*/
|
|
314
|
+
async filterQueryV6(token, host, project, mode, filterParams) {
|
|
315
|
+
const params = {
|
|
316
|
+
token,
|
|
317
|
+
host,
|
|
318
|
+
project,
|
|
319
|
+
mode,
|
|
320
|
+
...filterParams
|
|
321
|
+
};
|
|
322
|
+
logger_default.info("V6 \u8FC7\u6EE4\u5668\u67E5\u8BE2", { host, project, mode });
|
|
323
|
+
return await apiClient.post("filter_query_v6", params);
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* 递归获取问题及其子单(包含完整详情)
|
|
327
|
+
* @param depth 递归深度,默认 10
|
|
328
|
+
* @param currentLevel 当前层级(内部使用)
|
|
329
|
+
*
|
|
330
|
+
* 实现逻辑:
|
|
331
|
+
* 1. 调用 getIssue(includeChildren=true) 获取当前问题详情
|
|
332
|
+
* 2. API 返回的 children 字段只包含简略信息(status 是字符串,缺少 assigned_to 等)
|
|
333
|
+
* 3. 从 children 中提取子单 ID,递归调用 getIssue 获取每个子单的完整详情
|
|
334
|
+
* 4. 用完整详情替换原始的简略 children
|
|
335
|
+
*/
|
|
336
|
+
async getIssueWithChildren(token, host, project, issueId, depth = 10, currentLevel2 = 0) {
|
|
337
|
+
logger_default.info("\u9012\u5F52\u83B7\u53D6\u95EE\u9898\u8BE6\u60C5", { host, project, issueId, depth, currentLevel: currentLevel2 });
|
|
338
|
+
const issueResult = await this.getIssue(token, host, project, issueId, true);
|
|
339
|
+
if (!issueResult.success || !issueResult.data) {
|
|
340
|
+
return issueResult;
|
|
341
|
+
}
|
|
342
|
+
const issue = issueResult.data;
|
|
343
|
+
issue.level = currentLevel2;
|
|
344
|
+
if (currentLevel2 >= depth) {
|
|
345
|
+
return { success: true, data: issue };
|
|
346
|
+
}
|
|
347
|
+
const rawChildren = issue.children;
|
|
348
|
+
if (!rawChildren || rawChildren.length === 0) {
|
|
349
|
+
return { success: true, data: issue };
|
|
350
|
+
}
|
|
351
|
+
const directChildrenIds = rawChildren.filter((child) => child.id).map((child) => child.id);
|
|
352
|
+
if (directChildrenIds.length === 0) {
|
|
353
|
+
return { success: true, data: issue };
|
|
354
|
+
}
|
|
355
|
+
logger_default.info("\u83B7\u53D6\u5B50\u5355\u5B8C\u6574\u8BE6\u60C5", { parentId: issueId, childCount: directChildrenIds.length, level: currentLevel2 });
|
|
356
|
+
const childrenPromises = directChildrenIds.map(
|
|
357
|
+
(childId) => this.getIssueWithChildren(token, host, project, childId, depth, currentLevel2 + 1)
|
|
358
|
+
);
|
|
359
|
+
const childrenResults = await Promise.all(childrenPromises);
|
|
360
|
+
issue.children = childrenResults.filter((r) => r.success && r.data).map((r) => r.data);
|
|
361
|
+
return { success: true, data: issue };
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* 获取直接子单的 ID 列表
|
|
365
|
+
*/
|
|
366
|
+
async getDirectChildren(token, host, project, parentId) {
|
|
367
|
+
const filters = {
|
|
368
|
+
parent_id: { operator: "=", values: [parentId.toString()] }
|
|
369
|
+
};
|
|
370
|
+
const params = {
|
|
371
|
+
token,
|
|
372
|
+
host,
|
|
373
|
+
project,
|
|
374
|
+
filter_mode: "simple",
|
|
375
|
+
filters: JSON.stringify(filters),
|
|
376
|
+
c: JSON.stringify(["id"]),
|
|
377
|
+
per_page: 200
|
|
378
|
+
};
|
|
379
|
+
logger_default.info("\u67E5\u8BE2\u76F4\u63A5\u5B50\u5355", { host, project, parentId });
|
|
380
|
+
const result = await apiClient.get("filter_query_v6", params);
|
|
381
|
+
if (result.success && result.data) {
|
|
382
|
+
const data = result.data;
|
|
383
|
+
if (data.data?.list) {
|
|
384
|
+
const ids = data.data.list.map((item) => item.id);
|
|
385
|
+
return { success: true, data: ids };
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return { success: true, data: [] };
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* 查询子任务(按根任务和负责人过滤)
|
|
392
|
+
* 使用两步查询:先过滤获取 ID 列表,再批量获取详情
|
|
393
|
+
*/
|
|
394
|
+
async queryChildren(token, host, project, rootId, assignedToId, perPage = 100) {
|
|
395
|
+
const filters = {
|
|
396
|
+
root_id: { operator: "=", values: [rootId.toString()] }
|
|
397
|
+
};
|
|
398
|
+
if (assignedToId) {
|
|
399
|
+
filters.assigned_to_id = { operator: "=", values: [assignedToId.toString()] };
|
|
400
|
+
}
|
|
401
|
+
const params = {
|
|
402
|
+
token,
|
|
403
|
+
host,
|
|
404
|
+
project,
|
|
405
|
+
filter_mode: "simple",
|
|
406
|
+
filters: JSON.stringify(filters),
|
|
407
|
+
c: JSON.stringify(["id", "subject", "status", "tracker", "estimated_hours", "done_ratio", "assigned_to", "parent"]),
|
|
408
|
+
per_page: perPage
|
|
409
|
+
};
|
|
410
|
+
logger_default.info("\u67E5\u8BE2\u5B50\u4EFB\u52A1", { host, project, rootId, assignedToId });
|
|
411
|
+
const result = await apiClient.get("filter_query_v6", params);
|
|
412
|
+
if (result.success && result.data) {
|
|
413
|
+
const data = result.data;
|
|
414
|
+
if (data.data?.list && data.data.list.length > 0) {
|
|
415
|
+
const issueIds = data.data.list.map((item) => item.id);
|
|
416
|
+
const detailedIssues = await Promise.all(
|
|
417
|
+
issueIds.map(async (id) => {
|
|
418
|
+
const issueResult = await this.getIssue(token, host, project, id);
|
|
419
|
+
if (issueResult.success && issueResult.data) {
|
|
420
|
+
return {
|
|
421
|
+
id: issueResult.data.id,
|
|
422
|
+
subject: issueResult.data.subject,
|
|
423
|
+
status: issueResult.data.status?.name,
|
|
424
|
+
tracker: issueResult.data.tracker?.name,
|
|
425
|
+
estimated_hours: issueResult.data.estimated_hours,
|
|
426
|
+
done_ratio: issueResult.data.done_ratio,
|
|
427
|
+
assigned_to: issueResult.data.assigned_to?.name,
|
|
428
|
+
parent_id: issueResult.data.parent_id
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
return null;
|
|
432
|
+
})
|
|
433
|
+
);
|
|
434
|
+
return {
|
|
435
|
+
success: true,
|
|
436
|
+
data: {
|
|
437
|
+
total: detailedIssues.filter(Boolean).length,
|
|
438
|
+
issues: detailedIssues.filter(Boolean)
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return result;
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* 批量获取多个问题详情
|
|
447
|
+
* @param issueIds 问题 ID 数组
|
|
448
|
+
* @param options 可选参数
|
|
449
|
+
* @returns 返回所有问题的详情数组(包含成功和失败的结果)
|
|
450
|
+
*/
|
|
451
|
+
async getMultipleIssues(token, host, project, issueIds, options) {
|
|
452
|
+
const { includeChildren = false, includeRelations = false, depth = 0 } = options || {};
|
|
453
|
+
logger_default.info("\u6279\u91CF\u83B7\u53D6\u95EE\u9898\u8BE6\u60C5", { host, project, count: issueIds.length, issueIds });
|
|
454
|
+
const results = await Promise.all(
|
|
455
|
+
issueIds.map(async (issueId) => {
|
|
456
|
+
try {
|
|
457
|
+
let result;
|
|
458
|
+
if (includeChildren && depth > 0) {
|
|
459
|
+
result = await this.getIssueWithChildren(token, host, project, issueId, depth);
|
|
460
|
+
} else {
|
|
461
|
+
result = await this.getIssue(token, host, project, issueId, includeChildren, includeRelations);
|
|
462
|
+
}
|
|
463
|
+
if (result.success && result.data) {
|
|
464
|
+
return { id: issueId, success: true, data: result.data };
|
|
465
|
+
} else {
|
|
466
|
+
return {
|
|
467
|
+
id: issueId,
|
|
468
|
+
success: false,
|
|
469
|
+
error: result.post_error_msg || result.api_error_msg || result.message || result.msg || "\u83B7\u53D6\u5931\u8D25"
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
} catch (error) {
|
|
473
|
+
return {
|
|
474
|
+
id: issueId,
|
|
475
|
+
success: false,
|
|
476
|
+
error: error instanceof Error ? error.message : "\u672A\u77E5\u9519\u8BEF"
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
})
|
|
480
|
+
);
|
|
481
|
+
const successCount = results.filter((r) => r.success).length;
|
|
482
|
+
const failCount = results.filter((r) => !r.success).length;
|
|
483
|
+
logger_default.info("\u6279\u91CF\u83B7\u53D6\u5B8C\u6210", { total: issueIds.length, success: successCount, fail: failCount });
|
|
484
|
+
return {
|
|
485
|
+
success: true,
|
|
486
|
+
data: results
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* 同步子单:从源父单复制子单到目标父单
|
|
491
|
+
* @param sourceParentId 源父单 ID
|
|
492
|
+
* @param targetParentId 目标父单 ID
|
|
493
|
+
* @param assignedToMail 新子单的指派人邮箱
|
|
494
|
+
* @param options 选项
|
|
495
|
+
*/
|
|
496
|
+
async syncChildIssues(token, host, project, sourceParentId, targetParentId, assignedToMail, options) {
|
|
497
|
+
const { dryRun = false, depth = 10, skipExisting = true } = options || {};
|
|
498
|
+
logger_default.info("\u5F00\u59CB\u540C\u6B65\u5B50\u5355", {
|
|
499
|
+
sourceParentId,
|
|
500
|
+
targetParentId,
|
|
501
|
+
assignedToMail,
|
|
502
|
+
dryRun,
|
|
503
|
+
depth,
|
|
504
|
+
skipExisting
|
|
505
|
+
});
|
|
506
|
+
const result = {
|
|
507
|
+
totalTasks: 0,
|
|
508
|
+
totalCreated: 0,
|
|
509
|
+
totalSkipped: 0,
|
|
510
|
+
totalFailed: 0,
|
|
511
|
+
created: [],
|
|
512
|
+
skipped: [],
|
|
513
|
+
failed: []
|
|
514
|
+
};
|
|
515
|
+
try {
|
|
516
|
+
logger_default.info("\u6B63\u5728\u83B7\u53D6\u6E90\u7236\u5355\u7684\u5B50\u5355\u6811\u5F62\u7ED3\u6784...");
|
|
517
|
+
const sourceResult = await this.getIssueWithChildren(token, host, project, sourceParentId, depth);
|
|
518
|
+
if (!sourceResult.success || !sourceResult.data) {
|
|
519
|
+
return {
|
|
520
|
+
success: false,
|
|
521
|
+
message: `\u83B7\u53D6\u6E90\u7236\u5355 #${sourceParentId} \u5931\u8D25: ${sourceResult.message || "\u672A\u77E5\u9519\u8BEF"}`
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
const sourceIssue = sourceResult.data;
|
|
525
|
+
result.totalTasks = this.countAllChildren(sourceIssue);
|
|
526
|
+
logger_default.info(`\u6E90\u7236\u5355\u5171\u6709 ${result.totalTasks} \u4E2A\u5B50\u4EFB\u52A1`);
|
|
527
|
+
logger_default.info("\u6B63\u5728\u83B7\u53D6\u76EE\u6807\u7236\u5355\u4FE1\u606F...");
|
|
528
|
+
const targetResult = await this.getIssue(token, host, project, targetParentId, false, false);
|
|
529
|
+
if (!targetResult.success || !targetResult.data) {
|
|
530
|
+
return {
|
|
531
|
+
success: false,
|
|
532
|
+
message: `\u83B7\u53D6\u76EE\u6807\u7236\u5355 #${targetParentId} \u5931\u8D25: ${targetResult.message || "\u672A\u77E5\u9519\u8BEF"}`
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
const targetParentInfo = targetResult.data;
|
|
536
|
+
let existingTaskNames = /* @__PURE__ */ new Set();
|
|
537
|
+
if (skipExisting) {
|
|
538
|
+
logger_default.info("\u6B63\u5728\u83B7\u53D6\u76EE\u6807\u7236\u5355\u5DF2\u6709\u7684\u5B50\u5355...");
|
|
539
|
+
const existingResult = await this.getIssueWithChildren(token, host, project, targetParentId, depth);
|
|
540
|
+
if (existingResult.success && existingResult.data) {
|
|
541
|
+
existingTaskNames = this.collectAllTaskNames(existingResult.data);
|
|
542
|
+
logger_default.info(`\u76EE\u6807\u7236\u5355\u5DF2\u6709 ${existingTaskNames.size} \u4E2A\u5B50\u5355`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
logger_default.info("\u5F00\u59CB\u9012\u5F52\u540C\u6B65\u5B50\u5355...");
|
|
546
|
+
await this.syncChildrenRecursive(
|
|
547
|
+
token,
|
|
548
|
+
host,
|
|
549
|
+
project,
|
|
550
|
+
sourceIssue,
|
|
551
|
+
targetParentId,
|
|
552
|
+
targetParentInfo,
|
|
553
|
+
existingTaskNames,
|
|
554
|
+
assignedToMail,
|
|
555
|
+
dryRun,
|
|
556
|
+
result
|
|
557
|
+
);
|
|
558
|
+
logger_default.info("\u540C\u6B65\u5B8C\u6210", {
|
|
559
|
+
totalCreated: result.totalCreated,
|
|
560
|
+
totalSkipped: result.totalSkipped,
|
|
561
|
+
totalFailed: result.totalFailed
|
|
562
|
+
});
|
|
563
|
+
return { success: true, data: result };
|
|
564
|
+
} catch (error) {
|
|
565
|
+
logger_default.error("\u540C\u6B65\u5B50\u5355\u65F6\u53D1\u751F\u9519\u8BEF", error);
|
|
566
|
+
return {
|
|
567
|
+
success: false,
|
|
568
|
+
message: `\u540C\u6B65\u5B50\u5355\u65F6\u53D1\u751F\u9519\u8BEF: ${error instanceof Error ? error.message : "\u672A\u77E5\u9519\u8BEF"}`
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* 计算所有子任务数量(递归)
|
|
574
|
+
*/
|
|
575
|
+
countAllChildren(issue) {
|
|
576
|
+
let count = 0;
|
|
577
|
+
for (const child of issue.children || []) {
|
|
578
|
+
count++;
|
|
579
|
+
count += this.countAllChildren(child);
|
|
580
|
+
}
|
|
581
|
+
return count;
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* 收集所有任务名称(递归)
|
|
585
|
+
*/
|
|
586
|
+
collectAllTaskNames(issue) {
|
|
587
|
+
const names = /* @__PURE__ */ new Set();
|
|
588
|
+
if (issue.subject?.trim()) {
|
|
589
|
+
names.add(issue.subject.trim());
|
|
590
|
+
}
|
|
591
|
+
for (const child of issue.children || []) {
|
|
592
|
+
const childNames = this.collectAllTaskNames(child);
|
|
593
|
+
childNames.forEach((name) => names.add(name));
|
|
594
|
+
}
|
|
595
|
+
return names;
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* 递归同步子单
|
|
599
|
+
*/
|
|
600
|
+
async syncChildrenRecursive(token, host, project, sourceNode, targetParentId, targetParentInfo, existingTaskNames, assignedToMail, dryRun, result) {
|
|
601
|
+
for (const child of sourceNode.children || []) {
|
|
602
|
+
const taskName = child.subject?.trim() || "\u672A\u547D\u540D\u4EFB\u52A1";
|
|
603
|
+
const sourceId = child.id;
|
|
604
|
+
if (existingTaskNames.has(taskName)) {
|
|
605
|
+
logger_default.info(`\u23ED\uFE0F \u8DF3\u8FC7\u5DF2\u5B58\u5728\u7684\u4EFB\u52A1: ${taskName}`);
|
|
606
|
+
result.totalSkipped++;
|
|
607
|
+
result.skipped.push({
|
|
608
|
+
sourceId,
|
|
609
|
+
subject: taskName,
|
|
610
|
+
reason: "\u76EE\u6807\u7236\u5355\u4E0B\u5DF2\u5B58\u5728\u540C\u540D\u4EFB\u52A1"
|
|
611
|
+
});
|
|
612
|
+
const progress = result.totalCreated + result.totalSkipped + result.totalFailed;
|
|
613
|
+
console.error(`[${progress}/${result.totalTasks}] \u23ED\uFE0F \u8DF3\u8FC7: #${sourceId} ${taskName} (\u5DF2\u5B58\u5728)`);
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
const isLeafNode = !child.children || child.children.length === 0;
|
|
617
|
+
const createParams = {
|
|
618
|
+
token,
|
|
619
|
+
host,
|
|
620
|
+
project,
|
|
621
|
+
parent_issue_id: targetParentId,
|
|
622
|
+
subject: taskName,
|
|
623
|
+
// 继承自目标父单
|
|
624
|
+
tracker: targetParentInfo.tracker?.name,
|
|
625
|
+
status: "\u65B0\u5EFA",
|
|
626
|
+
// 覆盖指派人为"我"
|
|
627
|
+
assigned_to_mail: assignedToMail,
|
|
628
|
+
// 只有叶子节点才设置工时
|
|
629
|
+
estimated_hours: isLeafNode ? child.estimated_hours : void 0,
|
|
630
|
+
// 保留原任务的优先级
|
|
631
|
+
priority_id: child.priority?.id
|
|
632
|
+
};
|
|
633
|
+
const targetWithVersion = targetParentInfo;
|
|
634
|
+
if (targetWithVersion.fixed_version?.name) {
|
|
635
|
+
createParams.version = targetWithVersion.fixed_version.name;
|
|
636
|
+
}
|
|
637
|
+
const targetWithCustomFields = targetParentInfo;
|
|
638
|
+
if (targetWithCustomFields.custom_fields && targetWithCustomFields.custom_fields.length > 0) {
|
|
639
|
+
const customFieldMap = {};
|
|
640
|
+
const followsMails = [];
|
|
641
|
+
for (const field of targetWithCustomFields.custom_fields) {
|
|
642
|
+
if (field.value !== null && field.value !== void 0 && field.value !== "") {
|
|
643
|
+
if (field.identify === "IssuesQCFollow") {
|
|
644
|
+
const followsValue = field.value;
|
|
645
|
+
if (Array.isArray(followsValue)) {
|
|
646
|
+
for (const item of followsValue) {
|
|
647
|
+
if (item.user?.mail) {
|
|
648
|
+
followsMails.push(item.user.mail);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
} else if (field.field_format === "user") {
|
|
653
|
+
if (field.multiple && Array.isArray(field.value)) {
|
|
654
|
+
const userIds = [];
|
|
655
|
+
for (const item of field.value) {
|
|
656
|
+
if (item.user?.id) {
|
|
657
|
+
userIds.push(item.user.id);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
if (userIds.length > 0) {
|
|
661
|
+
customFieldMap[field.id] = userIds.join(",");
|
|
662
|
+
}
|
|
663
|
+
} else {
|
|
664
|
+
const userValue = field.value;
|
|
665
|
+
if (userValue.user?.id) {
|
|
666
|
+
customFieldMap[field.id] = userValue.user.id;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
} else {
|
|
670
|
+
customFieldMap[field.id] = field.value;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
if (Object.keys(customFieldMap).length > 0) {
|
|
675
|
+
createParams.custom_field = JSON.stringify(customFieldMap);
|
|
676
|
+
}
|
|
677
|
+
if (followsMails.length > 0) {
|
|
678
|
+
createParams.follows = followsMails;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
logger_default.info(`\u6B63\u5728\u521B\u5EFA\u5B50\u4EFB\u52A1: ${taskName}`, { sourceId, targetParentId, isLeafNode });
|
|
682
|
+
if (dryRun) {
|
|
683
|
+
logger_default.info(`[\u6A21\u62DF] \u5C06\u521B\u5EFA\u5B50\u4EFB\u52A1: ${taskName}`);
|
|
684
|
+
result.totalCreated++;
|
|
685
|
+
result.created.push({
|
|
686
|
+
sourceId,
|
|
687
|
+
newId: 0,
|
|
688
|
+
// 模拟模式没有真实 ID
|
|
689
|
+
subject: taskName,
|
|
690
|
+
parentId: targetParentId
|
|
691
|
+
});
|
|
692
|
+
if (child.children && child.children.length > 0) {
|
|
693
|
+
await this.syncChildrenRecursive(
|
|
694
|
+
token,
|
|
695
|
+
host,
|
|
696
|
+
project,
|
|
697
|
+
child,
|
|
698
|
+
0,
|
|
699
|
+
// 模拟模式下没有真实的新 ID
|
|
700
|
+
targetParentInfo,
|
|
701
|
+
existingTaskNames,
|
|
702
|
+
assignedToMail,
|
|
703
|
+
dryRun,
|
|
704
|
+
result
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
} else {
|
|
708
|
+
try {
|
|
709
|
+
const createResult = await this.createIssue(createParams);
|
|
710
|
+
if (createResult.success && createResult.data) {
|
|
711
|
+
const newId = createResult.data.id;
|
|
712
|
+
logger_default.info(`\u2705 \u6210\u529F\u521B\u5EFA\u5B50\u4EFB\u52A1: ID=${newId}, \u6807\u9898=${taskName}`);
|
|
713
|
+
result.totalCreated++;
|
|
714
|
+
result.created.push({
|
|
715
|
+
sourceId,
|
|
716
|
+
newId,
|
|
717
|
+
subject: taskName,
|
|
718
|
+
parentId: targetParentId
|
|
719
|
+
});
|
|
720
|
+
const progress = result.totalCreated + result.totalSkipped + result.totalFailed;
|
|
721
|
+
console.error(`[${progress}/${result.totalTasks}] \u2705 \u540C\u6B65\u6210\u529F: #${newId} \u2190 #${sourceId} ${taskName}`);
|
|
722
|
+
if (child.children && child.children.length > 0) {
|
|
723
|
+
logger_default.info(`\u{1F4C1} \u53D1\u73B0\u5B50\u4EFB\u52A1 ${newId} \u6709 ${child.children.length} \u4E2A\u5B50\u4EFB\u52A1\uFF0C\u7EE7\u7EED\u9012\u5F52\u521B\u5EFA...`);
|
|
724
|
+
await this.syncChildrenRecursive(
|
|
725
|
+
token,
|
|
726
|
+
host,
|
|
727
|
+
project,
|
|
728
|
+
child,
|
|
729
|
+
newId,
|
|
730
|
+
targetParentInfo,
|
|
731
|
+
existingTaskNames,
|
|
732
|
+
assignedToMail,
|
|
733
|
+
dryRun,
|
|
734
|
+
result
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
} else {
|
|
738
|
+
const errorMsg = createResult.post_error_msg || createResult.api_error_msg || createResult.message || "\u521B\u5EFA\u5931\u8D25";
|
|
739
|
+
logger_default.error(`\u274C \u521B\u5EFA\u5B50\u4EFB\u52A1\u5931\u8D25: ${taskName}`, errorMsg);
|
|
740
|
+
result.totalFailed++;
|
|
741
|
+
result.failed.push({
|
|
742
|
+
sourceId,
|
|
743
|
+
subject: taskName,
|
|
744
|
+
error: errorMsg
|
|
745
|
+
});
|
|
746
|
+
const progress = result.totalCreated + result.totalSkipped + result.totalFailed;
|
|
747
|
+
console.error(`[${progress}/${result.totalTasks}] \u274C \u540C\u6B65\u5931\u8D25: #${sourceId} ${taskName} - ${errorMsg}`);
|
|
748
|
+
}
|
|
749
|
+
} catch (error) {
|
|
750
|
+
const errorMsg = error instanceof Error ? error.message : "\u672A\u77E5\u9519\u8BEF";
|
|
751
|
+
logger_default.error(`\u274C \u521B\u5EFA\u5B50\u4EFB\u52A1\u65F6\u53D1\u751F\u9519\u8BEF: ${taskName}`, error);
|
|
752
|
+
result.totalFailed++;
|
|
753
|
+
result.failed.push({
|
|
754
|
+
sourceId,
|
|
755
|
+
subject: taskName,
|
|
756
|
+
error: errorMsg
|
|
757
|
+
});
|
|
758
|
+
const progress = result.totalCreated + result.totalSkipped + result.totalFailed;
|
|
759
|
+
console.error(`[${progress}/${result.totalTasks}] \u274C \u540C\u6B65\u5931\u8D25: #${sourceId} ${taskName} - ${errorMsg}`);
|
|
760
|
+
}
|
|
761
|
+
await sleep(500);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* 从 Markdown 批量创建子单
|
|
767
|
+
* @param parentId 父单 ID
|
|
768
|
+
* @param markdown Markdown 文本
|
|
769
|
+
* @param assignedToMail 指派人邮箱
|
|
770
|
+
* @param options 选项
|
|
771
|
+
*/
|
|
772
|
+
async batchCreateFromMarkdown(token, host, project, parentId, markdown, assignedToMail, options) {
|
|
773
|
+
const { dryRun = false, interval = 5e3, extraCustomFields = {} } = options || {};
|
|
774
|
+
logger_default.info("\u5F00\u59CB\u6279\u91CF\u521B\u5EFA\u5B50\u5355", {
|
|
775
|
+
parentId,
|
|
776
|
+
assignedToMail,
|
|
777
|
+
dryRun,
|
|
778
|
+
interval
|
|
779
|
+
});
|
|
780
|
+
const result = {
|
|
781
|
+
totalCreated: 0,
|
|
782
|
+
totalFailed: 0,
|
|
783
|
+
totalTasks: 0,
|
|
784
|
+
totalEstimatedHours: 0,
|
|
785
|
+
created: [],
|
|
786
|
+
failed: []
|
|
787
|
+
};
|
|
788
|
+
try {
|
|
789
|
+
const taskNodes = parseMarkdownToNodes(markdown);
|
|
790
|
+
if (taskNodes.length === 0) {
|
|
791
|
+
return {
|
|
792
|
+
success: false,
|
|
793
|
+
message: "\u6CA1\u6709\u89E3\u6790\u5230\u4EFB\u4F55\u4EFB\u52A1\uFF0C\u8BF7\u68C0\u67E5 Markdown \u683C\u5F0F"
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
result.totalTasks = countTasks(taskNodes);
|
|
797
|
+
result.totalEstimatedHours = sumEstimatedHours(taskNodes);
|
|
798
|
+
logger_default.info("Markdown \u89E3\u6790\u5B8C\u6210", {
|
|
799
|
+
totalTasks: result.totalTasks,
|
|
800
|
+
totalEstimatedHours: result.totalEstimatedHours
|
|
801
|
+
});
|
|
802
|
+
logger_default.info("\u6B63\u5728\u83B7\u53D6\u7236\u5355\u4FE1\u606F...");
|
|
803
|
+
const parentResult = await this.getIssue(token, host, project, parentId, false, false);
|
|
804
|
+
if (!parentResult.success || !parentResult.data) {
|
|
805
|
+
return {
|
|
806
|
+
success: false,
|
|
807
|
+
message: `\u83B7\u53D6\u7236\u5355 #${parentId} \u5931\u8D25: ${parentResult.message || "\u672A\u77E5\u9519\u8BEF"}`
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
const parentInfo = parentResult.data;
|
|
811
|
+
logger_default.info("\u5F00\u59CB\u9012\u5F52\u521B\u5EFA\u5B50\u5355...");
|
|
812
|
+
await this.batchCreateRecursive(
|
|
813
|
+
token,
|
|
814
|
+
host,
|
|
815
|
+
project,
|
|
816
|
+
taskNodes,
|
|
817
|
+
parentId,
|
|
818
|
+
parentInfo,
|
|
819
|
+
assignedToMail,
|
|
820
|
+
dryRun,
|
|
821
|
+
interval,
|
|
822
|
+
result,
|
|
823
|
+
extraCustomFields
|
|
824
|
+
);
|
|
825
|
+
logger_default.info("\u6279\u91CF\u521B\u5EFA\u5B8C\u6210", {
|
|
826
|
+
totalCreated: result.totalCreated,
|
|
827
|
+
totalFailed: result.totalFailed
|
|
828
|
+
});
|
|
829
|
+
return { success: true, data: result };
|
|
830
|
+
} catch (error) {
|
|
831
|
+
logger_default.error("\u6279\u91CF\u521B\u5EFA\u5B50\u5355\u65F6\u53D1\u751F\u9519\u8BEF", error);
|
|
832
|
+
return {
|
|
833
|
+
success: false,
|
|
834
|
+
message: `\u6279\u91CF\u521B\u5EFA\u5B50\u5355\u65F6\u53D1\u751F\u9519\u8BEF: ${error instanceof Error ? error.message : "\u672A\u77E5\u9519\u8BEF"}`
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* 递归批量创建子单
|
|
840
|
+
*/
|
|
841
|
+
async batchCreateRecursive(token, host, project, taskNodes, targetParentId, parentInfo, assignedToMail, dryRun, interval, result, extraCustomFields = {}) {
|
|
842
|
+
for (const node of taskNodes) {
|
|
843
|
+
const taskName = node.subject;
|
|
844
|
+
const isLeafNode = node.children.length === 0;
|
|
845
|
+
const createParams = {
|
|
846
|
+
token,
|
|
847
|
+
host,
|
|
848
|
+
project,
|
|
849
|
+
parent_issue_id: targetParentId,
|
|
850
|
+
subject: taskName,
|
|
851
|
+
// 继承自父单
|
|
852
|
+
tracker: parentInfo.tracker?.name,
|
|
853
|
+
status: "\u65B0\u5EFA",
|
|
854
|
+
// 指派人
|
|
855
|
+
assigned_to_mail: assignedToMail,
|
|
856
|
+
// 只有叶子节点才设置工时
|
|
857
|
+
estimated_hours: isLeafNode ? node.estimatedHours : void 0
|
|
858
|
+
};
|
|
859
|
+
const parentWithVersion = parentInfo;
|
|
860
|
+
if (parentWithVersion.fixed_version?.name) {
|
|
861
|
+
createParams.version = parentWithVersion.fixed_version.name;
|
|
862
|
+
}
|
|
863
|
+
const parentWithCustomFields = parentInfo;
|
|
864
|
+
if (parentWithCustomFields.custom_fields && parentWithCustomFields.custom_fields.length > 0) {
|
|
865
|
+
const customFieldMap = {};
|
|
866
|
+
const followsMails = [];
|
|
867
|
+
for (const field of parentWithCustomFields.custom_fields) {
|
|
868
|
+
if (field.value !== null && field.value !== void 0 && field.value !== "") {
|
|
869
|
+
if (field.identify === "IssuesQCFollow") {
|
|
870
|
+
const followsValue = field.value;
|
|
871
|
+
if (Array.isArray(followsValue)) {
|
|
872
|
+
for (const item of followsValue) {
|
|
873
|
+
if (item.user?.mail) {
|
|
874
|
+
followsMails.push(item.user.mail);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
} else if (field.field_format === "user") {
|
|
879
|
+
if (field.multiple && Array.isArray(field.value)) {
|
|
880
|
+
const userIds = [];
|
|
881
|
+
for (const item of field.value) {
|
|
882
|
+
if (item.user?.id) {
|
|
883
|
+
userIds.push(item.user.id);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
if (userIds.length > 0) {
|
|
887
|
+
customFieldMap[field.id] = userIds.join(",");
|
|
888
|
+
}
|
|
889
|
+
} else {
|
|
890
|
+
const userValue = field.value;
|
|
891
|
+
if (userValue.user?.id) {
|
|
892
|
+
customFieldMap[field.id] = userValue.user.id;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
} else {
|
|
896
|
+
customFieldMap[field.id] = field.value;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
const mergedCustomFields = { ...customFieldMap, ...extraCustomFields };
|
|
901
|
+
if (Object.keys(mergedCustomFields).length > 0) {
|
|
902
|
+
createParams.custom_field = JSON.stringify(mergedCustomFields);
|
|
903
|
+
}
|
|
904
|
+
if (followsMails.length > 0) {
|
|
905
|
+
createParams.follows = followsMails;
|
|
906
|
+
}
|
|
907
|
+
} else if (Object.keys(extraCustomFields).length > 0) {
|
|
908
|
+
createParams.custom_field = JSON.stringify(extraCustomFields);
|
|
909
|
+
}
|
|
910
|
+
logger_default.info(`\u6B63\u5728\u521B\u5EFA\u5B50\u4EFB\u52A1: ${taskName}`, { targetParentId, isLeafNode, estimatedHours: node.estimatedHours });
|
|
911
|
+
logger_default.debug("\u521B\u5EFA\u53C2\u6570", { custom_field: createParams.custom_field, follows: createParams.follows });
|
|
912
|
+
if (dryRun) {
|
|
913
|
+
logger_default.info(`[\u6A21\u62DF] \u5C06\u521B\u5EFA\u5B50\u4EFB\u52A1: ${taskName}`);
|
|
914
|
+
result.totalCreated++;
|
|
915
|
+
result.created.push({
|
|
916
|
+
newId: 0,
|
|
917
|
+
// 模拟模式没有真实 ID
|
|
918
|
+
subject: taskName,
|
|
919
|
+
parentId: targetParentId,
|
|
920
|
+
estimatedHours: node.estimatedHours
|
|
921
|
+
});
|
|
922
|
+
if (node.children.length > 0) {
|
|
923
|
+
await this.batchCreateRecursive(
|
|
924
|
+
token,
|
|
925
|
+
host,
|
|
926
|
+
project,
|
|
927
|
+
node.children,
|
|
928
|
+
0,
|
|
929
|
+
// 模拟模式下没有真实的新 ID
|
|
930
|
+
parentInfo,
|
|
931
|
+
assignedToMail,
|
|
932
|
+
dryRun,
|
|
933
|
+
interval,
|
|
934
|
+
result,
|
|
935
|
+
extraCustomFields
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
} else {
|
|
939
|
+
try {
|
|
940
|
+
const createResult = await this.createIssue(createParams);
|
|
941
|
+
if (createResult.success && createResult.data) {
|
|
942
|
+
const newId = createResult.data.id;
|
|
943
|
+
logger_default.info(`\u2705 \u6210\u529F\u521B\u5EFA\u5B50\u4EFB\u52A1: ID=${newId}, \u6807\u9898=${taskName}`);
|
|
944
|
+
result.totalCreated++;
|
|
945
|
+
result.created.push({
|
|
946
|
+
newId,
|
|
947
|
+
subject: taskName,
|
|
948
|
+
parentId: targetParentId,
|
|
949
|
+
estimatedHours: node.estimatedHours
|
|
950
|
+
});
|
|
951
|
+
const progress = result.totalCreated + result.totalFailed;
|
|
952
|
+
console.error(`[${progress}/${result.totalTasks}] \u2705 \u521B\u5EFA\u6210\u529F: #${newId} ${taskName}`);
|
|
953
|
+
if (node.children.length > 0) {
|
|
954
|
+
logger_default.info(`\u{1F4C1} \u53D1\u73B0\u5B50\u4EFB\u52A1 ${newId} \u6709 ${node.children.length} \u4E2A\u5B50\u4EFB\u52A1\uFF0C\u7EE7\u7EED\u9012\u5F52\u521B\u5EFA...`);
|
|
955
|
+
await this.batchCreateRecursive(
|
|
956
|
+
token,
|
|
957
|
+
host,
|
|
958
|
+
project,
|
|
959
|
+
node.children,
|
|
960
|
+
newId,
|
|
961
|
+
parentInfo,
|
|
962
|
+
assignedToMail,
|
|
963
|
+
dryRun,
|
|
964
|
+
interval,
|
|
965
|
+
result,
|
|
966
|
+
extraCustomFields
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
} else {
|
|
970
|
+
const errorMsg = createResult.post_error_msg || createResult.api_error_msg || createResult.message || "\u521B\u5EFA\u5931\u8D25";
|
|
971
|
+
logger_default.error(`\u274C \u521B\u5EFA\u5B50\u4EFB\u52A1\u5931\u8D25: ${taskName}`, errorMsg);
|
|
972
|
+
result.totalFailed++;
|
|
973
|
+
result.failed.push({
|
|
974
|
+
subject: taskName,
|
|
975
|
+
parentId: targetParentId,
|
|
976
|
+
error: errorMsg
|
|
977
|
+
});
|
|
978
|
+
const progress = result.totalCreated + result.totalFailed;
|
|
979
|
+
console.error(`[${progress}/${result.totalTasks}] \u274C \u521B\u5EFA\u5931\u8D25: ${taskName} - ${errorMsg}`);
|
|
980
|
+
}
|
|
981
|
+
} catch (error) {
|
|
982
|
+
const errorMsg = error instanceof Error ? error.message : "\u672A\u77E5\u9519\u8BEF";
|
|
983
|
+
logger_default.error(`\u274C \u521B\u5EFA\u5B50\u4EFB\u52A1\u65F6\u53D1\u751F\u9519\u8BEF: ${taskName}`, error);
|
|
984
|
+
result.totalFailed++;
|
|
985
|
+
result.failed.push({
|
|
986
|
+
subject: taskName,
|
|
987
|
+
parentId: targetParentId,
|
|
988
|
+
error: errorMsg
|
|
989
|
+
});
|
|
990
|
+
const progress = result.totalCreated + result.totalFailed;
|
|
991
|
+
console.error(`[${progress}/${result.totalTasks}] \u274C \u521B\u5EFA\u5931\u8D25: ${taskName} - ${errorMsg}`);
|
|
992
|
+
}
|
|
993
|
+
await sleep(interval);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
};
|
|
998
|
+
var issueService = new IssueService();
|
|
999
|
+
|
|
1000
|
+
// src/services/time-entry-service.ts
|
|
1001
|
+
var TimeEntryService = class {
|
|
1002
|
+
/**
|
|
1003
|
+
* 查询工时条目
|
|
1004
|
+
*/
|
|
1005
|
+
async queryTimeEntries(params) {
|
|
1006
|
+
const requestParams = {
|
|
1007
|
+
token: params.token,
|
|
1008
|
+
host: params.host,
|
|
1009
|
+
project: params.project
|
|
1010
|
+
};
|
|
1011
|
+
if (params.from_date) requestParams.from_date = params.from_date;
|
|
1012
|
+
if (params.to_date) requestParams.to_date = params.to_date;
|
|
1013
|
+
if (params.user_id) requestParams.user_id = params.user_id;
|
|
1014
|
+
if (params.activity_id) requestParams.activity_id = params.activity_id;
|
|
1015
|
+
if (params.member_of_group_id) requestParams.member_of_group_id = params.member_of_group_id;
|
|
1016
|
+
if (params.tracker_id) requestParams.tracker_id = params.tracker_id;
|
|
1017
|
+
if (params.version_id) requestParams.version_id = params.version_id;
|
|
1018
|
+
if (params.offset !== void 0) requestParams.offset = params.offset;
|
|
1019
|
+
if (params.limit !== void 0) requestParams.limit = params.limit;
|
|
1020
|
+
logger_default.info("\u67E5\u8BE2\u5DE5\u65F6\u6761\u76EE", { host: params.host, project: params.project });
|
|
1021
|
+
return await apiClient.get("query_time_entries", requestParams);
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* 获取工时条目选项(活动类型列表等)
|
|
1025
|
+
* 使用 time_entry GET API
|
|
1026
|
+
*/
|
|
1027
|
+
async getTimeEntryOptions(token, host, project, issueId) {
|
|
1028
|
+
logger_default.info("\u83B7\u53D6\u5DE5\u65F6\u6761\u76EE\u9009\u9879", { host, project, issueId });
|
|
1029
|
+
const params = { token, host, project };
|
|
1030
|
+
if (issueId) params.issue_id = issueId;
|
|
1031
|
+
return await apiClient.get("time_entry", params);
|
|
1032
|
+
}
|
|
1033
|
+
/**
|
|
1034
|
+
* 创建工时条目
|
|
1035
|
+
* 使用 time_entry API(非 save_time_entry)
|
|
1036
|
+
*/
|
|
1037
|
+
async createTimeEntry(params) {
|
|
1038
|
+
logger_default.info("\u521B\u5EFA\u5DE5\u65F6\u6761\u76EE", { params });
|
|
1039
|
+
return await apiClient.post("time_entry", params);
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* 更新工时条目
|
|
1043
|
+
*/
|
|
1044
|
+
async updateTimeEntry(params) {
|
|
1045
|
+
logger_default.info("\u66F4\u65B0\u5DE5\u65F6\u6761\u76EE", { params });
|
|
1046
|
+
return await apiClient.post("save_time_entry", params);
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* 删除工时条目
|
|
1050
|
+
*/
|
|
1051
|
+
async deleteTimeEntry(token, host, timeEntryId) {
|
|
1052
|
+
logger_default.info("\u5220\u9664\u5DE5\u65F6\u6761\u76EE", { host, timeEntryId });
|
|
1053
|
+
return await apiClient.get("delete_time_entry", {
|
|
1054
|
+
token,
|
|
1055
|
+
host,
|
|
1056
|
+
id: timeEntryId
|
|
1057
|
+
// 使用 id 参数,非 time_entry_id
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
};
|
|
1061
|
+
var timeEntryService = new TimeEntryService();
|
|
1062
|
+
|
|
1063
|
+
// src/services/user-service.ts
|
|
1064
|
+
var UserService = class {
|
|
1065
|
+
/**
|
|
1066
|
+
* 测试连接 (通过获取项目列表来验证)
|
|
1067
|
+
*/
|
|
1068
|
+
async testConnection(token, host, project) {
|
|
1069
|
+
logger_default.info("\u6D4B\u8BD5\u8FDE\u63A5", { host, project });
|
|
1070
|
+
return await apiClient.get("project", { token, host });
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* 获取项目列表
|
|
1074
|
+
*/
|
|
1075
|
+
async getProjects(token, host) {
|
|
1076
|
+
logger_default.info("\u83B7\u53D6\u9879\u76EE\u5217\u8868", { host });
|
|
1077
|
+
return await apiClient.get("project", { token, host });
|
|
1078
|
+
}
|
|
1079
|
+
/**
|
|
1080
|
+
* 获取项目用户
|
|
1081
|
+
*/
|
|
1082
|
+
async getProjectUsers(token, host, project) {
|
|
1083
|
+
logger_default.info("\u83B7\u53D6\u9879\u76EE\u7528\u6237", { host, project });
|
|
1084
|
+
return await apiClient.get("user", { token, host, project });
|
|
1085
|
+
}
|
|
1086
|
+
/**
|
|
1087
|
+
* 获取主机信息
|
|
1088
|
+
*/
|
|
1089
|
+
async getHostInfo(token, host) {
|
|
1090
|
+
logger_default.info("\u83B7\u53D6\u4E3B\u673A\u4FE1\u606F", { host });
|
|
1091
|
+
return await apiClient.get("host", { token, host });
|
|
1092
|
+
}
|
|
1093
|
+
};
|
|
1094
|
+
var userService = new UserService();
|
|
1095
|
+
|
|
1096
|
+
// src/utils/config.ts
|
|
1097
|
+
import * as fs from "fs";
|
|
1098
|
+
import * as path from "path";
|
|
1099
|
+
import * as os from "os";
|
|
1100
|
+
var DEFAULT_CONFIG_DIR = path.join(os.homedir(), ".config", "pm-cli");
|
|
1101
|
+
var DEFAULT_CONFIG_FILE = "config.json";
|
|
1102
|
+
function getConfigPath(customPath) {
|
|
1103
|
+
if (customPath) {
|
|
1104
|
+
return customPath;
|
|
1105
|
+
}
|
|
1106
|
+
if (process.env.PM_CLI_CONFIG) {
|
|
1107
|
+
return process.env.PM_CLI_CONFIG;
|
|
1108
|
+
}
|
|
1109
|
+
return path.join(DEFAULT_CONFIG_DIR, DEFAULT_CONFIG_FILE);
|
|
1110
|
+
}
|
|
1111
|
+
function readConfig(customPath) {
|
|
1112
|
+
const configPath = getConfigPath(customPath);
|
|
1113
|
+
if (!fs.existsSync(configPath)) {
|
|
1114
|
+
return {
|
|
1115
|
+
default: {},
|
|
1116
|
+
profiles: {}
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
try {
|
|
1120
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
1121
|
+
return JSON.parse(content);
|
|
1122
|
+
} catch {
|
|
1123
|
+
return {
|
|
1124
|
+
default: {},
|
|
1125
|
+
profiles: {}
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
function resolveCredentials(options) {
|
|
1130
|
+
const config = readConfig(options.config);
|
|
1131
|
+
const result = {
|
|
1132
|
+
token: config.default.token,
|
|
1133
|
+
host: config.default.host,
|
|
1134
|
+
project: config.default.project
|
|
1135
|
+
};
|
|
1136
|
+
if (options.profile && config.profiles[options.profile]) {
|
|
1137
|
+
const profile = config.profiles[options.profile];
|
|
1138
|
+
if (profile.token) result.token = profile.token;
|
|
1139
|
+
if (profile.host) result.host = profile.host;
|
|
1140
|
+
if (profile.project) result.project = profile.project;
|
|
1141
|
+
}
|
|
1142
|
+
if (process.env.NETEASE_TOKEN) result.token = process.env.NETEASE_TOKEN;
|
|
1143
|
+
if (process.env.NETEASE_HOST) result.host = process.env.NETEASE_HOST;
|
|
1144
|
+
if (process.env.NETEASE_PROJECT) result.project = process.env.NETEASE_PROJECT;
|
|
1145
|
+
if (options.token) result.token = options.token;
|
|
1146
|
+
if (options.host) result.host = options.host;
|
|
1147
|
+
if (options.project) result.project = options.project;
|
|
1148
|
+
return result;
|
|
1149
|
+
}
|
|
1150
|
+
function validateCredentials(creds, requiredFields = ["token", "host", "project"]) {
|
|
1151
|
+
const missing = [];
|
|
1152
|
+
for (const field of requiredFields) {
|
|
1153
|
+
if (!creds[field]) {
|
|
1154
|
+
missing.push(field);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
return {
|
|
1158
|
+
valid: missing.length === 0,
|
|
1159
|
+
missing
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// src/utils/url-parser.ts
|
|
1164
|
+
var PM_URL_PATH_PATTERN = /https?:\/\/([^/]+\.pm\.netease\.com)\/v6\/issues\/(\d+)/;
|
|
1165
|
+
var PM_URL_QUERY_PATTERN = /https?:\/\/([^/]+\.pm\.netease\.com)\/v6\/issues\?/;
|
|
1166
|
+
function parsePmLink(url) {
|
|
1167
|
+
const pathMatch = url.match(PM_URL_PATH_PATTERN);
|
|
1168
|
+
if (pathMatch) {
|
|
1169
|
+
return {
|
|
1170
|
+
host: pathMatch[1],
|
|
1171
|
+
issueId: pathMatch[2]
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
const queryMatch = url.match(PM_URL_QUERY_PATTERN);
|
|
1175
|
+
if (queryMatch) {
|
|
1176
|
+
const host = queryMatch[1];
|
|
1177
|
+
const issueIdMatch = url.match(/[?&]issue_id=(\d+)/);
|
|
1178
|
+
if (issueIdMatch) {
|
|
1179
|
+
return {
|
|
1180
|
+
host,
|
|
1181
|
+
issueId: issueIdMatch[1]
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
return null;
|
|
1186
|
+
}
|
|
1187
|
+
function isPmLink(str) {
|
|
1188
|
+
return PM_URL_PATH_PATTERN.test(str) || PM_URL_QUERY_PATTERN.test(str);
|
|
1189
|
+
}
|
|
1190
|
+
function extractPmLinks(text) {
|
|
1191
|
+
const results = [];
|
|
1192
|
+
const urlPattern = /https?:\/\/[^\s]+\.pm\.netease\.com\/v6\/issues[^\s]*/g;
|
|
1193
|
+
const urls = text.match(urlPattern) || [];
|
|
1194
|
+
for (const url of urls) {
|
|
1195
|
+
const info = parsePmLink(url);
|
|
1196
|
+
if (info) {
|
|
1197
|
+
results.push(info);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
return results;
|
|
1201
|
+
}
|
|
1202
|
+
export {
|
|
1203
|
+
ApiClient,
|
|
1204
|
+
IssueService,
|
|
1205
|
+
TimeEntryService,
|
|
1206
|
+
UserService,
|
|
1207
|
+
apiClient,
|
|
1208
|
+
extractPmLinks,
|
|
1209
|
+
isPmLink,
|
|
1210
|
+
issueService,
|
|
1211
|
+
parsePmLink,
|
|
1212
|
+
resolveCredentials,
|
|
1213
|
+
setLogLevel,
|
|
1214
|
+
timeEntryService,
|
|
1215
|
+
userService,
|
|
1216
|
+
validateCredentials
|
|
1217
|
+
};
|
|
1218
|
+
//# sourceMappingURL=api.js.map
|