Files
project-search/src/routes/search.ts

109 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);