@@ -12,8 +12,10 @@ import * as fs from 'fs';
1212import * as path from 'path' ;
1313import * as mysql from 'mysql2/promise' ;
1414import * as childProcess from 'child_process' ;
15+ import { exec } from 'child_process' ;
1516import { promisify } from 'util' ;
1617import { ragService , CONFIG as RAG_CONFIG } from './ragService' ;
18+ import { parseString } from 'xml2js' ;
1719
1820
1921/** 定义工具的接口 */
@@ -321,6 +323,7 @@ export const searchTool: Tool = {
321323 }
322324 } ,
323325} ;
326+ registerTool ( searchTool ) ;
324327
325328// 2. 获取当前日期时间
326329export const getCurrentDateTime : Tool = {
@@ -336,6 +339,7 @@ export const getCurrentDateTime: Tool = {
336339 return now . toLocaleString ( ) ;
337340 } ,
338341} ;
342+ registerTool ( getCurrentDateTime ) ;
339343
340344// 3. 读取指定路径文本
341345export const readTextFile : Tool = {
@@ -360,6 +364,7 @@ export const readTextFile: Tool = {
360364 }
361365 } ,
362366} ;
367+ registerTool ( readTextFile ) ;
363368
364369// 4. 连接 MySQL 查表
365370export const queryMySQL : Tool = {
@@ -392,28 +397,170 @@ export const queryMySQL: Tool = {
392397 }
393398 } ,
394399} ;
395-
396- // 5. 读取 SVN 日志
400+ registerTool ( queryMySQL ) ;
401+
402+ // SVN 日志条目类型
403+ type LogEntry = {
404+ revision : string ;
405+ author ?: string ;
406+ date ?: string ;
407+ message ?: string ;
408+ paths ?: Array < { action : string ; path : string } > ;
409+ } ;
410+
411+ // 增强错误类型
412+ type SVNError = Error & { stderr ?: string } ;
413+ const execAsync = promisify ( exec ) ;
397414export const getSVNLog : Tool = {
398415 name : 'get_svn_log' ,
399- description : '获取 SVN 仓库的日志。 ' ,
416+ description : '获取SVN日志,支持多种查询条件 ' ,
400417 parameters : {
401418 type : 'object' ,
402419 properties : {
403- repoPath : { type : 'string' , description : 'SVN 仓库的本地路径。' } ,
420+ path : {
421+ type : 'string' ,
422+ description : `仓库路径,支持以下格式:
423+ - 本地工作副本路径:'/projects/myapp'
424+ - 仓库URL:'http://svn.example.com/repo'
425+ - 仓库子目录:'http://svn.example.com/repo/trunk/src'`
426+ } ,
427+ author : {
428+ type : 'string' ,
429+ description : '提交者名称筛选(支持模糊匹配)'
430+ } ,
431+ keyword : {
432+ type : 'string' ,
433+ description : '提交信息关键词筛选(支持模糊匹配)'
434+ } ,
435+ startDate : {
436+ type : 'string' ,
437+ description : `开始时间,支持格式:
438+ - 绝对时间:'2024-03-01'
439+ - 相对时间:'1h'(最近1小时)/'24h'(最近1天)/'7d'(最近7天)/'30d'(最近30天)`
440+ } ,
441+ endDate : {
442+ type : 'string' ,
443+ description : `结束时间,支持格式:
444+ - 绝对时间:'2024-03-15'
445+ - 相对时间:'1h'(1小时前)`
446+ } ,
447+ revisionStart : {
448+ type : 'number' ,
449+ description : '起始版本号(包含)'
450+ } ,
451+ revisionEnd : {
452+ type : 'number' ,
453+ description : '结束版本号(包含)'
454+ } ,
455+ limit : {
456+ type : 'number' ,
457+ description : '返回结果最大数量'
458+ }
404459 } ,
405- required : [ 'repoPath ' ] ,
460+ required : [ 'path ' ] ,
406461 } ,
407- function : async ( args : { repoPath : string } ) => {
408- try {
409- const exec = promisify ( childProcess . exec ) ;
410- const { stdout } = await exec ( `svn log "${ args . repoPath } "` ) ;
411- return stdout ;
412- } catch ( error : any ) {
413- return `获取 SVN 日志失败: ${ error . message } ` ;
462+ function : async ( args ) : Promise < string > => {
463+ try {
464+ const params : string [ ] = [ ] ;
465+ let repoPath = args . path ;
466+
467+ // 时间处理函数
468+ const parseTime = ( timeStr ?: string , isEndTime = false ) : string => {
469+ if ( ! timeStr ) { return isEndTime ? 'HEAD' : '1' ; }
470+
471+ const relativeMatch = timeStr . match ( / ^ ( \d + ) ( [ h d ] ) $ / ) ;
472+ if ( relativeMatch ) {
473+ const now = new Date ( ) ;
474+ const [ , value , unit ] = relativeMatch ;
475+ const ms = parseInt ( value ) * ( unit === 'h' ? 3600 : 86400 ) * 1000 ;
476+ return `{${ new Date ( now . getTime ( ) - ms ) . toISOString ( ) . slice ( 0 , 19 ) } }` ;
477+ }
478+
479+ if ( / ^ \d { 4 } - \d { 2 } - \d { 2 } $ / . test ( timeStr ) ) {
480+ return isEndTime
481+ ? `{${ timeStr } 23:59:59}`
482+ : `{${ timeStr } 00:00:00}` ;
483+ }
484+
485+ throw new Error ( `Invalid time format: ${ timeStr } ` ) ;
486+ } ;
487+
488+ // 构建查询参数
489+ if ( args . author ) { params . push ( `--search "author:${ args . author } "` ) ; }
490+ if ( args . keyword ) { params . push ( `--search "message:${ args . keyword } "` ) ; }
491+
492+ const startRev = parseTime ( args . startDate ) ;
493+ const endRev = parseTime ( args . endDate , true ) ;
494+ if ( startRev !== '1' || endRev !== 'HEAD' ) {
495+ params . push ( `--revision ${ startRev } :${ endRev } ` ) ;
414496 }
415- } ,
497+
498+ if ( args . revisionStart || args . revisionEnd ) {
499+ const start = args . revisionStart ?? 1 ;
500+ const end = args . revisionEnd ?? 'HEAD' ;
501+ params . push ( `--revision ${ start } :${ end } ` ) ;
502+ }
503+
504+ if ( args . limit ) { params . push ( `-l ${ args . limit } ` ) ; }
505+
506+ // 获取仓库根地址
507+ if ( ! / ^ ( h t t p | h t t p s | s v n ) : \/ \/ / . test ( repoPath ) ) {
508+ const { stdout } = await execAsync (
509+ `svn info "${ repoPath } " --show-item repos-root-url` ,
510+ { encoding : 'utf8' }
511+ ) ;
512+ repoPath = stdout . trim ( ) ;
513+ }
514+
515+ // 执行命令
516+ const { stdout } = await execAsync (
517+ `svn log "${ repoPath } " --xml ${ params . join ( ' ' ) } ` ,
518+ { encoding : 'gbk' as BufferEncoding , maxBuffer : 1024 * 1024 * 10 }
519+ ) ;
520+
521+ // 解析XML
522+ const parsed = await new Promise < any > ( ( resolve , reject ) => {
523+ parseString ( stdout , ( err , result ) => {
524+ err ? reject ( err ) : resolve ( result ) ;
525+ } ) ;
526+ } ) ;
527+
528+ // 转换为LogEntry数组
529+ const entries : LogEntry [ ] = ( parsed . log . logentry || [ ] ) . map (
530+ ( entry : any ) => ( {
531+ revision : entry . $ . revision ,
532+ author : entry . author ?. [ 0 ] ,
533+ date : entry . date ?. [ 0 ] ,
534+ message : entry . msg ?. [ 0 ] ?. trim ( ) ,
535+ paths : ( entry . paths ?. [ 0 ] ?. path || [ ] ) . map ( ( p : any ) => ( {
536+ action : p . $ . action ,
537+ path : p . _
538+ } ) )
539+ } )
540+ ) ;
541+
542+ return JSON . stringify ( entries , null , 2 ) ;
543+
544+ } catch ( error ) {
545+ const err = error as SVNError ;
546+ const errorMapping : Record < string , string > = {
547+ '175002' : '认证失败' ,
548+ '160013' : '路径不存在' ,
549+ '200009' : '无效时间格式' ,
550+ '205000' : '无效版本号'
551+ } ;
552+
553+ const errorCode = err . stderr ?. match ( / s v n : E ( \d + ) : / ) ?. [ 1 ] || '' ;
554+ const message = errorCode in errorMapping
555+ ? `SVN错误 [E${ errorCode } ]: ${ errorMapping [ errorCode ] } `
556+ : `操作失败: ${ err . message ?. replace ( / ^ s v n : E \d + : / , '' ) || '未知错误' } ` ;
557+
558+ return message ;
559+ }
560+ }
416561} ;
562+
563+ registerTool ( getSVNLog ) ;
417564
418565// 6. 比对 SVN 本地差异的 diff
419566export const getSVNDiff : Tool = {
@@ -436,6 +583,7 @@ export const getSVNDiff: Tool = {
436583 }
437584 } ,
438585} ;
586+ registerTool ( getSVNDiff ) ;
439587
440588// 7. 比对 GitHub 本地差异的 diff
441589export const getGitDiff : Tool = {
@@ -458,6 +606,7 @@ export const getGitDiff: Tool = {
458606 }
459607 } ,
460608} ;
609+ registerTool ( getGitDiff ) ;
461610
462611// 8. Grep 搜索指定目录文本
463612export const grepSearch : Tool = {
@@ -481,6 +630,7 @@ export const grepSearch: Tool = {
481630 }
482631 } ,
483632} ;
633+ registerTool ( grepSearch ) ;
484634
485635// 9. 在路径下递归搜索文件
486636export const findFiles : Tool = {
@@ -507,39 +657,50 @@ export const findFiles: Tool = {
507657 function : async ( args : { directory : string ; pattern : string ; useRegex : boolean } ) => {
508658 try {
509659 const { directory, pattern, useRegex } = args ;
510- const results : string [ ] = [ ] ;
511-
512- // 递归搜索函数
513- const searchDir = async ( currentDir : string ) => {
514- const files = await fs . promises . readdir ( currentDir ) ;
660+ const queue : string [ ] = [ directory ] ;
661+ const results : string [ ] = [ ] ; // 声明结果数组 <--- 修复点
662+
663+ while ( queue . length > 0 ) {
664+ const currentDir = queue . shift ( ) ! ;
665+ const files = await fs . promises . readdir ( currentDir , { withFileTypes : true } ) ;
666+
515667 for ( const file of files ) {
516- const filePath = path . join ( currentDir , file ) ;
517- const stat = await fs . promises . stat ( filePath ) ;
518- if ( stat . isDirectory ( ) ) {
519- await searchDir ( filePath ) ; // 递归搜索子目录
520- } else {
521- const fileName = path . basename ( filePath ) ;
522- if ( useRegex ) {
523- const regex = new RegExp ( pattern ) ;
524- if ( regex . test ( fileName ) ) {
525- results . push ( filePath ) ;
526- }
668+ try {
669+ const filePath = path . join ( currentDir , file . name ) ;
670+
671+ if ( file . isDirectory ( ) ) {
672+ queue . push ( filePath ) ;
527673 } else {
528- if ( fileName === pattern ) {
529- results . push ( filePath ) ;
674+ // 严格匹配模式
675+ if ( ! useRegex ) {
676+ if ( file . name === pattern ) {
677+ return filePath ; // 直接返回首个匹配项
678+ }
679+ }
680+ // 正则表达式模式
681+ else {
682+ const regex = new RegExp ( pattern ) ;
683+ if ( regex . test ( file . name ) ) {
684+ results . push ( filePath ) ; // 收集所有匹配项
685+ }
530686 }
531687 }
688+ } catch ( error ) {
689+ continue ;
532690 }
533691 }
534- } ;
535-
536- await searchDir ( directory ) ;
537- return results . join ( '\n' ) ;
692+ }
693+
694+ // 根据模式返回不同结果
695+ return useRegex
696+ ? results . join ( '\n' ) // 正则模式返回所有结果
697+ : '' ; // 严格模式未找到返回空
538698 } catch ( error : any ) {
539- return `搜索文件失败 : ${ error . message } ` ;
699+ return `搜索失败 : ${ error . message } ` ;
540700 }
541701 } ,
542702} ;
703+ registerTool ( findFiles ) ;
543704
544705// 修改后的writeMemory工具(移除本地嵌入生成和存储)
545706export const writeMemory : Tool = {
0 commit comments