@@ -72,6 +72,7 @@ const definitionFillContentNodeLimit = 20
7272const definitionFillContentLineLimit = 200
7373const DefaultMaxCodeSnippetLines = 500
7474const DefaultMaxCodeSnippets = 200
75+ const maxSignatureLength = 200
7576
7677// NewCodebaseService 创建新的代码库服务
7778func NewCodebaseService (
@@ -631,6 +632,42 @@ func (l *codebaseService) DeleteIndex(ctx context.Context, req *dto.DeleteIndexR
631632 return nil
632633}
633634
635+ func (s * codebaseService ) GetFileSkeleton (ctx context.Context , req * dto.GetFileSkeletonRequest ) (* dto.FileSkeletonData , error ) {
636+ // 1. 参数校验
637+ if req .WorkspacePath == "" || req .FilePath == "" {
638+ return nil , errs .NewMissingParamError ("workspacePath or filePath" )
639+ }
640+
641+ // 2. 路径处理(相对/绝对)
642+ filePath := req .FilePath
643+ if ! filepath .IsAbs (filePath ) {
644+ filePath = filepath .Join (req .WorkspacePath , filePath )
645+ }
646+
647+ // 验证路径是否在 workspace 内
648+ if err := s .checkPath (ctx , req .WorkspacePath , []string {filePath }); err != nil {
649+ return nil , err
650+ }
651+
652+ // 3. 获取原始 FileElementTable
653+ table , err := s .indexer .GetFileElementTable (ctx , req .WorkspacePath , filePath )
654+ if err != nil {
655+ return nil , fmt .Errorf ("failed to get file element table: %w" , err )
656+ }
657+
658+ // 4. 读取文件内容(用于提取签名和还原imports)
659+ fileContent , err := s .workspaceReader .ReadFile (ctx , filePath , types.ReadOptions {})
660+ if err != nil {
661+ s .logger .Warn ("failed to read file content for %s: %v" , filePath , err )
662+ fileContent = nil // 继续处理,但签名和imports还原会失败
663+ }
664+
665+ // 5. 转换数据结构
666+ result := convertToFileSkeletonData (table , fileContent , req .FilteredBy )
667+
668+ return result , nil
669+ }
670+
634671func convertStatus (status int ) string {
635672 var indexStatus string
636673 switch status {
@@ -645,3 +682,131 @@ func convertStatus(status int) string {
645682 }
646683 return indexStatus
647684}
685+
686+ // convertToFileSkeletonData 转换 FileElementTable 到 FileSkeletonData
687+ func convertToFileSkeletonData (
688+ table * codegraphpb.FileElementTable ,
689+ fileContent []byte ,
690+ filteredBy string ,
691+ ) * dto.FileSkeletonData {
692+ // 按行分割文件内容(KISS原则)
693+ var lines []string
694+ if fileContent != nil {
695+ lines = strings .Split (string (fileContent ), "\n " )
696+ }
697+
698+ // 转换 imports
699+ imports := make ([]* dto.FileSkeletonImport , 0 , len (table .Imports ))
700+ for _ , imp := range table .Imports {
701+ content := restoreImportContent (lines , imp .Range )
702+ imports = append (imports , & dto.FileSkeletonImport {
703+ Content : content ,
704+ Range : convertRange (imp .Range ),
705+ })
706+ }
707+
708+ // 转换 package
709+ var pkg * dto.FileSkeletonPackage
710+ if table .Package != nil {
711+ pkg = & dto.FileSkeletonPackage {
712+ Name : table .Package .Name ,
713+ Range : convertRange (table .Package .Range ),
714+ }
715+ }
716+
717+ // 转换和过滤 elements
718+ elements := make ([]* dto.FileSkeletonElement , 0 , len (table .Elements ))
719+ for _ , elem := range table .Elements {
720+ // 根据 filteredBy 参数过滤
721+ if filteredBy == "definition" && ! elem .IsDefinition {
722+ continue
723+ }
724+ if filteredBy == "reference" && elem .IsDefinition {
725+ continue
726+ }
727+
728+ // 提取签名
729+ signature := extractSignature (lines , elem .Range , maxSignatureLength )
730+ if signature == "" {
731+ signature = elem .Name // 如果为空则使用 name
732+ }
733+
734+ elements = append (elements , & dto.FileSkeletonElement {
735+ Name : elem .Name ,
736+ Signature : signature ,
737+ IsDefinition : elem .IsDefinition ,
738+ ElementType : convertElementType (elem .ElementType ),
739+ Range : convertRange (elem .Range ),
740+ })
741+ }
742+
743+ return & dto.FileSkeletonData {
744+ Path : table .Path ,
745+ Language : table .Language ,
746+ Timestamp : table .Timestamp ,
747+ Imports : imports ,
748+ Package : pkg ,
749+ Elements : elements ,
750+ }
751+ }
752+
753+ // extractSignature 提取签名(从指定行读取,限制长度)
754+ func extractSignature (lines []string , ranges []int32 , maxLength int ) string {
755+ if len (ranges ) < 1 || lines == nil || len (lines ) == 0 {
756+ return ""
757+ }
758+ lineNumber := int (ranges [0 ]) // 0-based
759+ if lineNumber < 0 || lineNumber >= len (lines ) {
760+ return ""
761+ }
762+ line := strings .TrimSpace (lines [lineNumber ])
763+ if len (line ) > maxLength {
764+ return line [:maxLength ]
765+ }
766+ return line
767+ }
768+
769+ // restoreImportContent 还原 import 的原始内容
770+ func restoreImportContent (lines []string , ranges []int32 ) string {
771+ if len (ranges ) < 3 || lines == nil {
772+ return ""
773+ }
774+ startLine := int (ranges [0 ]) // 0-based
775+ endLine := int (ranges [2 ]) // 0-based
776+
777+ if startLine < 0 || endLine >= len (lines ) || startLine > endLine {
778+ return ""
779+ }
780+
781+ // 单行import
782+ if startLine == endLine {
783+ return strings .TrimSpace (lines [startLine ])
784+ }
785+
786+ // 多行import(合并为一行)
787+ var builder strings.Builder
788+ for i := startLine ; i <= endLine && i < len (lines ); i ++ {
789+ trimmed := strings .TrimSpace (lines [i ])
790+ if trimmed != "" {
791+ builder .WriteString (trimmed )
792+ if i < endLine {
793+ builder .WriteString (" " )
794+ }
795+ }
796+ }
797+ return builder .String ()
798+ }
799+
800+ // convertElementType 转换 ElementType 枚举到字符串名称
801+ func convertElementType (et codegraphpb.ElementType ) string {
802+ return codegraphpb .ElementType_name [int32 (et )]
803+ }
804+
805+ // convertRange 转换 range(+1 转换:0-based -> 1-based)
806+ func convertRange (protoRange []int32 ) []int {
807+ result := make ([]int , len (protoRange ))
808+ for i , v := range protoRange {
809+ result [i ] = int (v ) + 1
810+ }
811+ return result
812+ }
0 commit comments