@@ -13,7 +13,64 @@ app
metadata : {
args : {
q : z.string ( ) . optional ( ) . describe ( '搜索关键词,选填;留空或不传则返回全部文件' ) ,
projectPath : z.string( ) . optional ( ) . describe( '按项目根目录路径过滤,仅返回该项目下的文件,选填') ,
// projectPath: z.string().optional(). describe( '按项目根目录路径过滤,仅返回该项目下的文件,选填'),
filepath : z.string ( ) . optional ( ) . describe ( '按文件绝对路径过滤,选填' ) ,
repo : z.string ( ) . optional ( ) . describe ( '按代码仓库标识过滤(如 owner/repo) , 选填' ) ,
title : z.string ( ) . optional ( ) . describe ( '按人工标注的标题字段过滤,选填' ) ,
tags : z.array ( z . string ( ) ) . optional ( ) . describe ( '按人工标注的标签列表过滤,选填' ) ,
summary : z.string ( ) . optional ( ) . describe ( '按人工标注的摘要字段过滤,选填' ) ,
description : z.string ( ) . optional ( ) . describe ( '按人工标注的描述字段过滤,选填' ) ,
link : z.string ( ) . optional ( ) . describe ( '按人工标注的外部链接字段过滤,选填' ) ,
sort : z.array ( z . string ( ) ) . optional ( ) . describe ( '排序规则数组,格式为 ["字段:asc"] 或 ["字段:desc"],选填,当 q 为空时默认为 ["projectPath:asc"]' ) ,
limit : z.number ( ) . optional ( ) . describe ( '返回结果数量上限,选填,当 q 为空时默认为 1000' ) ,
getContent : z.boolean ( ) . optional ( ) . describe ( '是否返回文件内容,默认为 false; 如果为 true, 则在结果中包含 content 字段,内容以 base64 编码返回,适用于前端预览或下载场景' ) ,
projects : z.array ( z . string ( ) ) . optional ( ) . describe ( '按项目名称列表过滤,选填,默认不穿,只过滤当前工作区的项目' ) ,
}
}
} )
. define ( async ( ctx ) = > {
type SearchQuery = { q? : string ; projectPath? : string ; filepath? : string ; repo? : string ; title? : string ; tags? : string [ ] ; summary? : string ; description? : string ; link? : string ; sort? : string [ ] ; limit? : number ; getContent? : boolean ; projects? : string [ ] }
let { q , projectPath , filepath , repo , title , tags , summary , description , link , sort , limit , getContent = false , projects } = ctx . query as SearchQuery ;
if ( ! q ) {
sort = sort ? ? [ 'projectPath:asc' ] ;
limit = limit ? ? 1000 ;
}
let hits : any [ ] = [ ] ;
const getOnlyProjects = async ( projectPaths : string [ ] ) = > {
if ( projectPaths . length === 0 ) return [ ] ;
let searchPromises = projectPaths . map ( projectPath = > manager . projectSearch . searchFiles ( q , { projectPath , filepath , repo , title , tags , summary , description , link , sort , limit , getContent } ) ) ;
const results = await Promise . all ( searchPromises ) ;
return results . flat ( ) ;
}
if ( ! projects || projects . length === 0 ) {
// 如果没有指定项目名称列表,则默认搜索当前工作区的所有项目
const projects = await manager . projectStore . listProjects ( ) ;
const projectPaths = projects . filter ( p = > p . status === 'active' ) . map ( p = > p . path ) ;
hits = await getOnlyProjects ( projectPaths ) ;
} else {
// 如果指定了项目名称列表,则只搜索这些项目, 同时确保这些项目在当前工作区中是存在的
const _projects = await manager . projectStore . listProjects ( ) ;
let _hasProjects = projects . filter ( p = > _projects . some ( _p = > _p . path === p ) ) ;
hits = await getOnlyProjects ( _hasProjects ) ;
}
ctx . body = { list : hits } ;
} )
. addTo ( app ) ;
/**
* 搜索文件
* query: { q, projectPath?, repo? }
*/
app
. route ( {
path : 'project-search' ,
key : 'search' ,
description : '在已索引的项目文件中执行全文搜索,支持按仓库、目录、标签等字段过滤,以及自定义排序和数量限制' ,
middleware : [ 'auth-admin' ] ,
metadata : {
args : {
q : z.string ( ) . optional ( ) . describe ( '搜索关键词,选填;留空或不传则返回全部文件' ) ,
projectPath : z.string ( ) . describe ( '按项目根目录路径过滤,仅返回该项目下的文件,必填' ) ,
filepath : z.string ( ) . optional ( ) . describe ( '按文件绝对路径过滤,选填' ) ,
repo : z.string ( ) . optional ( ) . describe ( '按代码仓库标识过滤(如 owner/repo) , 选填' ) ,
title : z.string ( ) . optional ( ) . describe ( '按人工标注的标题字段过滤,选填' ) ,
@@ -28,13 +85,24 @@ app
}
} )
. define ( async ( ctx ) = > {
let { q , projectPath , filepath , repo , title , tags , summary , description , link , sort , limit , getContent = false } = ctx . query as { q? : string ; projectPath? : string ; filepath? : string ; repo? : string ; title? : string ; tags? : string [ ] ; summary? : string ; description? : string ; link? : string ; sort? : string [ ] ; limit? : number ; getContent? : boolean };
type SearchQuery = { q? : string ; projectPath? : string ; filepath? : string ; repo? : string ; title? : string ; tags? : string [ ] ; summary? : string ; description? : string ; link? : string ; sort? : string [ ] ; limit? : number ; getContent? : boolean ; projects? : string [ ] }
let { q , projectPath , filepath , repo , title , tags , summary , description , link , sort , limit , getContent = false , projects } = ctx . query as SearchQuery ;
if ( ! q ) {
sort = sort ? ? [ 'projectPath:asc' ] ;
limit = limit ? ? 1000 ;
}
const projectSearch = manager . projectSearch ;
const hits = await projectSearch . searchFiles ( q , { projectPath , filepath , repo , title , tags , summary , description , link , sort , limit , getContent } ) ;
if ( ! projectPath ) {
ctx . throw ( 400 , 'projectPath 参数不能为空' ) ;
return ;
}
let hits : any [ ] = [ ] ;
const getOnlyProjects = async ( projectPaths : string [ ] ) = > {
if ( projectPaths . length === 0 ) return [ ] ;
let searchPromises = projectPaths . map ( projectPath = > manager . projectSearch . searchFiles ( q , { projectPath , filepath , repo , title , tags , summary , description , link , sort , limit , getContent } ) ) ;
const results = await Promise . all ( searchPromises ) ;
return results . flat ( ) ;
}
hits = await getOnlyProjects ( [ projectPath ! ] ) ;
ctx . body = { list : hits } ;
} )
. addTo ( app ) ;