import { app, manager } from '../app.ts'; import { z } from 'zod' /** * 搜索文件 * query: { q, projectPath?, repo? } */ app .route({ path: 'project-search', key: 'files', description: '在已索引的项目文件中执行全文搜索,支持按仓库、目录、标签等字段过滤,以及自定义排序和数量限制', middleware: ['auth-admin'], metadata: { args: { q: 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('按人工标注的标题字段过滤,选填'), 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 编码返回,适用于前端预览或下载场景'), } } }) .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; } 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);