feat: 更新 @kevisual/router 依赖至 0.1.2,增强 git 路径解析功能并优化项目搜索逻辑
This commit is contained in:
@@ -1,18 +1,20 @@
|
||||
import { execSync } from 'node:child_process'
|
||||
import path from 'node:path';
|
||||
|
||||
export const getPathnameFromGitUrl = (url: string): string => {
|
||||
export const getPathnameFromGitUrl = (url: string): { pathname: string, filename: string } => {
|
||||
const _url = new URL(url.replace(/\.git$/, ''));
|
||||
const pathname = _url.pathname.replace(/^\/+/, '').replace(/\/+$/, ''); // 去除开头和结尾的斜杠
|
||||
return pathname;
|
||||
const _pathname = _url.pathname;
|
||||
const pathname = _pathname.replace(/^\/+/, '').replace(/\/+$/, ''); // 去除开头和结尾的斜杠
|
||||
const filename = path.basename(_pathname);
|
||||
return { pathname, filename };
|
||||
}
|
||||
export const getGitPathname = (repoPath: string): { url: string, pathname: string } | null => {
|
||||
export const getGitPathname = (repoPath: string): { url: string, pathname: string, filename: string } | null => {
|
||||
try {
|
||||
const url = execSync('git config --get remote.origin.url', { cwd: repoPath, encoding: 'utf-8' }).trim();
|
||||
if (url) {
|
||||
return {
|
||||
url,
|
||||
pathname: getPathnameFromGitUrl(url),
|
||||
...getPathnameFromGitUrl(url),
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -112,7 +112,7 @@ app.route({
|
||||
} else {
|
||||
ctx.throw(400, 'repo 参数不能为空,且必须是完整的 URL 或者 owner/repo 格式');
|
||||
}
|
||||
const name = getPathnameFromGitUrl(cloneRepoUrl);
|
||||
const { filename: name } = getPathnameFromGitUrl(cloneRepoUrl);
|
||||
if (!name) {
|
||||
ctx.throw(400, '无法从 repo 参数解析出项目名称,请检查 repo 格式是否正确');
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user