Skip to content

Commit 9702573

Browse files
committed
feat: story 命令版本模糊匹配,需求/任务过滤已完成
1 parent b3fbcf3 commit 9702573

1 file changed

Lines changed: 122 additions & 12 deletions

File tree

src/commands/stroy.command.ts

Lines changed: 122 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
CustomFieldId,
77
IssueItem,
88
IssueItemV2,
9+
IssueStatusId,
910
IssueTrackerId,
1011
ProjectMember,
1112
} from '../types';
@@ -24,10 +25,113 @@ interface StoryTaskContext {
2425
storyTaskMap: Map<number, IssueItemV2[]>;
2526
}
2627

27-
function validateVersion(version: string): void {
28-
if (!/^\d{4}$/.test(version)) {
29-
throw new Error('版本格式不正确,应为 4 位数字,例如 2605');
28+
function normalizeVersionValue(version: string): string {
29+
return version.toLowerCase().replace(/[\s._\-()[\]]+/g, '');
30+
}
31+
32+
function extractVersionDigits(version: string): string {
33+
return version.replace(/\D/g, '');
34+
}
35+
36+
function isSubsequence(input: string, target: string): boolean {
37+
if (!input) {
38+
return false;
39+
}
40+
41+
let targetIndex = 0;
42+
43+
for (const char of input) {
44+
targetIndex = target.indexOf(char, targetIndex);
45+
if (targetIndex === -1) {
46+
return false;
47+
}
48+
targetIndex++;
49+
}
50+
51+
return true;
52+
}
53+
54+
function scoreVersionCandidate(input: string, candidate: string): number {
55+
const normalizedInput = normalizeVersionValue(input);
56+
const normalizedCandidate = normalizeVersionValue(candidate);
57+
const inputDigits = extractVersionDigits(input);
58+
const candidateDigits = extractVersionDigits(candidate);
59+
60+
if (!normalizedInput || !normalizedCandidate) {
61+
return 0;
62+
}
63+
64+
if (normalizedInput === normalizedCandidate) {
65+
return 100;
66+
}
67+
68+
if (inputDigits && inputDigits === candidateDigits) {
69+
return 95;
70+
}
71+
72+
if (
73+
normalizedCandidate.includes(normalizedInput) ||
74+
normalizedInput.includes(normalizedCandidate)
75+
) {
76+
return 80;
77+
}
78+
79+
if (inputDigits && candidateDigits) {
80+
if (candidateDigits.includes(inputDigits) || inputDigits.includes(candidateDigits)) {
81+
return 75;
82+
}
83+
84+
const inputNumber = Number(inputDigits);
85+
const candidateNumber = Number(candidateDigits);
86+
if (Number.isSafeInteger(inputNumber) && Number.isSafeInteger(candidateNumber)) {
87+
const diff = Math.abs(inputNumber - candidateNumber);
88+
if (diff <= 20) {
89+
return 60 - diff;
90+
}
91+
}
3092
}
93+
94+
if (
95+
isSubsequence(normalizedInput, normalizedCandidate) ||
96+
isSubsequence(normalizedCandidate, normalizedInput)
97+
) {
98+
return 50;
99+
}
100+
101+
return 0;
102+
}
103+
104+
function suggestVersionOptions(input: string, options: string[]): string[] {
105+
return options
106+
.map((option) => ({
107+
option,
108+
score: scoreVersionCandidate(input, option),
109+
}))
110+
.filter((item) => item.score >= 50)
111+
.sort((a, b) => b.score - a.score || a.option.localeCompare(b.option, 'zh-CN'))
112+
.slice(0, 5)
113+
.map((item) => item.option);
114+
}
115+
116+
async function resolveStoryVersion(
117+
businessService: BusinessService,
118+
projectId: string,
119+
version: string
120+
): Promise<string> {
121+
const optionsMap = await businessService.getCustomFieldOptions(projectId, [
122+
CustomFieldId.VERSION,
123+
]);
124+
const versionOptions = optionsMap[CustomFieldId.VERSION] || [];
125+
const matchedVersion = versionOptions.find((option) => option === version);
126+
127+
if (matchedVersion) {
128+
return matchedVersion;
129+
}
130+
131+
const suggestions = suggestVersionOptions(version, versionOptions);
132+
const suggestionMessage = suggestions.length > 0 ? `,可能的版本:${suggestions.join('、')}` : '';
133+
134+
throw new Error(`版本 ${version} 不存在${suggestionMessage}`);
31135
}
32136

33137
function getDevelopmentEnd(): string {
@@ -43,25 +147,31 @@ function getDevelopmentEnd(): string {
43147
function isCurrentDevelopmentTask(issue: IssueItemV2, developmentEnd: string): boolean {
44148
return (
45149
issue.tracker?.id === IssueTrackerId.TASK &&
150+
isActiveIssueStatus(issue.status?.id) &&
46151
issue.customValueNew?.[CustomFieldId.DEVELOPMENT_END] === developmentEnd
47152
);
48153
}
49154

155+
function isActiveIssueStatus(statusId?: number): boolean {
156+
return statusId !== IssueStatusId.REJECTED && statusId !== IssueStatusId.CLOSED;
157+
}
158+
50159
async function loadStoryTaskContext(
51160
version: string,
52161
cliOptions: CliOptions = {}
53162
): Promise<StoryTaskContext> {
54-
validateVersion(version);
55-
56163
const { projectId, roleIds, config } = loadConfig(cliOptions);
57164
const developmentEnd = getDevelopmentEnd();
58165
const businessService = new BusinessService(config);
59-
60-
const stories = await businessService.getStoriesByVersionAndDevelopmentEnd(
61-
projectId,
62-
version,
63-
developmentEnd
64-
);
166+
const resolvedVersion = await resolveStoryVersion(businessService, projectId, version);
167+
168+
const stories = (
169+
await businessService.getStoriesByVersionAndDevelopmentEnd(
170+
projectId,
171+
resolvedVersion,
172+
developmentEnd
173+
)
174+
).filter((story) => isActiveIssueStatus(story.status?.id));
65175
const storyTaskMap = new Map<number, IssueItemV2[]>();
66176

67177
for (const story of stories) {
@@ -76,7 +186,7 @@ async function loadStoryTaskContext(
76186
businessService,
77187
projectId,
78188
roleIds,
79-
version,
189+
version: resolvedVersion,
80190
developmentEnd,
81191
stories,
82192
storyTaskMap,

0 commit comments

Comments
 (0)