109 lines
6.5 KiB
TypeScript
109 lines
6.5 KiB
TypeScript
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);
|