@@ -47,9 +47,22 @@ function detectCurrentIDE(): string {
4747 return "vscode" ;
4848}
4949
50+ /**
51+ * 模板版本号。每次更新模板内容时递增。
52+ * rulesInjector 会检测已注入文件的版本号,低于此版本则自动更新。
53+ */
54+ const TEMPLATE_VERSION = 2 ;
55+
56+ /** 从文件内容中提取版本号,找不到返回 0(旧版无版本标记) */
57+ function extractVersion ( content : string ) : number {
58+ const match = content . match ( / < ! - - T e s t P i l o t - T e m p l a t e - V e r s i o n : ( \d + ) - - > / ) ;
59+ return match ? parseInt ( match [ 1 ] , 10 ) : 0 ;
60+ }
61+
5062/** 规则模板内容(精简版:通用规则 + 引导AI读取平台专属规则文件) */
5163function getTemplateContent ( ) : string {
52- return `# TestPilot AI — 编程AI蓝本自动生成规则
64+ return `<!-- TestPilot-Template-Version: ${ TEMPLATE_VERSION } -->
65+ # TestPilot AI — 编程AI蓝本自动生成规则
5366
5467> 本文件由 TestPilot AI 插件自动注入,指导编程AI在开发过程中自动生成和维护测试蓝本。
5568> 你可以根据项目需求自由修改本文件内容。
@@ -291,12 +304,28 @@ function injectPlatformTemplates(
291304 const templates = getPlatformTemplates ( extensionPath ) ;
292305 const targetDir = path . join ( workspaceRoot , ".testpilot" , "platforms" ) ;
293306
294- // 如果 .testpilot/platforms 目录已存在并且有内容,跳过
307+ // 检查 .testpilot/platforms 目录
295308 if ( fs . existsSync ( targetDir ) ) {
296309 const existing = fs . readdirSync ( targetDir ) . filter ( ( f : string ) => f . endsWith ( ".md" ) ) ;
297310 if ( existing . length >= PLATFORM_FILES . length ) {
298- outputChannel ?. appendLine ( `[TestPilot AI] ⏭️ .testpilot/platforms/ 已有 ${ existing . length } 个模板,跳过` ) ;
299- return created ;
311+ // 所有文件都存在,检查版本号决定是否需要更新
312+ let needsUpdate = false ;
313+ for ( const fileName of PLATFORM_FILES ) {
314+ const targetPath = path . join ( targetDir , fileName ) ;
315+ if ( fs . existsSync ( targetPath ) ) {
316+ const content = fs . readFileSync ( targetPath , "utf-8" ) ;
317+ const ver = extractVersion ( content ) ;
318+ if ( ver < TEMPLATE_VERSION ) {
319+ needsUpdate = true ;
320+ break ;
321+ }
322+ }
323+ }
324+ if ( ! needsUpdate ) {
325+ outputChannel ?. appendLine ( `[TestPilot AI] ⏭️ .testpilot/platforms/ 已有 ${ existing . length } 个模板且版本最新,跳过` ) ;
326+ return created ;
327+ }
328+ outputChannel ?. appendLine ( `[TestPilot AI] 🔄 .testpilot/platforms/ 版本过旧,更新中...` ) ;
300329 }
301330 }
302331
@@ -305,7 +334,7 @@ function injectPlatformTemplates(
305334 fs . mkdirSync ( targetDir , { recursive : true } ) ;
306335 }
307336
308- // 写入每个平台模板(不存在才创建 )
337+ // 写入每个平台模板(不存在则创建,版本过旧则更新 )
309338 for ( const [ fileName , content ] of templates ) {
310339 const targetPath = path . join ( targetDir , fileName ) ;
311340 if ( ! fs . existsSync ( targetPath ) ) {
@@ -316,6 +345,18 @@ function injectPlatformTemplates(
316345 const msg = err instanceof Error ? err . message : String ( err ) ;
317346 outputChannel ?. appendLine ( `[TestPilot AI] ⚠️ 创建 ${ fileName } 失败: ${ msg } ` ) ;
318347 }
348+ } else {
349+ // 已存在:检查版本号
350+ try {
351+ const existingContent = fs . readFileSync ( targetPath , "utf-8" ) ;
352+ const existingVersion = extractVersion ( existingContent ) ;
353+ if ( existingVersion < TEMPLATE_VERSION ) {
354+ fs . writeFileSync ( targetPath , content , "utf-8" ) ;
355+ created . push ( `.testpilot/platforms/${ fileName } (v${ existingVersion } →v${ TEMPLATE_VERSION } )` ) ;
356+ }
357+ } catch {
358+ // 静默忽略
359+ }
319360 }
320361 }
321362
@@ -373,17 +414,54 @@ export function injectRules(
373414
374415 if ( fs . existsSync ( fullPath ) ) {
375416 if ( isAgentsMd ) {
376- // AGENTS.md / CLAUDE.md 是 TestPilot 专属文件,已存在则跳过
377- skipped . push ( relPath ) ;
417+ // AGENTS.md / CLAUDE.md:检查版本号,旧版本则自动更新
418+ try {
419+ const existingContent = fs . readFileSync ( fullPath , "utf-8" ) ;
420+ const existingVersion = extractVersion ( existingContent ) ;
421+ if ( existingVersion >= TEMPLATE_VERSION ) {
422+ skipped . push ( relPath ) ;
423+ continue ;
424+ }
425+ // 版本过旧,更新文件
426+ fs . writeFileSync ( fullPath , template , "utf-8" ) ;
427+ created . push ( relPath + ` (v${ existingVersion } →v${ TEMPLATE_VERSION } )` ) ;
428+ outputChannel ?. appendLine (
429+ `[TestPilot AI] 🔄 ${ relPath } 版本过旧(v${ existingVersion } ),已更新到 v${ TEMPLATE_VERSION } ` ,
430+ ) ;
431+ } catch ( err ) {
432+ const msg = err instanceof Error ? err . message : String ( err ) ;
433+ outputChannel ?. appendLine ( `[TestPilot AI] ⚠️ 更新 ${ relPath } 失败: ${ msg } ` ) ;
434+ skipped . push ( relPath ) ;
435+ }
378436 continue ;
379437 }
380438
381439 // IDE 专用规则文件(如 .github/copilot-instructions.md):追加模式
382440 try {
383441 const existingContent = fs . readFileSync ( fullPath , "utf-8" ) ;
384442 if ( existingContent . includes ( TESTPILOT_MARKER ) ) {
385- // 已包含 TestPilot 规则,跳过
386- skipped . push ( relPath ) ;
443+ // 已包含 TestPilot 规则,检查版本
444+ const existingVersion = extractVersion ( existingContent ) ;
445+ if ( existingVersion >= TEMPLATE_VERSION ) {
446+ skipped . push ( relPath ) ;
447+ continue ;
448+ }
449+ // 版本过旧:替换 TestPilot 部分(保留用户自己的内容)
450+ const markerIndex = existingContent . indexOf ( TESTPILOT_MARKER ) ;
451+ // 向前找分隔线或版本标记
452+ let startIndex = existingContent . lastIndexOf ( "---" , markerIndex ) ;
453+ if ( startIndex === - 1 ) startIndex = existingContent . lastIndexOf ( "<!-- TestPilot" , markerIndex ) ;
454+ if ( startIndex === - 1 ) startIndex = markerIndex ;
455+ const userContent = existingContent . substring ( 0 , startIndex ) . trimEnd ( ) ;
456+ if ( userContent . length > 0 ) {
457+ fs . writeFileSync ( fullPath , userContent + "\n\n---\n\n" + template , "utf-8" ) ;
458+ } else {
459+ fs . writeFileSync ( fullPath , template , "utf-8" ) ;
460+ }
461+ created . push ( relPath + ` (v${ existingVersion } →v${ TEMPLATE_VERSION } )` ) ;
462+ outputChannel ?. appendLine (
463+ `[TestPilot AI] 🔄 ${ relPath } TestPilot规则已更新到 v${ TEMPLATE_VERSION } ` ,
464+ ) ;
387465 continue ;
388466 }
389467 // 在用户已有内容末尾追加 TestPilot 规则
@@ -444,10 +522,20 @@ export function injectRules(
444522
445523/**
446524 * 检查工作区是否需要注入规则
447- * 缺少 AGENTS.md 或缺少 .testpilot/platforms 目录都需要注入
525+ * 缺少 AGENTS.md、 .testpilot/platforms 目录不全、或版本过旧都需要注入
448526 */
449527export function needsInjection ( workspaceRoot : string ) : boolean {
450- if ( ! fs . existsSync ( path . join ( workspaceRoot , "AGENTS.md" ) ) ) {
528+ const agentsPath = path . join ( workspaceRoot , "AGENTS.md" ) ;
529+ if ( ! fs . existsSync ( agentsPath ) ) {
530+ return true ;
531+ }
532+ // 检查 AGENTS.md 版本号
533+ try {
534+ const content = fs . readFileSync ( agentsPath , "utf-8" ) ;
535+ if ( extractVersion ( content ) < TEMPLATE_VERSION ) {
536+ return true ;
537+ }
538+ } catch {
451539 return true ;
452540 }
453541 // 检查平台模板是否已注入
@@ -456,7 +544,24 @@ export function needsInjection(workspaceRoot: string): boolean {
456544 return true ;
457545 }
458546 const existing = fs . readdirSync ( platformDir ) . filter ( ( f : string ) => f . endsWith ( ".md" ) ) ;
459- return existing . length < PLATFORM_FILES . length ;
547+ if ( existing . length < PLATFORM_FILES . length ) {
548+ return true ;
549+ }
550+ // 检查平台模板版本
551+ for ( const fileName of PLATFORM_FILES ) {
552+ const filePath = path . join ( platformDir , fileName ) ;
553+ if ( fs . existsSync ( filePath ) ) {
554+ try {
555+ const content = fs . readFileSync ( filePath , "utf-8" ) ;
556+ if ( extractVersion ( content ) < TEMPLATE_VERSION ) {
557+ return true ;
558+ }
559+ } catch {
560+ return true ;
561+ }
562+ }
563+ }
564+ return false ;
460565}
461566
462567/**
0 commit comments