@@ -93,6 +93,12 @@ export class AttachmentCollector {
9393 * 处理单个 @ 提及
9494 */
9595 private async processOne ( mention : AtMention ) : Promise < Attachment > {
96+ // Glob 模式处理
97+ if ( mention . isGlob ) {
98+ logger . debug ( `Processing glob pattern: ${ mention . path } ` ) ;
99+ return await this . processGlob ( mention . path ) ;
100+ }
101+
96102 // 安全验证
97103 const absolutePath = await PathSecurity . validatePath (
98104 mention . path ,
@@ -104,10 +110,10 @@ export class AttachmentCollector {
104110
105111 const stats = await fs . stat ( realPath ) ;
106112
107- // 目录处理
113+ // 目录处理 - 展示树结构
108114 if ( stats . isDirectory ( ) ) {
109115 logger . debug ( `Processing directory: ${ mention . path } ` ) ;
110- return await this . readDirectory ( realPath , mention . path ) ;
116+ return await this . renderDirectoryTree ( realPath , mention . path ) ;
111117 }
112118
113119 // 文件处理
@@ -224,9 +230,9 @@ export class AttachmentCollector {
224230 }
225231
226232 /**
227- * 读取目录内容
233+ * 渲染目录树结构(不读取文件内容,仅展示结构)
228234 */
229- private async readDirectory (
235+ private async renderDirectoryTree (
230236 absolutePath : string ,
231237 relativePath : string
232238 ) : Promise < Attachment > {
@@ -258,44 +264,191 @@ export class AttachmentCollector {
258264
259265 logger . debug ( `Found ${ files . length } files in directory: ${ relativePath } ` ) ;
260266
261- // 限制文件数量
262- const maxFiles = 50 ;
267+ // 构建树形结构
268+ const tree = this . buildFileTree ( files ) ;
269+ const treeContent = this . printTree ( tree , relativePath ) ;
270+
271+ // 限制显示的文件数
272+ const maxFiles = 500 ;
273+ const suffix =
274+ files . length > maxFiles
275+ ? `\n\n[... and ${ files . length - maxFiles } more files]`
276+ : '' ;
277+
278+ return {
279+ type : 'directory' ,
280+ path : relativePath ,
281+ content : treeContent + suffix ,
282+ metadata : {
283+ lines : files . length ,
284+ truncated : files . length > maxFiles ,
285+ } ,
286+ } ;
287+ }
288+
289+ /**
290+ * 构建文件树结构
291+ */
292+ private buildFileTree ( files : string [ ] ) : Map < string , any > {
293+ const tree = new Map < string , any > ( ) ;
294+
295+ for ( const file of files ) {
296+ const parts = file . split ( '/' ) ;
297+ let current = tree ;
298+
299+ for ( let i = 0 ; i < parts . length ; i ++ ) {
300+ const part = parts [ i ] ;
301+ const isFile = i === parts . length - 1 ;
302+
303+ if ( ! current . has ( part ) ) {
304+ current . set ( part , isFile ? null : new Map < string , any > ( ) ) ;
305+ }
306+
307+ if ( ! isFile ) {
308+ current = current . get ( part ) ;
309+ }
310+ }
311+ }
312+
313+ return tree ;
314+ }
315+
316+ /**
317+ * 打印树形结构为 ASCII 格式
318+ */
319+ private printTree (
320+ tree : Map < string , any > ,
321+ rootPath : string ,
322+ prefix : string = '' ,
323+ isLast : boolean = true
324+ ) : string {
325+ const lines : string [ ] = [ ] ;
326+
327+ // 根目录
328+ if ( prefix === '' ) {
329+ lines . push ( `${ rootPath } /` ) ;
330+ }
331+
332+ // 按名称排序,目录优先
333+ const entries = Array . from ( tree . entries ( ) ) . sort ( ( a , b ) => {
334+ const aIsDir = a [ 1 ] instanceof Map ;
335+ const bIsDir = b [ 1 ] instanceof Map ;
336+ if ( aIsDir !== bIsDir ) return bIsDir ? 1 : - 1 ;
337+ return a [ 0 ] . localeCompare ( b [ 0 ] ) ;
338+ } ) ;
339+
340+ entries . forEach ( ( [ name , value ] , index ) => {
341+ const isLastEntry = index === entries . length - 1 ;
342+ const connector = isLastEntry ? '└── ' : '├── ' ;
343+ const isDir = value instanceof Map ;
344+
345+ lines . push ( `${ prefix } ${ connector } ${ name } ${ isDir ? '/' : '' } ` ) ;
346+
347+ if ( isDir && value . size > 0 ) {
348+ const newPrefix = prefix + ( isLastEntry ? ' ' : '│ ' ) ;
349+ lines . push ( this . printTree ( value , '' , newPrefix , isLastEntry ) ) ;
350+ }
351+ } ) ;
352+
353+ return lines . filter ( ( l ) => l ) . join ( '\n' ) ;
354+ }
355+
356+ /**
357+ * 处理 Glob 模式
358+ */
359+ private async processGlob ( pattern : string ) : Promise < Attachment > {
360+ // 使用 fast-glob 展开模式
361+ const files = ( await fg ( pattern , {
362+ cwd : this . options . cwd ,
363+ dot : false ,
364+ followSymbolicLinks : false ,
365+ onlyFiles : true ,
366+ unique : true ,
367+ ignore : [
368+ 'node_modules/**' ,
369+ '.git/**' ,
370+ 'dist/**' ,
371+ 'build/**' ,
372+ '.next/**' ,
373+ '.cache/**' ,
374+ 'coverage/**' ,
375+ ] ,
376+ } ) ) as string [ ] ;
377+
378+ if ( files . length === 0 ) {
379+ return {
380+ type : 'error' ,
381+ path : pattern ,
382+ content : '' ,
383+ error : `No files matched pattern: ${ pattern } ` ,
384+ } ;
385+ }
386+
387+ logger . debug ( `Glob pattern "${ pattern } " matched ${ files . length } files` ) ;
388+
389+ // 限制最大文件数
390+ const maxFiles = 30 ;
263391 const limitedFiles = files . slice ( 0 , maxFiles ) ;
264392
265- // 读取所有文件(并行)
393+ // 读取所有匹配的文件
266394 const fileContents = await Promise . allSettled (
267395 limitedFiles . map ( async ( file ) => {
268- const filePath = path . join ( absolutePath , file ) ;
396+ const absolutePath = path . join ( this . options . cwd , file ) ;
269397 try {
270- const content = await fs . readFile ( filePath , 'utf-8' ) ;
271- // 限制每个文件的长度
272- const truncated =
273- content . length > 10000 ? content . slice ( 0 , 10000 ) + '\n...' : content ;
274- return `--- ${ file } ---\n${ truncated } \n` ;
398+ const content = await fs . readFile ( absolutePath , 'utf-8' ) ;
399+ const lines = content . split ( '\n' ) ;
400+
401+ // 限制每个文件的行数
402+ const maxLinesPerFile = 200 ;
403+ let truncatedContent = content ;
404+ let truncated = false ;
405+
406+ if ( lines . length > maxLinesPerFile ) {
407+ truncatedContent = lines . slice ( 0 , maxLinesPerFile ) . join ( '\n' ) ;
408+ truncatedContent += `\n\n[... truncated ${ lines . length - maxLinesPerFile } lines ...]` ;
409+ truncated = true ;
410+ }
411+
412+ return {
413+ path : file ,
414+ content : truncatedContent ,
415+ lines : lines . length ,
416+ truncated,
417+ } ;
275418 } catch ( error ) {
276- return `--- ${ file } ---\n[Error reading file: ${ error instanceof Error ? error . message : 'unknown error' } ]\n` ;
419+ return {
420+ path : file ,
421+ content : `[Error: ${ error instanceof Error ? error . message : 'unknown error' } ]` ,
422+ lines : 0 ,
423+ truncated : false ,
424+ } ;
277425 }
278426 } )
279427 ) ;
280428
281- // 合并所有文件内容
282- const contentParts = fileContents . map ( ( result ) =>
283- result . status === 'fulfilled' ? result . value : '[Error]'
429+ // 组装结果
430+ const results = fileContents
431+ . map ( ( result ) => ( result . status === 'fulfilled' ? result . value : null ) )
432+ . filter ( ( r ) : r is NonNullable < typeof r > => r !== null ) ;
433+
434+ // 格式化为多文件附件
435+ const contentParts = results . map (
436+ ( r ) => `--- ${ r . path } (${ r . lines } lines${ r . truncated ? ', truncated' : '' } ) ---\n${ r . content } `
284437 ) ;
285438
286- const content = contentParts . join ( '\n' ) ;
439+ const content = contentParts . join ( '\n\n ' ) ;
287440 const suffix =
288441 files . length > maxFiles
289- ? `\n\n[... ${ files . length - maxFiles } more files omitted ... ]`
442+ ? `\n\n[... and ${ files . length - maxFiles } more files matched ]`
290443 : '' ;
291444
292445 return {
293- type : 'directory ' ,
294- path : relativePath ,
446+ type : 'file ' ,
447+ path : pattern ,
295448 content : content + suffix ,
296449 metadata : {
297- lines : files . length ,
298- truncated : files . length > maxFiles ,
450+ lines : results . reduce ( ( sum , r ) => sum + r . lines , 0 ) ,
451+ truncated : files . length > maxFiles || results . some ( ( r ) => r . truncated ) ,
299452 } ,
300453 } ;
301454 }
0 commit comments