@@ -60,6 +60,7 @@ const TARGETS: CheckTarget[] = [
6060 { site : 'wikipedia' , command : 'trending' , args : [ '--limit' , '10' ] } ,
6161 { site : 'sinafinance' , command : 'news' , args : [ '--limit' , '10' ] } ,
6262 { site : 'weread' , command : 'ranking' , args : [ '--limit' , '10' ] } ,
63+ // "不开玩笑 Jokes Aside" podcast by 猫头鹰喜剧
6364 { site : 'xiaoyuzhou' , command : 'podcast' , args : [ '61791d921989541784257779' ] } ,
6465 { site : 'yollomi' , command : 'models' } ,
6566] ;
@@ -74,17 +75,30 @@ function loadSnapshot(site: string, command: string): CommandSchema | null {
7475 const p = snapshotPath ( site , command ) ;
7576 if ( ! fs . existsSync ( p ) ) return null ;
7677 try {
77- return JSON . parse ( fs . readFileSync ( p , 'utf8' ) ) ;
78+ const data = JSON . parse ( fs . readFileSync ( p , 'utf8' ) ) ;
79+ // Validate snapshot structure to avoid crashes in diffSchemas
80+ if ( ! data || typeof data !== 'object' || typeof data . fields !== 'object' || typeof data . rowCount !== 'number' ) {
81+ console . warn ( `Warning: invalid snapshot structure for ${ site } /${ command } , treating as first run` ) ;
82+ return null ;
83+ }
84+ return data as CommandSchema ;
7885 } catch ( err ) {
7986 console . warn ( `Warning: corrupt snapshot for ${ site } /${ command } , treating as first run:` , err ) ;
8087 return null ;
8188 }
8289}
8390
91+ /** 原子写入:先写临时文件再 rename,防止 CI cancel 导致截断 JSON */
92+ function atomicWrite ( filePath : string , content : string ) : void {
93+ const tmp = filePath + '.tmp' ;
94+ fs . writeFileSync ( tmp , content ) ;
95+ fs . renameSync ( tmp , filePath ) ;
96+ }
97+
8498/** 保存命令快照 */
8599function saveSnapshot ( schema : CommandSchema , site : string , command : string ) : void {
86100 fs . mkdirSync ( SNAPSHOT_DIR , { recursive : true } ) ;
87- fs . writeFileSync ( snapshotPath ( site , command ) , JSON . stringify ( schema , null , 2 ) + '\n' ) ;
101+ atomicWrite ( snapshotPath ( site , command ) , JSON . stringify ( schema , null , 2 ) + '\n' ) ;
88102}
89103
90104/** 失败元数据目录(与快照分离,跨 drift 事件保留) */
@@ -112,7 +126,7 @@ function saveFailureCount(site: string, command: string, count: number): void {
112126 if ( count === 0 ) {
113127 if ( fs . existsSync ( metaPath ) ) fs . unlinkSync ( metaPath ) ;
114128 } else {
115- fs . writeFileSync ( metaPath , JSON . stringify ( { count } ) + '\n' ) ;
129+ atomicWrite ( metaPath , JSON . stringify ( { count } ) + '\n' ) ;
116130 }
117131}
118132
@@ -130,7 +144,8 @@ async function runCommand(target: CheckTarget): Promise<{ data: unknown[] | null
130144 return { data : parsed } ;
131145 } catch ( err : any ) {
132146 const msg = err . stderr ?. trim ( ) || err . message || 'Unknown error' ;
133- return { data : null , error : `Exit code ${ err . code ?? 1 } : ${ msg . slice ( 0 , 200 ) } ` } ;
147+ const exitCode = err . status ?? err . code ?? 'unknown' ;
148+ return { data : null , error : `Exit code ${ exitCode } : ${ msg . slice ( 0 , 200 ) } ` } ;
134149 }
135150}
136151
@@ -195,13 +210,16 @@ async function main(): Promise<void> {
195210 }
196211 }
197212
213+ // 统一时间戳,避免跨日不一致
214+ const now = new Date ( ) ;
215+
198216 // 输出人类可读摘要
199- console . log ( formatReport ( results ) ) ;
217+ console . log ( formatReport ( results , now ) ) ;
200218
201219 // 写入 JSON 报告(供 CI artifact 上传)
202220 fs . mkdirSync ( SNAPSHOT_DIR , { recursive : true } ) ;
203- const report = buildReport ( results ) ;
204- fs . writeFileSync (
221+ const report = buildReport ( results , now ) ;
222+ atomicWrite (
205223 path . join ( SNAPSHOT_DIR , 'drift-report.json' ) ,
206224 JSON . stringify ( report , null , 2 ) + '\n' ,
207225 ) ;
0 commit comments