ai-helper-agent 1.13.0 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/search.js +20 -7
- package/src/security.js +16 -5
- package/src/server.js +52 -0
- package/src/skill/executor.js +1 -0
package/package.json
CHANGED
package/src/search.js
CHANGED
|
@@ -231,13 +231,21 @@ export async function searchFiles(rootPath, filePattern = '*', recursive = true,
|
|
|
231
231
|
/**
|
|
232
232
|
* 使用 rg 搜索文件内容(快速)
|
|
233
233
|
*/
|
|
234
|
-
function searchContentWithRg(rootPath, pattern, filePattern, maxResults, contextLines) {
|
|
234
|
+
function searchContentWithRg(rootPath, pattern, filePattern, caseSensitive, recursive, maxResults, contextLines) {
|
|
235
235
|
return new Promise((resolve) => {
|
|
236
236
|
const args = [
|
|
237
237
|
'--no-heading', '--line-number',
|
|
238
238
|
'--max-count', String(maxResults),
|
|
239
239
|
'-C', String(contextLines)
|
|
240
240
|
];
|
|
241
|
+
// 大小写不敏感时添加 -i(与 searchContentNative 的 caseSensitive 语义对齐)
|
|
242
|
+
if (!caseSensitive) {
|
|
243
|
+
args.push('-i');
|
|
244
|
+
}
|
|
245
|
+
// 非递归搜索:仅搜索直接子文件,不进入子目录(与 fd --max-depth 1 语义一致)
|
|
246
|
+
if (!recursive) {
|
|
247
|
+
args.push('--max-depth', '1');
|
|
248
|
+
}
|
|
241
249
|
if (filePattern) {
|
|
242
250
|
args.push('--glob', filePattern);
|
|
243
251
|
}
|
|
@@ -255,10 +263,12 @@ function searchContentWithRg(rootPath, pattern, filePattern, maxResults, context
|
|
|
255
263
|
|
|
256
264
|
proc.stdout.on('data', (chunk) => stdout.push(chunk));
|
|
257
265
|
|
|
258
|
-
proc.on('close', (
|
|
266
|
+
proc.on('close', () => {
|
|
259
267
|
clearTimeout(timeout);
|
|
260
268
|
if (timedOut) return;
|
|
261
|
-
|
|
269
|
+
// 不再按退出码过滤:Windows 下 rg 可能返回退出码 2(错误)但仍带有效匹配结果,
|
|
270
|
+
// 原逻辑 `code !== 0 && code !== 1` 会把这种情况的输出一并丢弃,导致有匹配却返回 0 条。
|
|
271
|
+
// 只要 stdout 有内容就解析;stdout 为空时 parseRgOutput 自然返回 []。
|
|
262
272
|
const output = Buffer.concat(stdout).toString('utf-8').trim();
|
|
263
273
|
resolve(parseRgOutput(output, contextLines));
|
|
264
274
|
});
|
|
@@ -298,7 +308,7 @@ function parseRgOutput(output, contextLines) {
|
|
|
298
308
|
/**
|
|
299
309
|
* Node.js 原生搜索文件内容(回退方案,异步 + 分片防止阻塞事件循环)
|
|
300
310
|
*/
|
|
301
|
-
async function searchContentNative(rootPath, pattern, filePattern, caseSensitive, maxResults, contextLines) {
|
|
311
|
+
async function searchContentNative(rootPath, pattern, filePattern, caseSensitive, recursive, maxResults, contextLines) {
|
|
302
312
|
const results = [];
|
|
303
313
|
const searchPattern = caseSensitive ? pattern : pattern.toLowerCase();
|
|
304
314
|
|
|
@@ -319,6 +329,8 @@ async function searchContentNative(rootPath, pattern, filePattern, caseSensitive
|
|
|
319
329
|
if (entry.isDirectory()) {
|
|
320
330
|
if (DEFAULT_IGNORE_DIRS.has(entry.name)) continue;
|
|
321
331
|
if (entry.name.startsWith('.')) continue;
|
|
332
|
+
// 非递归搜索:跳过子目录,仅搜索当前目录下的文件
|
|
333
|
+
if (!recursive) continue;
|
|
322
334
|
await walk(fullPath);
|
|
323
335
|
} else if (entry.isFile()) {
|
|
324
336
|
if (filePattern && !matchGlob(entry.name, filePattern)) continue;
|
|
@@ -366,11 +378,12 @@ async function searchContentNative(rootPath, pattern, filePattern, caseSensitive
|
|
|
366
378
|
* @param {string} pattern - 搜索文本
|
|
367
379
|
* @param {string} [filePattern] - 可选文件类型过滤,如 "*.js"
|
|
368
380
|
* @param {boolean} caseSensitive - 是否大小写敏感
|
|
381
|
+
* @param {boolean} recursive - 是否递归搜索子目录
|
|
369
382
|
* @param {number} maxResults - 最大结果数
|
|
370
383
|
* @param {number} contextLines - 上下文行数
|
|
371
384
|
* @returns {{ success: boolean, results: Array, total: number, engine: string }}
|
|
372
385
|
*/
|
|
373
|
-
export async function searchContent(rootPath, pattern, filePattern = null, caseSensitive = false, maxResults = 100, contextLines = 2) {
|
|
386
|
+
export async function searchContent(rootPath, pattern, filePattern = null, caseSensitive = false, recursive = true, maxResults = 100, contextLines = 2) {
|
|
374
387
|
const pathCheck = await checkPath(rootPath);
|
|
375
388
|
if (!pathCheck.allowed) {
|
|
376
389
|
return { success: false, error: pathCheck.reason };
|
|
@@ -389,9 +402,9 @@ export async function searchContent(rootPath, pattern, filePattern = null, caseS
|
|
|
389
402
|
|
|
390
403
|
let results;
|
|
391
404
|
if (rgAvailable) {
|
|
392
|
-
results = await searchContentWithRg(resolved, pattern, filePattern, maxResults, contextLines);
|
|
405
|
+
results = await searchContentWithRg(resolved, pattern, filePattern, caseSensitive, recursive, maxResults, contextLines);
|
|
393
406
|
} else {
|
|
394
|
-
results = await searchContentNative(resolved, pattern, filePattern, caseSensitive, maxResults, contextLines);
|
|
407
|
+
results = await searchContentNative(resolved, pattern, filePattern, caseSensitive, recursive, maxResults, contextLines);
|
|
395
408
|
}
|
|
396
409
|
|
|
397
410
|
return {
|
package/src/security.js
CHANGED
|
@@ -8,12 +8,23 @@ const isWin32 = platform() === 'win32';
|
|
|
8
8
|
|
|
9
9
|
function normalizePathFormat(pathStr) {
|
|
10
10
|
if (!pathStr || typeof pathStr !== 'string') return pathStr;
|
|
11
|
-
//
|
|
12
|
-
//
|
|
11
|
+
// 1. 把异常的 "/C:/" 形式(带前导斜杠的盘符)规整为 "C:/"
|
|
12
|
+
// 仅匹配 /<字母>:/ ,不会误伤 /home/ 等普通路径段
|
|
13
13
|
pathStr = pathStr.replace(/\/([a-zA-Z]):(\/|$)/g, '$1:$2');
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
|
|
14
|
+
// 2. MSYS/Git Bash 风格路径:/d/Users/... → D:/Users/...
|
|
15
|
+
// 前导 / + 单字母盘符 + / ,无冒号,是 MSYS 特有的挂载点表示法
|
|
16
|
+
if (isWin32) {
|
|
17
|
+
pathStr = pathStr.replace(/^\/([a-zA-Z])\//, '$1:/');
|
|
18
|
+
}
|
|
19
|
+
// 3. 统一为正斜杠(path.resolve/normalize 在所有平台都能正确处理正斜杠并归一化到平台分隔符)
|
|
20
|
+
pathStr = pathStr.replace(/\\/g, '/');
|
|
21
|
+
// 4. Windows 盘符统一为大写:d:/ → D:/
|
|
22
|
+
// path.resolve 保留盘符原始大小写,而白名单前缀比较是大小写敏感的字符串比较,
|
|
23
|
+
// 小写盘符路径与大写盘符白名单前缀比较会失败(d:\Users\.startsWith(D:\Users\) → false)
|
|
24
|
+
if (isWin32) {
|
|
25
|
+
pathStr = pathStr.replace(/^([a-zA-Z]):/, m => m.toUpperCase());
|
|
26
|
+
}
|
|
27
|
+
return pathStr;
|
|
17
28
|
}
|
|
18
29
|
|
|
19
30
|
// ==================== 硬阻止目录(任何情况下都不可访问) ====================
|
package/src/server.js
CHANGED
|
@@ -844,6 +844,57 @@ export function startServer() {
|
|
|
844
844
|
}
|
|
845
845
|
}
|
|
846
846
|
|
|
847
|
+
// === 移除允许访问的目录(需认证,不可移除当前工作目录) ===
|
|
848
|
+
if (pathname === '/api/config/allowed-paths/remove' && req.method === 'POST') {
|
|
849
|
+
const targetPath = body.path;
|
|
850
|
+
if (!targetPath || typeof targetPath !== 'string') {
|
|
851
|
+
return jsonResponse(res, 400, { success: false, error: '缺少 path 参数' });
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
let resolvedPath = targetPath.trim();
|
|
855
|
+
if (resolvedPath === '~') {
|
|
856
|
+
resolvedPath = homedir();
|
|
857
|
+
} else if (resolvedPath.startsWith('~/')) {
|
|
858
|
+
resolvedPath = join(homedir(), resolvedPath.slice(2));
|
|
859
|
+
}
|
|
860
|
+
if (!isAbsolute(resolvedPath)) {
|
|
861
|
+
return jsonResponse(res, 400, { success: false, error: 'path 必须是绝对路径' });
|
|
862
|
+
}
|
|
863
|
+
resolvedPath = resolve(resolvedPath);
|
|
864
|
+
|
|
865
|
+
try {
|
|
866
|
+
const freshConfig = loadConfig();
|
|
867
|
+
const currentWorkdir = resolve(freshConfig.workdir || '');
|
|
868
|
+
// 禁止移除当前工作目录
|
|
869
|
+
if (resolvedPath === currentWorkdir) {
|
|
870
|
+
return jsonResponse(res, 400, { success: false, error: '不能移除当前工作目录,请先切换到其它目录' });
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
const allowedPaths = Array.isArray(freshConfig.allowedPaths) ? freshConfig.allowedPaths : [];
|
|
874
|
+
// 跨平台路径比较:统一分隔符 + 大小写不敏感
|
|
875
|
+
const norm = s => s.replace(/\\/g, '/').toLowerCase();
|
|
876
|
+
const filtered = allowedPaths.filter(p => norm(p) !== norm(resolvedPath));
|
|
877
|
+
|
|
878
|
+
// 没有变化说明该路径不在列表中
|
|
879
|
+
if (filtered.length === allowedPaths.length) {
|
|
880
|
+
return jsonResponse(res, 200, { success: true, allowedPaths: filtered, message: '该目录不在允许列表中' });
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
const updatedConfig = { ...freshConfig, allowedPaths: filtered };
|
|
884
|
+
await saveConfig(updatedConfig);
|
|
885
|
+
|
|
886
|
+
logSystem('allowed_path_removed', { path: resolvedPath, extId });
|
|
887
|
+
return jsonResponse(res, 200, {
|
|
888
|
+
success: true,
|
|
889
|
+
allowedPaths: updatedConfig.allowedPaths,
|
|
890
|
+
message: '已从允许列表中移除'
|
|
891
|
+
});
|
|
892
|
+
} catch (err) {
|
|
893
|
+
logError('config', 'allowed_path_remove_error', { path: resolvedPath, error: err.message });
|
|
894
|
+
return jsonResponse(res, 500, { success: false, error: `移除允许目录失败: ${err.message}` });
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
847
898
|
// === 文件操作 ===
|
|
848
899
|
|
|
849
900
|
// 搜索文件(按文件名模式)
|
|
@@ -877,6 +928,7 @@ export function startServer() {
|
|
|
877
928
|
body.pattern,
|
|
878
929
|
body.filePattern || null,
|
|
879
930
|
body.caseSensitive || false,
|
|
931
|
+
body.recursive !== false,
|
|
880
932
|
maxResults,
|
|
881
933
|
body.contextLines !== undefined ? body.contextLines : 2
|
|
882
934
|
);
|
package/src/skill/executor.js
CHANGED