feat: 更新 @kevisual/router 依赖至 0.1.2,增强 git 路径解析功能并优化项目搜索逻辑
This commit is contained in:
@@ -20,7 +20,7 @@
|
|||||||
"@kevisual/context": "^0.0.8",
|
"@kevisual/context": "^0.0.8",
|
||||||
"@kevisual/dts": "^0.0.4",
|
"@kevisual/dts": "^0.0.4",
|
||||||
"@kevisual/remote-app": "^0.0.7",
|
"@kevisual/remote-app": "^0.0.7",
|
||||||
"@kevisual/router": "^0.1.1",
|
"@kevisual/router": "^0.1.2",
|
||||||
"es-toolkit": "^1.45.1",
|
"es-toolkit": "^1.45.1",
|
||||||
"eventemitter3": "^5.0.4",
|
"eventemitter3": "^5.0.4",
|
||||||
"fast-glob": "^3.3.3",
|
"fast-glob": "^3.3.3",
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
import { execSync } from 'node:child_process'
|
import { execSync } from 'node:child_process'
|
||||||
import path from 'node:path';
|
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 _url = new URL(url.replace(/\.git$/, ''));
|
||||||
const pathname = _url.pathname.replace(/^\/+/, '').replace(/\/+$/, ''); // 去除开头和结尾的斜杠
|
const _pathname = _url.pathname;
|
||||||
return 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 {
|
try {
|
||||||
const url = execSync('git config --get remote.origin.url', { cwd: repoPath, encoding: 'utf-8' }).trim();
|
const url = execSync('git config --get remote.origin.url', { cwd: repoPath, encoding: 'utf-8' }).trim();
|
||||||
if (url) {
|
if (url) {
|
||||||
return {
|
return {
|
||||||
url,
|
url,
|
||||||
pathname: getPathnameFromGitUrl(url),
|
...getPathnameFromGitUrl(url),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ app.route({
|
|||||||
} else {
|
} else {
|
||||||
ctx.throw(400, 'repo 参数不能为空,且必须是完整的 URL 或者 owner/repo 格式');
|
ctx.throw(400, 'repo 参数不能为空,且必须是完整的 URL 或者 owner/repo 格式');
|
||||||
}
|
}
|
||||||
const name = getPathnameFromGitUrl(cloneRepoUrl);
|
const { filename: name } = getPathnameFromGitUrl(cloneRepoUrl);
|
||||||
if (!name) {
|
if (!name) {
|
||||||
ctx.throw(400, '无法从 repo 参数解析出项目名称,请检查 repo 格式是否正确');
|
ctx.throw(400, '无法从 repo 参数解析出项目名称,请检查 repo 格式是否正确');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,64 @@ app
|
|||||||
metadata: {
|
metadata: {
|
||||||
args: {
|
args: {
|
||||||
q: z.string().optional().describe('搜索关键词,选填;留空或不传则返回全部文件'),
|
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('按文件绝对路径过滤,选填'),
|
filepath: z.string().optional().describe('按文件绝对路径过滤,选填'),
|
||||||
repo: z.string().optional().describe('按代码仓库标识过滤(如 owner/repo),选填'),
|
repo: z.string().optional().describe('按代码仓库标识过滤(如 owner/repo),选填'),
|
||||||
title: z.string().optional().describe('按人工标注的标题字段过滤,选填'),
|
title: z.string().optional().describe('按人工标注的标题字段过滤,选填'),
|
||||||
@@ -28,13 +85,24 @@ app
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.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) {
|
if (!q) {
|
||||||
sort = sort ?? ['projectPath:asc'];
|
sort = sort ?? ['projectPath:asc'];
|
||||||
limit = limit ?? 1000;
|
limit = limit ?? 1000;
|
||||||
}
|
}
|
||||||
const projectSearch = manager.projectSearch;
|
if (!projectPath) {
|
||||||
const hits = await projectSearch.searchFiles(q, { projectPath, filepath, repo, title, tags, summary, description, link, sort, limit, getContent });
|
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 };
|
ctx.body = { list: hits };
|
||||||
})
|
})
|
||||||
.addTo(app);
|
.addTo(app);
|
||||||
|
|||||||
Reference in New Issue
Block a user