feat: 更新 @kevisual/router 依赖至 0.1.2,增强 git 路径解析功能并优化项目搜索逻辑

This commit is contained in:
xiongxiao
2026-03-15 20:57:40 +08:00
committed by cnb
parent 609a0987e9
commit 2de68a9960
4 changed files with 81 additions and 11 deletions

View File

@@ -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",

View File

@@ -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;

View File

@@ -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 格式是否正确');
} }

View File

@@ -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);