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
33137function getDevelopmentEnd ( ) : string {
@@ -43,25 +147,31 @@ function getDevelopmentEnd(): string {
43147function 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+
50159async 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