@zqagent/core 0.3.13 → 0.3.16

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.
Files changed (37) hide show
  1. package/dist/{TurnEngine-BZjUGic_.d.ts → TurnEngine-C8qqbilC.d.ts} +18 -3
  2. package/dist/{TurnEngine-CMGdcSdp.d.cts → TurnEngine-DKSczYaA.d.cts} +18 -3
  3. package/dist/{conflictGuard-BBOOIxxK.d.cts → conflictGuard-BuICnWEI.d.cts} +8 -0
  4. package/dist/{conflictGuard-BBOOIxxK.d.ts → conflictGuard-BuICnWEI.d.ts} +8 -0
  5. package/dist/index.cjs +3372 -1136
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.cts +221 -6
  8. package/dist/index.d.ts +221 -6
  9. package/dist/index.js +3391 -1155
  10. package/dist/index.js.map +1 -1
  11. package/dist/internal/engine/FileLogHook.d.cts +1 -1
  12. package/dist/internal/engine/FileLogHook.d.ts +1 -1
  13. package/dist/internal/update/controller.cjs +1 -1
  14. package/dist/internal/update/controller.js +1 -1
  15. package/dist/internal/update/platform.cjs +1 -1
  16. package/dist/internal/update/platform.js +1 -1
  17. package/dist/tools/diff.d.cts +1 -1
  18. package/dist/tools/diff.d.ts +1 -1
  19. package/dist/tools/explore.cjs +504 -127
  20. package/dist/tools/explore.cjs.map +1 -1
  21. package/dist/tools/explore.d.cts +1 -1
  22. package/dist/tools/explore.d.ts +1 -1
  23. package/dist/tools/explore.js +504 -127
  24. package/dist/tools/explore.js.map +1 -1
  25. package/dist/tools/files.cjs +267 -47
  26. package/dist/tools/files.cjs.map +1 -1
  27. package/dist/tools/files.d.cts +1 -1
  28. package/dist/tools/files.d.ts +1 -1
  29. package/dist/tools/files.js +267 -47
  30. package/dist/tools/files.js.map +1 -1
  31. package/dist/tools/search.cjs +103 -24
  32. package/dist/tools/search.cjs.map +1 -1
  33. package/dist/tools/search.d.cts +8 -2
  34. package/dist/tools/search.d.ts +8 -2
  35. package/dist/tools/search.js +103 -24
  36. package/dist/tools/search.js.map +1 -1
  37. package/package.json +6 -5
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/tools/files.ts","../../src/tools/pathSafety.ts"],"sourcesContent":["/**\n * 文件系统工具\n *\n * 提供 read_file / write_file / list_files 三个文件工具的实现,\n * 并注册到 ToolRegistry。所有工具都做了路径范围检查(限制在\n * 工作区内,防止通过 ../ 逃逸)。实现为工厂函数(makeXxx),\n * 以便为每个引擎实例绑定各自的工作区根目录。\n */\nimport * as fs from \"fs\";\nimport * as path from \"path\";\nimport { ToolRegistry, RiskClass, type ToolMetadata } from \"./ToolRegistry.js\";\nimport { resolveWithinRoot } from \"./pathSafety.js\";\nimport type { ConflictGuard } from \"./conflictGuard.js\";\n\n/** read_file 默认最大读取行数 */\nconst DEFAULT_MAX_LINES = 2000;\n/** 单次多文件读取的总行数上限(防止一次返回撑爆上下文) */\nconst MULTI_READ_MAX_TOTAL_LINES = 1500;\n/** 单行最大字符数(超长行截断,防止输出爆炸) */\nconst MAX_LINE_CHARS = 500;\n\n/**\n * read_file — 行号标注版 (对齐 Python 版)\n *\n * 返回带行号的内容,格式 '12|text',便于引用为 path:line;\n * 大文件通过 start_line / max_lines 窗口化阅读。\n *\n * 行号用最短分隔(`12|`)而不是 `padStart(6) + \\t`:行号前缀实测占\n * read_file 输出的 12.7%,每次迭代都要重发,瘦身属于纯无损节省。\n *\n * 也支持 paths 一次读取多个文件/区间:每次工具往返都要重发全部上下文,\n * 合并读取能直接减少迭代次数。\n */\nfunction makeReadFile(workspace: string, guard?: ConflictGuard) {\n const root = path.resolve(workspace);\n\n /** 读取单个窗口,返回结果对象(不抛错,失败转为 error 字段) */\n function readOne(\n filePath: string,\n startLine: number,\n maxLines: number,\n ): Record<string, unknown> {\n // 路径范围检查 + 存在性/文件类型检查。\n // 用解析后的真实路径做后续 fs 操作,避免校验后 symlink 组件被替换。\n const resolved = resolveWithinRoot(root, filePath);\n if (!resolved) {\n return { path: filePath, error: \"路径在工作区之外\" };\n }\n const target = resolved.real;\n if (!fs.existsSync(target) || !fs.statSync(target).isFile()) {\n return { path: filePath, error: `不是文件: ${filePath}` };\n }\n\n // 按窗口截取行,超长行截断并标注\n const selected: string[] = [];\n let total = 0;\n try {\n const content = fs.readFileSync(target, \"utf-8\");\n // 记录本会话读到的版本:后续写入若发现磁盘内容已变(别的会话改过),\n // 会被 ConflictGuard 拦下,而不是静默覆盖\n guard?.noteRead(target, content);\n const lines = content.split(\"\\n\");\n total = lines.length;\n for (let i = startLine - 1; i < Math.min(startLine - 1 + maxLines, lines.length); i++) {\n let text = lines[i];\n if (text.length > MAX_LINE_CHARS) {\n text = text.slice(0, MAX_LINE_CHARS) + \"… (line truncated)\";\n }\n selected.push(`${i + 1}|${text}`);\n }\n } catch (exc) {\n return { path: filePath, error: `读取失败: ${String(exc)}` };\n }\n\n const end = startLine + selected.length - 1;\n const result: Record<string, unknown> = {\n path: path.relative(resolved.root, target),\n start_line: startLine,\n end_line: end,\n total_lines: total,\n content: selected.join(\"\\n\"),\n };\n // 文件未读完时提示继续阅读的入口\n if (end < total) {\n result.note = `显示第 ${startLine}-${end} 行,共 ${total} 行;调用 read_file(start_line=${end + 1}) 继续阅读`;\n }\n return result;\n }\n\n return function read_file(args: Record<string, unknown>) {\n const startLine = Math.max(1, Number(args.start_line ?? 1));\n const maxLines = Math.min(Number(args.max_lines ?? DEFAULT_MAX_LINES), DEFAULT_MAX_LINES);\n\n // 多文件模式:一次读取多个文件,均受同一窗口参数约束\n const paths = Array.isArray(args.paths)\n ? args.paths.map((p) => String(p ?? \"\")).filter(Boolean)\n : [];\n if (paths.length > 0) {\n const files: Record<string, unknown>[] = [];\n let remainingLines = MULTI_READ_MAX_TOTAL_LINES;\n for (const filePath of paths) {\n const perFile = Math.min(maxLines, Math.max(1, remainingLines));\n const one = readOne(filePath, startLine, perFile);\n files.push(one);\n // 出错的文件不占预算;成功读取的按实际窗口扣减\n if (!one.error) remainingLines -= (one.content as string).split(\"\\n\").length;\n if (remainingLines <= 0) break;\n }\n return {\n files,\n count: files.length,\n ...(files.length < paths.length\n ? { note: `已达到单次多文件读取上限(共 ${MULTI_READ_MAX_TOTAL_LINES} 行),其余路径未读取` }\n : {}),\n };\n }\n\n const filePath = String(args.path ?? \"\");\n if (!filePath) {\n return { error: \"缺少 path(或 paths)参数\" };\n }\n return readOne(filePath, startLine, maxLines);\n };\n}\n\n/**\n * write_file — 路径范围检查 + UTF-8 写入\n *\n * 自动创建父目录;overwrite=false 时拒绝覆盖已存在文件。\n */\nfunction makeWriteFile(workspace: string, guard?: ConflictGuard) {\n const root = path.resolve(workspace);\n\n return function write_file(args: Record<string, unknown>) {\n const filePath = String(args.path ?? \"\");\n const content = String(args.content ?? \"\");\n const overwrite = args.overwrite !== false;\n\n // 路径范围 + 类型检查(不能写目录、默认不覆盖已有文件)\n const resolved = resolveWithinRoot(root, filePath);\n if (!resolved) {\n return { error: \"路径在工作区之外\" };\n }\n const target = resolved.real;\n if (fs.existsSync(target) && fs.statSync(target).isDirectory()) {\n return { error: \"路径是目录,不是文件\" };\n }\n if (fs.existsSync(target) && !overwrite) {\n return { error: \"文件已存在 (overwrite=false)\" };\n }\n // 只有本会话读过该文件时才校验:读过之后又被别的会话改动 → 拒绝覆盖\n const conflict = guard?.checkWrite(target, filePath);\n if (conflict) return { error: conflict.message };\n\n try {\n fs.mkdirSync(path.dirname(target), { recursive: true });\n fs.writeFileSync(target, content, \"utf-8\");\n guard?.noteWrite(target, content);\n } catch (exc) {\n return { error: `写入失败: ${String(exc)}` };\n }\n\n return path.relative(resolved.root, target);\n };\n}\n\n/**\n * list_files — 列出目录内容\n *\n * 跳过隐藏文件(. 开头)及 node_modules / __pycache__ / dist 等\n * 无意义目录;目录项以 / 后缀标识;结果按名称排序。\n */\nfunction makeListFiles(workspace: string) {\n const root = path.resolve(workspace);\n\n return function list_files(args: Record<string, unknown>) {\n const dirPath = String(args.path ?? \".\");\n const maxResults = Math.min(Number(args.max_results ?? 100), 100);\n const resolved = resolveWithinRoot(root, dirPath);\n if (!resolved) return { error: \"路径在工作区之外\" };\n const target = resolved.real;\n if (!fs.existsSync(target) || !fs.statSync(target).isDirectory()) {\n return { error: `不是目录: ${dirPath}` };\n }\n\n try {\n const entries = fs.readdirSync(target, { withFileTypes: true });\n const results: string[] = [];\n for (const e of entries) {\n if (results.length >= maxResults) break;\n if (e.name.startsWith(\".\")) continue;\n if (e.name === \"node_modules\" || e.name === \"__pycache__\" || e.name === \"dist\") continue;\n results.push(e.isDirectory() ? `${e.name}/` : e.name);\n }\n results.sort();\n return { path: path.relative(resolved.root, target), entries: results.slice(0, maxResults) };\n } catch (exc) {\n return { error: `列出文件失败: ${String(exc)}` };\n }\n };\n}\n\n/** replace_in_file — 精确文本替换 (对齐 Python 版 aisuite) */\nfunction makeReplaceInFile(workspace: string, guard?: ConflictGuard) {\n const root = path.resolve(workspace);\n\n return function replace_in_file(args: Record<string, unknown>) {\n const filePath = String(args.path ?? \"\");\n const oldStr = String(args.old ?? \"\");\n const newStr = String(args.new ?? \"\");\n const expectedReplacements = Math.max(1, Number(args.expected_replacements ?? 1));\n\n if (!oldStr) return { error: \"old 不能为空\" };\n\n const resolved = resolveWithinRoot(root, filePath);\n if (!resolved) return { error: \"路径在工作区之外\" };\n const target = resolved.real;\n if (!fs.existsSync(target) || !fs.statSync(target).isFile()) {\n return { error: `不是文件: ${filePath}` };\n }\n // 先看本会话读到的版本是否已过期(被别的会话改过)\n const conflict = guard?.checkWrite(target, filePath);\n if (conflict) return { error: conflict.message };\n\n try {\n const content = fs.readFileSync(target, \"utf-8\");\n const count = content.split(oldStr).length - 1;\n\n if (count !== expectedReplacements) {\n return { error: `期望 ${expectedReplacements} 处替换,实际找到 ${count} 处: ${filePath}` };\n }\n\n const updated = content.replaceAll(oldStr, newStr);\n fs.writeFileSync(target, updated, \"utf-8\");\n guard?.noteWrite(target, updated);\n return {\n path: path.relative(resolved.root, target),\n replacements: count,\n chars_before: content.length,\n chars_after: updated.length,\n };\n } catch (exc) {\n return { error: `替换失败: ${String(exc)}` };\n }\n };\n}\n\n/**\n * 注册只读文件工具(read_file / list_files)。\n *\n * 供 explore 子代理等「只读」场景使用,使声明的 schema 与实际能力一致。\n */\nexport function registerReadOnlyFileTools(\n registry: ToolRegistry,\n workspace: string,\n guard?: ConflictGuard,\n): void {\n const readFile = makeReadFile(workspace, guard);\n registry.register(\n (args) => readFile(args),\n { category: \"filesystem\", riskLevel: \"low\", capabilities: [\"read_file\"], requiresApproval: false },\n {\n type: \"function\",\n function: {\n name: \"read_file\",\n description: \"读取文本文件,返回带行号的内容 (格式: '12|text'),以便引用为 path:line。path 与 paths 至少传一个。大文件窗口化:传递 start_line 从上一次读取的位置继续。需要同时查看多个文件/区间时用 paths 一次读取,减少往返。只读。\",\n parameters: {\n type: \"object\",\n properties: {\n path: { type: \"string\", description: \"文件路径 (相对于工作区)\" },\n paths: {\n type: \"array\",\n items: { type: \"string\" },\n description: \"一次读取多个文件 (与 path 二选一);多个候选文件应一次读完,不要逐个发请求\",\n },\n start_line: { type: \"integer\", description: \"起始行号 (从1开始,默认1)\" },\n max_lines: { type: \"integer\", description: `最大行数 (默认 ${DEFAULT_MAX_LINES},多文件合计上限 ${MULTI_READ_MAX_TOTAL_LINES})` },\n },\n },\n },\n },\n );\n\n const listFiles = makeListFiles(workspace);\n registry.register(\n (args) => listFiles(args),\n { category: \"filesystem\", riskLevel: \"low\", capabilities: [\"list_files\"], requiresApproval: false },\n {\n type: \"function\",\n function: {\n name: \"list_files\",\n description: \"列出目录下的文件和子目录。跳过隐藏文件和 node_modules 等目录。\",\n parameters: {\n type: \"object\",\n properties: {\n path: { type: \"string\", description: \"目录路径 (相对于工作区,默认 '.')\" },\n max_results: { type: \"integer\", description: \"最大结果数 (默认 100)\" },\n },\n },\n },\n },\n );\n}\n\n/**\n * 注册文件工具到 ToolRegistry\n *\n * read_file / list_files 为低风险(无需审批);write_file / replace_in_file\n * 为中风险且 requiresApproval=true(由权限引擎决定是否需要审批)。\n */\nexport function registerFileTools(\n registry: ToolRegistry,\n workspace: string,\n guard?: ConflictGuard,\n): void {\n registerReadOnlyFileTools(registry, workspace, guard);\n\n const writeFile = makeWriteFile(workspace, guard);\n registry.register(\n (args) => writeFile(args),\n { category: \"filesystem\", riskLevel: \"medium\", capabilities: [\"write_file\"], requiresApproval: true },\n {\n type: \"function\",\n function: {\n name: \"write_file\",\n description: \"写入一个 UTF-8 文本文件。用于创建新文件或完全重写现有文件。\",\n parameters: {\n type: \"object\",\n properties: {\n path: { type: \"string\", description: \"文件路径 (相对于工作区)\" },\n content: { type: \"string\", description: \"文件内容 (UTF-8)\" },\n overwrite: { type: \"boolean\", description: \"是否覆盖已存在的文件 (默认 true)\" },\n },\n required: [\"path\", \"content\"],\n },\n },\n },\n );\n\n const replaceFile = makeReplaceInFile(workspace, guard);\n registry.register(\n (args) => replaceFile(args),\n { category: \"filesystem\", riskLevel: \"medium\", capabilities: [\"edit_file\"], requiresApproval: true },\n {\n type: \"function\",\n function: {\n name: \"replace_in_file\",\n description: \"在文件中精确替换一段文本。先按预期次数精确匹配 old 字符串,然后替换为 new。用于小范围精确编辑。\",\n parameters: {\n type: \"object\",\n properties: {\n path: { type: \"string\", description: \"文件路径 (相对于工作区)\" },\n old: { type: \"string\", description: \"要被替换的精确文本\" },\n new: { type: \"string\", description: \"替换后的新文本\" },\n expected_replacements: { type: \"integer\", description: \"期望的替换次数 (默认 1)\" },\n },\n required: [\"path\", \"old\", \"new\"],\n },\n },\n },\n );\n}\n","/**\n * 路径安全辅助函数\n *\n * 判断目标路径是否真正位于工作区根目录之内。\n *\n * 仅做字符串比较(path.relative)无法防御符号链接逃逸:\n * 工作区内若存在指向外部的 symlink,`workspace/link/secret.txt`\n * 的字符串前缀合法,但实际读写发生在链接目标(工作区外)。\n *\n * 因此这里用 fs.realpathSync 解析符号链接后再与根目录的 realpath\n * 比较。目标尚不存在(如 write_file 新建文件)时,向上回溯到最近\n * 已存在的祖先目录解析,再拼回剩余路径段。\n *\n * 校验通过时把解析后的真实路径一并返回:调用方必须用它(而非原始\n * 相对路径)做实际 fs 操作,否则「校验后、操作前」路径组件被替换为\n * 指向外部的 symlink 仍会逃逸(TOCTOU)。\n */\nimport * as fs from \"fs\";\nimport * as path from \"path\";\n\n/**\n * 判断 `path.relative(root, target)` 的结果是否落在 root 之内。\n *\n * 逃逸必然恰好是 `..` 或以 `..` + 分隔符开头,不能用 `startsWith(\"..\")`:\n * 工作区里名为 `..foo` 的合法条目会被误判为逃逸而拒绝。\n * 跨盘符时(Windows)path.relative 返回绝对路径,一并拒绝。\n *\n * 导出供自行做 `path.relative` 校验的调用方复用(如目录浏览新建目录、\n * git 面板的 repo / path 参数),避免各处重复写 `startsWith(\"..\")`。\n */\nexport function isInsideRelative(rel: string): boolean {\n return (\n !path.isAbsolute(rel) && rel !== \"..\" && !rel.startsWith(`..${path.sep}`)\n );\n}\n\n/** 是否为文件系统根(\"/\"、\"C:\\\\\") */\nfunction isFilesystemRoot(p: string): boolean {\n return path.parse(p).root === p;\n}\n\n/** 校验通过的路径:root 为工作区根的真实路径,real 为目标的真实路径 */\nexport interface ResolvedPath {\n root: string;\n real: string;\n}\n\n/**\n * 校验 target(相对 workspace 的路径)是否位于 workspace 内,并返回\n * 解析符号链接后的真实路径;不在工作区内时返回 null。\n *\n * - 解析符号链接:路径中任何 symlink 组件都会先 realpath 再校验;\n * broken symlink(存在但无法解析)一律拒绝。\n * - 目标不存在时(写新文件):回溯到最近存在的祖先做 realpath,\n * 再拼接剩余段,仍能捕获祖先链中的 symlink 逃逸。\n * - 工作区根若就是文件系统根(`WORKSPACE=/`):此时任何绝对路径都在\n * 其内,等同没有沙箱,直接判为越界。\n */\nexport function resolveWithinRoot(\n workspace: string,\n target: string,\n): ResolvedPath | null {\n const abs = path.resolve(workspace, target);\n\n let rootReal: string;\n try {\n rootReal = fs.realpathSync(workspace);\n } catch {\n // 根目录不存在(如权限引擎尚未初始化工作区):\n // 没有实际目录结构即无 symlink 逃逸面,退化为纯字符串路径比较。\n const rootAbs = path.resolve(workspace);\n if (!isInsideRelative(path.relative(rootAbs, abs))) return null;\n return { root: rootAbs, real: abs };\n }\n if (isFilesystemRoot(rootReal)) return null;\n\n let real: string;\n try {\n real = fs.realpathSync(abs);\n } catch {\n // abs 不存在:向上回溯找最近存在的祖先\n let probe = abs;\n const tail: string[] = [];\n for (;;) {\n // 组件存在(含 broken symlink)但 realpath 失败 → 无法解析,拒绝\n const exists = fs.existsSync(probe) || fs.lstatSync(probe, { throwIfNoEntry: false }) !== undefined;\n if (exists) {\n try {\n real = path.join(fs.realpathSync(probe), ...tail);\n break;\n } catch {\n return null;\n }\n }\n tail.unshift(path.basename(probe));\n const parent = path.dirname(probe);\n if (parent === probe) return null; // 已到文件系统根\n probe = parent;\n }\n }\n\n if (!isInsideRelative(path.relative(rootReal, real))) return null;\n return { root: rootReal, real };\n}\n\n/**\n * 判断 target 是否安全地位于 workspace 内(不需要真实路径时用这个)。\n *\n * 注意:删除类操作不应据此改用 realpath 去 unlink —— 那会删掉符号链接\n * 指向的目标而非链接本身;这类操作请用 resolveEntryWithinRoot。\n */\nexport function isWithinRoot(workspace: string, target: string): boolean {\n return resolveWithinRoot(workspace, target) !== null;\n}\n\n/**\n * 解析「目录项本身」的路径:父目录取 realpath,最后一段保持原样。\n *\n * 供 unlink / rename 等作用于条目自身的操作用:父目录不可被 symlink\n * 替换,而最后一段若是符号链接,被操作的是链接本身,不会被跟随。\n */\nexport function resolveEntryWithinRoot(\n workspace: string,\n target: string,\n): ResolvedPath | null {\n const abs = path.resolve(workspace, target);\n const parent = resolveWithinRoot(workspace, path.dirname(abs));\n if (!parent) return null;\n return { root: parent.root, real: path.join(parent.real, path.basename(abs)) };\n}\n"],"mappings":"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","names":["fs","path","filePath"]}
1
+ {"version":3,"sources":["../../src/tools/files.ts","../../src/tools/pathSafety.ts","../../src/paths.ts","../../src/engine/toolResultBudget.ts","../../src/tools/ignore.ts"],"sourcesContent":["/**\n * 文件系统工具\n *\n * 提供 read_file / write_file / list_files 三个文件工具的实现,\n * 并注册到 ToolRegistry。所有工具都做了路径范围检查(限制在\n * 工作区内,防止通过 ../ 逃逸)。实现为工厂函数(makeXxx),\n * 以便为每个引擎实例绑定各自的工作区根目录。\n */\nimport * as fs from \"fs\";\nimport * as path from \"path\";\nimport { createHash } from \"node:crypto\";\nimport { StringDecoder } from \"node:string_decoder\";\nimport { ToolRegistry, RiskClass, type ToolMetadata } from \"./ToolRegistry.js\";\nimport { resolveWithinRoot } from \"./pathSafety.js\";\nimport type { ConflictGuard } from \"./conflictGuard.js\";\nimport { toolResultBudget } from \"../engine/toolResultBudget.js\";\nimport { ALWAYS_SKIP_DIRS, makeIgnoreFilter } from \"./ignore.js\";\n\n/** read_file 默认最大读取行数 */\nconst DEFAULT_MAX_LINES = 2000;\n/** 单次多文件读取的总行数上限(防止一次返回撑爆上下文) */\nconst MULTI_READ_MAX_TOTAL_LINES = 1500;\n/** 单行最大字符数(超长行截断,防止输出爆炸) */\nconst MAX_LINE_CHARS = 500;\n/** 流式读取的分块大小 */\nconst READ_CHUNK_BYTES = 64 * 1024;\n/** 结果里留给元信息(路径 / 行号 / note)的字符余量 */\nconst RESULT_META_RESERVE = 800;\n\n/**\n * read_file — 行号标注版 (对齐 Python 版)\n *\n * 返回带行号的内容,格式 '12|text',便于引用为 path:line;\n * 大文件通过 start_line / max_lines 窗口化阅读。\n *\n * 行号用最短分隔(`12|`)而不是 `padStart(6) + \\t`:行号前缀实测占\n * read_file 输出的 12.7%,每次迭代都要重发,瘦身属于纯无损节省。\n *\n * 也支持 paths 一次读取多个文件/区间:每次工具往返都要重发全部上下文,\n * 合并读取能直接减少迭代次数。\n */\nfunction makeReadFile(workspace: string, guard?: ConflictGuard) {\n const root = path.resolve(workspace);\n\n /**\n * 流式读取一个行窗口。\n *\n * 两件事同时做,都不需要把整篇内容留在内存里:\n * 1. 只保留请求窗口内的行(超过字符预算即停止收集);\n * 2. 逐块喂 sha256,得到与 `contentDigest` 一致的内容指纹(写冲突检测用)。\n *\n * 早先是 `fs.readFileSync` 全量 + `split(\"\\n\")` 建成数组再切片:读一个 50MB\n * 的生成文件只取 10 行,也要付出 50MB 字符串 + 上百万元素数组的内存峰值。\n * 现在内存占用与文件大小无关,只在块内做一次分割。\n *\n * 注意 totalLines 仍需读完整个文件才能准确(行数是结果字段,模型据此决定\n * 要不要继续读),因此 I/O 仍是全量的;省下的是内存与 split 的 CPU。\n */\n function readWindow(\n target: string,\n startLine: number,\n maxLines: number,\n charBudget: number,\n onDigest: (digest: string) => void,\n ): { lines: string[]; totalLines: number; capped: boolean } {\n const fd = fs.openSync(target, \"r\");\n try {\n const decoder = new StringDecoder(\"utf-8\");\n const hash = createHash(\"sha256\");\n const buf = Buffer.allocUnsafe(READ_CHUNK_BYTES);\n const lines: string[] = [];\n /** 已完成的完整行数 */\n let lineNo = 0;\n /** 上一块末尾未结束的行 */\n let pending = \"\";\n let chars = 0;\n /** 已达字符预算:停止收集内容,但继续数行 */\n let capped = false;\n let pos = 0;\n\n const takeLine = (text: string): void => {\n lineNo += 1;\n // 超预算后仍然继续数行(total_lines 必须准确),只是不再收集内容。\n // 不另开「纯计数」分支:那样少一层状态,也避免末尾无换行时少算一行。\n if (capped || lineNo < startLine || lines.length >= maxLines) return;\n const clipped =\n text.length > MAX_LINE_CHARS\n ? text.slice(0, MAX_LINE_CHARS) + \"… (line truncated)\"\n : text;\n const rendered = `${lineNo}|${clipped}`;\n // +1 是换行符\n if (chars + rendered.length + 1 > charBudget) {\n capped = true;\n return;\n }\n chars += rendered.length + 1;\n lines.push(rendered);\n };\n\n for (;;) {\n const n = fs.readSync(fd, buf, 0, READ_CHUNK_BYTES, pos);\n if (n <= 0) break;\n pos += n;\n // 哈希喂解码后的文本(而非原始字节):与 contentDigest 对\n // readFileSync(target, \"utf-8\") 的结果保持一致,非法 UTF-8 时也不会漂移\n const decoded = decoder.write(buf.subarray(0, n));\n hash.update(decoded, \"utf8\");\n const text = pending + decoded;\n let from = 0;\n for (;;) {\n const idx = text.indexOf(\"\\n\", from);\n if (idx < 0) break;\n takeLine(text.slice(from, idx).replace(/\\r$/, \"\"));\n from = idx + 1;\n }\n pending = text.slice(from);\n }\n /*\n * 末行没有换行符收尾时也要算作一行。\n *\n * `decoder.end()` 的返回值必须一并喂给哈希:它是上一块被扣住的残字节\n * (多字节字符被分块边界切断时,或文件末尾就是半个字符)。漏掉它,\n * 大文件读侧的指纹就与 `contentDigest(readFileSync(..., \"utf-8\"))` 不一致,\n * 写回时会误报「别人改过这个文件」。\n */\n const rest = decoder.end();\n if (rest) hash.update(rest, \"utf8\");\n const tail = pending + rest;\n if (tail.length > 0) takeLine(tail.replace(/\\r$/, \"\"));\n\n onDigest(hash.digest(\"hex\").slice(0, 16));\n return { lines, totalLines: lineNo, capped };\n } finally {\n fs.closeSync(fd);\n }\n }\n\n /** 读取单个窗口,返回结果对象(不抛错,失败转为 error 字段) */\n function readOne(\n filePath: string,\n startLine: number,\n maxLines: number,\n charBudget: number,\n ): Record<string, unknown> {\n // 路径范围检查 + 存在性/文件类型检查。\n // 用解析后的真实路径做后续 fs 操作,避免校验后 symlink 组件被替换。\n const resolved = resolveWithinRoot(root, filePath);\n if (!resolved) {\n return { path: filePath, error: \"路径在工作区之外\" };\n }\n const target = resolved.real;\n if (!fs.existsSync(target) || !fs.statSync(target).isFile()) {\n return { path: filePath, error: `不是文件: ${filePath}` };\n }\n\n let read: { lines: string[]; totalLines: number; capped: boolean };\n try {\n read = readWindow(target, startLine, maxLines, charBudget, (digest) => {\n // 记录本会话读到的版本:后续写入若发现磁盘内容已变(别的会话改过),\n // 会被 ConflictGuard 拦下,而不是静默覆盖\n guard?.noteReadDigest(target, digest);\n });\n } catch (exc) {\n return { path: filePath, error: `读取失败: ${String(exc)}` };\n }\n\n const { lines: selected, totalLines: total, capped } = read;\n const end = startLine + selected.length - 1;\n const result: Record<string, unknown> = {\n path: path.relative(resolved.root, target),\n start_line: startLine,\n end_line: end,\n total_lines: total,\n content: selected.join(\"\\n\"),\n };\n // note 必须与**实际送达的内容**一致。\n //\n // 早先这里只按行数判断(end < total),但结果在进入上下文前还要过一道\n // toolResultBudget 的字符裁剪,实际送达的往往远少于 note 声称的行数——\n // 模型据此以为「前面那些行我已经看过了」,直接跳过中间大段内容。\n // 现在窗口本身就按同一份预算收敛,capped 即代表真实到达的边界。\n if (capped) {\n result.note =\n `已返回第 ${startLine}-${end} 行(本次已达单次读取上限约 ${charBudget} 字符,` +\n `共 ${total} 行);调用 read_file(start_line=${end + 1}) 继续阅读`;\n } else if (end < total) {\n result.note = `显示第 ${startLine}-${end} 行,共 ${total} 行;调用 read_file(start_line=${end + 1}) 继续阅读`;\n }\n return result;\n }\n\n return function read_file(args: Record<string, unknown>) {\n const startLine = Math.max(1, Number(args.start_line ?? 1));\n const requestedLines = Number(args.max_lines ?? DEFAULT_MAX_LINES);\n const maxLines = Number.isFinite(requestedLines)\n ? Math.min(Math.max(1, Math.floor(requestedLines)), DEFAULT_MAX_LINES)\n : DEFAULT_MAX_LINES;\n\n // 与 toolResultBudget 用同一份预算:窗口超限就收敛,而不是等结果被\n // 事后裁掉(那样 note 与实际内容会对不上)\n const fullBudget = Math.max(1000, toolResultBudget(\"read_file\") - RESULT_META_RESERVE);\n\n // 多文件模式:一次读取多个文件,均受同一窗口参数约束\n const paths = Array.isArray(args.paths)\n ? args.paths.map((p) => String(p ?? \"\")).filter(Boolean)\n : [];\n if (paths.length > 0) {\n // 多文件共享同一份预算,按文件数均分,避免第一个文件吃掉全部额度\n const perFileBudget = Math.max(1000, Math.floor(fullBudget / paths.length));\n const files: Record<string, unknown>[] = [];\n let remainingLines = MULTI_READ_MAX_TOTAL_LINES;\n for (const filePath of paths) {\n const perFile = Math.min(maxLines, Math.max(1, remainingLines));\n const one = readOne(filePath, startLine, perFile, perFileBudget);\n files.push(one);\n // 出错的文件不占预算;成功读取的按实际窗口扣减\n if (!one.error) remainingLines -= (one.content as string).split(\"\\n\").length;\n if (remainingLines <= 0) break;\n }\n return {\n files,\n count: files.length,\n ...(files.length < paths.length\n ? { note: `已达到单次多文件读取上限(共 ${MULTI_READ_MAX_TOTAL_LINES} 行),其余路径未读取` }\n : {}),\n };\n }\n\n const filePath = String(args.path ?? \"\");\n if (!filePath) {\n return { error: \"缺少 path(或 paths)参数\" };\n }\n return readOne(filePath, startLine, maxLines, fullBudget);\n };\n}\n\n/**\n * write_file — 路径范围检查 + UTF-8 写入\n *\n * 自动创建父目录;overwrite=false 时拒绝覆盖已存在文件。\n */\nfunction makeWriteFile(workspace: string, guard?: ConflictGuard) {\n const root = path.resolve(workspace);\n\n return function write_file(args: Record<string, unknown>) {\n const filePath = String(args.path ?? \"\");\n const content = String(args.content ?? \"\");\n const overwrite = args.overwrite !== false;\n\n // 路径范围 + 类型检查(不能写目录、默认不覆盖已有文件)\n const resolved = resolveWithinRoot(root, filePath);\n if (!resolved) {\n return { error: \"路径在工作区之外\" };\n }\n const target = resolved.real;\n if (fs.existsSync(target) && fs.statSync(target).isDirectory()) {\n return { error: \"路径是目录,不是文件\" };\n }\n if (fs.existsSync(target) && !overwrite) {\n return { error: \"文件已存在 (overwrite=false)\" };\n }\n // 只有本会话读过该文件时才校验:读过之后又被别的会话改动 → 拒绝覆盖\n const conflict = guard?.checkWrite(target, filePath);\n if (conflict) return { error: conflict.message };\n\n try {\n fs.mkdirSync(path.dirname(target), { recursive: true });\n fs.writeFileSync(target, content, \"utf-8\");\n guard?.noteWrite(target, content);\n } catch (exc) {\n return { error: `写入失败: ${String(exc)}` };\n }\n\n return path.relative(resolved.root, target);\n };\n}\n\n/** 递归列目录的默认/最大深度 */\nconst DEFAULT_LIST_DEPTH = 3;\nconst MAX_LIST_DEPTH = 6;\n/** 递归模式下的默认条目上限(非递归仍是 100) */\nconst DEFAULT_RECURSIVE_LIMIT = 400;\nconst MAX_RECURSIVE_LIMIT = 1000;\n\n/**\n * list_files — 列出目录内容\n *\n * 默认只列一层(保持既有行为);recursive=true 时按 max_depth 递归。\n * 跳过隐藏项、常见构建产物目录,并遵守工作区根目录的 `.gitignore`\n * (简化子集,见 {@link loadGitignore})。\n *\n * 递归的意义:大仓库里了解结构原先要靠模型一层层点开,每次 `list_files`\n * 都是一轮 LLM 往返(每轮重发全部上下文)。一次拿到有限深度的结构,\n * 能直接省掉好几轮。\n */\nfunction makeListFiles(workspace: string) {\n const root = path.resolve(workspace);\n\n return function list_files(args: Record<string, unknown>) {\n const dirPath = String(args.path ?? \".\");\n const recursive = args.recursive === true;\n const requestedMax = Number(args.max_results ?? (recursive ? DEFAULT_RECURSIVE_LIMIT : 100));\n const hardCap = recursive ? MAX_RECURSIVE_LIMIT : 100;\n const maxResults = Number.isFinite(requestedMax)\n ? Math.max(1, Math.min(Math.floor(requestedMax), hardCap))\n : hardCap;\n const requestedDepth = Number(args.max_depth ?? DEFAULT_LIST_DEPTH);\n const maxDepth = Number.isFinite(requestedDepth)\n ? Math.max(1, Math.min(Math.floor(requestedDepth), MAX_LIST_DEPTH))\n : DEFAULT_LIST_DEPTH;\n\n const resolved = resolveWithinRoot(root, dirPath);\n if (!resolved) return { error: \"路径在工作区之外\" };\n const target = resolved.real;\n if (!fs.existsSync(target) || !fs.statSync(target).isDirectory()) {\n return { error: `不是目录: ${dirPath}` };\n }\n\n // 相对路径一律以 resolved.root(已 realpath)为基准:macOS 上 /var 是\n // /private/var 的符号链接,若拿未解析的 root 去 relative 会算出一串 \"..\"\n const realRoot = resolved.root;\n // 忽略判定与 repo map 共用同一套规则(隐藏项 + 构建产物 + .gitignore)\n const isIgnored = makeIgnoreFilter(realRoot);\n\n try {\n if (!recursive) {\n const results: string[] = [];\n for (const e of fs.readdirSync(target, { withFileTypes: true })) {\n if (results.length >= maxResults) break;\n // 用相对工作区的路径做 gitignore 匹配(path 可能指向子目录)\n const rel = path.relative(realRoot, path.join(target, e.name)).split(path.sep).join(\"/\");\n if (isIgnored(rel, e.isDirectory())) continue;\n results.push(e.isDirectory() ? `${e.name}/` : e.name);\n }\n results.sort();\n return { path: path.relative(realRoot, target), entries: results.slice(0, maxResults) };\n }\n\n // 递归:广度优先,按层推进,深度有上限、条目有预算\n const results: string[] = [];\n let truncated = false;\n const queue: Array<{ dir: string; depth: number }> = [{ dir: target, depth: 1 }];\n while (queue.length > 0) {\n const { dir, depth } = queue.shift()!;\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(dir, { withFileTypes: true });\n } catch {\n continue;\n }\n // 同一层内先目录后文件、再按名字,读起来像一棵树\n entries.sort((a, b) => {\n if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? -1 : 1;\n return a.name.localeCompare(b.name, \"zh\");\n });\n for (const e of entries) {\n if (results.length >= maxResults) {\n truncated = true;\n break;\n }\n const full = path.join(dir, e.name);\n const rel = path.relative(realRoot, full).split(path.sep).join(\"/\");\n const isDir = e.isDirectory();\n if (isIgnored(rel, isDir)) continue;\n results.push(isDir ? `${rel}/` : rel);\n if (isDir && depth < maxDepth) queue.push({ dir: full, depth: depth + 1 });\n }\n if (truncated) break;\n }\n return {\n path: path.relative(realRoot, target),\n entries: results,\n ...(truncated\n ? {\n note:\n `已达到条目上限 ${maxResults},结果被截断;` +\n `请缩小 path 到具体子目录,或降低 max_depth(当前 ${maxDepth})`,\n }\n : {}),\n };\n } catch (exc) {\n return { error: `列出文件失败: ${String(exc)}` };\n }\n };\n}\n\n/** replace_in_file — 精确文本替换 (对齐 Python 版 aisuite) */\nfunction makeReplaceInFile(workspace: string, guard?: ConflictGuard) {\n const root = path.resolve(workspace);\n\n return function replace_in_file(args: Record<string, unknown>) {\n const filePath = String(args.path ?? \"\");\n const oldStr = String(args.old ?? \"\");\n const newStr = String(args.new ?? \"\");\n const expectedReplacements = Math.max(1, Number(args.expected_replacements ?? 1));\n\n if (!oldStr) return { error: \"old 不能为空\" };\n\n const resolved = resolveWithinRoot(root, filePath);\n if (!resolved) return { error: \"路径在工作区之外\" };\n const target = resolved.real;\n if (!fs.existsSync(target) || !fs.statSync(target).isFile()) {\n return { error: `不是文件: ${filePath}` };\n }\n // 先看本会话读到的版本是否已过期(被别的会话改过)\n const conflict = guard?.checkWrite(target, filePath);\n if (conflict) return { error: conflict.message };\n\n try {\n const content = fs.readFileSync(target, \"utf-8\");\n const count = content.split(oldStr).length - 1;\n\n if (count !== expectedReplacements) {\n return { error: `期望 ${expectedReplacements} 处替换,实际找到 ${count} 处: ${filePath}` };\n }\n\n const updated = content.replaceAll(oldStr, newStr);\n fs.writeFileSync(target, updated, \"utf-8\");\n guard?.noteWrite(target, updated);\n return {\n path: path.relative(resolved.root, target),\n replacements: count,\n chars_before: content.length,\n chars_after: updated.length,\n };\n } catch (exc) {\n return { error: `替换失败: ${String(exc)}` };\n }\n };\n}\n\n/**\n * 注册只读文件工具(read_file / list_files)。\n *\n * 供 explore 子代理等「只读」场景使用,使声明的 schema 与实际能力一致。\n */\nexport function registerReadOnlyFileTools(\n registry: ToolRegistry,\n workspace: string,\n guard?: ConflictGuard,\n): void {\n const readFile = makeReadFile(workspace, guard);\n registry.register(\n (args) => readFile(args),\n { category: \"filesystem\", riskLevel: \"low\", capabilities: [\"read_file\"], requiresApproval: false },\n {\n type: \"function\",\n function: {\n name: \"read_file\",\n description: \"读取文本文件,返回带行号的内容 (格式: '12|text'),以便引用为 path:line。path 与 paths 至少传一个。大文件窗口化:传递 start_line 从上一次读取的位置继续。需要同时查看多个文件/区间时用 paths 一次读取,减少往返。只读。\",\n parameters: {\n type: \"object\",\n properties: {\n path: { type: \"string\", description: \"文件路径 (相对于工作区)\" },\n paths: {\n type: \"array\",\n items: { type: \"string\" },\n description: \"一次读取多个文件 (与 path 二选一);多个候选文件应一次读完,不要逐个发请求\",\n },\n start_line: { type: \"integer\", description: \"起始行号 (从1开始,默认1)\" },\n max_lines: { type: \"integer\", description: `最大行数 (默认 ${DEFAULT_MAX_LINES},多文件合计上限 ${MULTI_READ_MAX_TOTAL_LINES})` },\n },\n },\n },\n },\n );\n\n const listFiles = makeListFiles(workspace);\n registry.register(\n (args) => listFiles(args),\n { category: \"filesystem\", riskLevel: \"low\", capabilities: [\"list_files\"], requiresApproval: false },\n {\n type: \"function\",\n function: {\n name: \"list_files\",\n description:\n \"列出目录下的文件和子目录。跳过隐藏项、常见构建产物目录,并遵守工作区根目录的 .gitignore。\" +\n \"了解大仓库结构时用 recursive=true 一次拿到有限深度(默认 3 层)的树,\" +\n \"比逐层调用省很多往返;条目过多时会截断并提示。\",\n parameters: {\n type: \"object\",\n properties: {\n path: { type: \"string\", description: \"目录路径 (相对于工作区,默认 '.')\" },\n recursive: {\n type: \"boolean\",\n description: \"递归列出子目录(默认 false,只列一层)\",\n },\n max_depth: {\n type: \"integer\",\n description: `递归深度 (默认 ${DEFAULT_LIST_DEPTH},上限 ${MAX_LIST_DEPTH};仅 recursive 时生效)`,\n },\n max_results: {\n type: \"integer\",\n description: `最大结果数(非递归默认 100 / 上限 100;递归默认 ${DEFAULT_RECURSIVE_LIMIT} / 上限 ${MAX_RECURSIVE_LIMIT})`,\n },\n },\n },\n },\n },\n );\n}\n\n/**\n * 注册文件工具到 ToolRegistry\n *\n * read_file / list_files 为低风险(无需审批);write_file / replace_in_file\n * 为中风险且 requiresApproval=true(由权限引擎决定是否需要审批)。\n */\nexport function registerFileTools(\n registry: ToolRegistry,\n workspace: string,\n guard?: ConflictGuard,\n): void {\n registerReadOnlyFileTools(registry, workspace, guard);\n\n const writeFile = makeWriteFile(workspace, guard);\n registry.register(\n (args) => writeFile(args),\n { category: \"filesystem\", riskLevel: \"medium\", capabilities: [\"write_file\"], requiresApproval: true },\n {\n type: \"function\",\n function: {\n name: \"write_file\",\n description: \"写入一个 UTF-8 文本文件。用于创建新文件或完全重写现有文件。\",\n parameters: {\n type: \"object\",\n properties: {\n path: { type: \"string\", description: \"文件路径 (相对于工作区)\" },\n content: { type: \"string\", description: \"文件内容 (UTF-8)\" },\n overwrite: { type: \"boolean\", description: \"是否覆盖已存在的文件 (默认 true)\" },\n },\n required: [\"path\", \"content\"],\n },\n },\n },\n );\n\n const replaceFile = makeReplaceInFile(workspace, guard);\n registry.register(\n (args) => replaceFile(args),\n { category: \"filesystem\", riskLevel: \"medium\", capabilities: [\"edit_file\"], requiresApproval: true },\n {\n type: \"function\",\n function: {\n name: \"replace_in_file\",\n description: \"在文件中精确替换一段文本。先按预期次数精确匹配 old 字符串,然后替换为 new。用于小范围精确编辑。\",\n parameters: {\n type: \"object\",\n properties: {\n path: { type: \"string\", description: \"文件路径 (相对于工作区)\" },\n old: { type: \"string\", description: \"要被替换的精确文本\" },\n new: { type: \"string\", description: \"替换后的新文本\" },\n expected_replacements: { type: \"integer\", description: \"期望的替换次数 (默认 1)\" },\n },\n required: [\"path\", \"old\", \"new\"],\n },\n },\n },\n );\n}\n","/**\n * 路径安全辅助函数\n *\n * 判断目标路径是否真正位于工作区根目录之内。\n *\n * 仅做字符串比较(path.relative)无法防御符号链接逃逸:\n * 工作区内若存在指向外部的 symlink,`workspace/link/secret.txt`\n * 的字符串前缀合法,但实际读写发生在链接目标(工作区外)。\n *\n * 因此这里用 fs.realpathSync 解析符号链接后再与根目录的 realpath\n * 比较。目标尚不存在(如 write_file 新建文件)时,向上回溯到最近\n * 已存在的祖先目录解析,再拼回剩余路径段。\n *\n * 校验通过时把解析后的真实路径一并返回:调用方必须用它(而非原始\n * 相对路径)做实际 fs 操作,否则「校验后、操作前」路径组件被替换为\n * 指向外部的 symlink 仍会逃逸(TOCTOU)。\n */\nimport * as fs from \"fs\";\nimport * as path from \"path\";\n\n/**\n * 判断 `path.relative(root, target)` 的结果是否落在 root 之内。\n *\n * 逃逸必然恰好是 `..` 或以 `..` + 分隔符开头,不能用 `startsWith(\"..\")`:\n * 工作区里名为 `..foo` 的合法条目会被误判为逃逸而拒绝。\n * 跨盘符时(Windows)path.relative 返回绝对路径,一并拒绝。\n *\n * 导出供自行做 `path.relative` 校验的调用方复用(如目录浏览新建目录、\n * git 面板的 repo / path 参数),避免各处重复写 `startsWith(\"..\")`。\n */\nexport function isInsideRelative(rel: string): boolean {\n return (\n !path.isAbsolute(rel) && rel !== \"..\" && !rel.startsWith(`..${path.sep}`)\n );\n}\n\n/** 是否为文件系统根(\"/\"、\"C:\\\\\") */\nfunction isFilesystemRoot(p: string): boolean {\n return path.parse(p).root === p;\n}\n\n/** 校验通过的路径:root 为工作区根的真实路径,real 为目标的真实路径 */\nexport interface ResolvedPath {\n root: string;\n real: string;\n}\n\n/**\n * 校验 target(相对 workspace 的路径)是否位于 workspace 内,并返回\n * 解析符号链接后的真实路径;不在工作区内时返回 null。\n *\n * - 解析符号链接:路径中任何 symlink 组件都会先 realpath 再校验;\n * broken symlink(存在但无法解析)一律拒绝。\n * - 目标不存在时(写新文件):回溯到最近存在的祖先做 realpath,\n * 再拼接剩余段,仍能捕获祖先链中的 symlink 逃逸。\n * - 工作区根若就是文件系统根(`WORKSPACE=/`):此时任何绝对路径都在\n * 其内,等同没有沙箱,直接判为越界。\n */\nexport function resolveWithinRoot(\n workspace: string,\n target: string,\n): ResolvedPath | null {\n const abs = path.resolve(workspace, target);\n\n let rootReal: string;\n try {\n rootReal = fs.realpathSync(workspace);\n } catch {\n // 根目录不存在(如权限引擎尚未初始化工作区):\n // 没有实际目录结构即无 symlink 逃逸面,退化为纯字符串路径比较。\n const rootAbs = path.resolve(workspace);\n if (!isInsideRelative(path.relative(rootAbs, abs))) return null;\n return { root: rootAbs, real: abs };\n }\n if (isFilesystemRoot(rootReal)) return null;\n\n let real: string;\n try {\n real = fs.realpathSync(abs);\n } catch {\n // abs 不存在:向上回溯找最近存在的祖先\n let probe = abs;\n const tail: string[] = [];\n for (;;) {\n // 组件存在(含 broken symlink)但 realpath 失败 → 无法解析,拒绝\n const exists = fs.existsSync(probe) || fs.lstatSync(probe, { throwIfNoEntry: false }) !== undefined;\n if (exists) {\n try {\n real = path.join(fs.realpathSync(probe), ...tail);\n break;\n } catch {\n return null;\n }\n }\n tail.unshift(path.basename(probe));\n const parent = path.dirname(probe);\n if (parent === probe) return null; // 已到文件系统根\n probe = parent;\n }\n }\n\n if (!isInsideRelative(path.relative(rootReal, real))) return null;\n return { root: rootReal, real };\n}\n\n/**\n * 判断 target 是否安全地位于 workspace 内(不需要真实路径时用这个)。\n *\n * 注意:删除类操作不应据此改用 realpath 去 unlink —— 那会删掉符号链接\n * 指向的目标而非链接本身;这类操作请用 resolveEntryWithinRoot。\n */\nexport function isWithinRoot(workspace: string, target: string): boolean {\n return resolveWithinRoot(workspace, target) !== null;\n}\n\n/**\n * 解析「目录项本身」的路径:父目录取 realpath,最后一段保持原样。\n *\n * 供 unlink / rename 等作用于条目自身的操作用:父目录不可被 symlink\n * 替换,而最后一段若是符号链接,被操作的是链接本身,不会被跟随。\n */\nexport function resolveEntryWithinRoot(\n workspace: string,\n target: string,\n): ResolvedPath | null {\n const abs = path.resolve(workspace, target);\n const parent = resolveWithinRoot(workspace, path.dirname(abs));\n if (!parent) return null;\n return { root: parent.root, real: path.join(parent.real, path.basename(abs)) };\n}\n","/**\n * 本机配置 / 状态目录的跨平台解析\n *\n * POSIX 沿用 XDG 约定;Windows 上 `HOME` 在 cmd / PowerShell 中通常不存在,\n * 若直接回退 `/tmp` 会把配置写到 `\\tmp\\...`(当前盘符根),且与\n * `%APPDATA%` 的常规位置不一致。统一顺序:显式 XDG 变量(用户显式指定,\n * 优先级最高)→ 平台原生目录 → XDG 默认位置(家目录)→ 临时目录。\n */\nimport * as os from \"os\";\nimport * as path from \"path\";\n\n/** 应用目录名(`~/.config/zqagent` 的最后一段) */\nconst APP_DIR_NAME = \"zqagent\";\n\n/**\n * 节点默认数据目录名(相对启动目录,即 `./.zqagent_data`)。\n * server / CLI 在既无 `--data-dir` 也无 `DATA_DIR` 时用它兜底,\n * 集中在此避免各处字面量走散。\n */\nexport const DEFAULT_DATA_DIR = \".zqagent_data\";\n\n/**\n * 官方 Hub 基址(不带结尾斜杠):CLI 在既无 `--hub` 也无 `HUB_URL` 时用它兜底\n * (首个交互式接入提示的默认值)。自建 Hub 请用 `--hub` / `HUB_URL` 覆盖。\n */\nexport const DEFAULT_HUB_URL = \"https://hub.cd-zq.com\";\n\n/** 是否为 Windows(仅此处需要区分平台;APPDATA 在别的系统上可能是无关变量) */\nconst IS_WINDOWS = process.platform === \"win32\";\n\n/** 用户主目录:`HOME` → Windows `USERPROFILE` → `os.homedir()` → 临时目录 */\nfunction homeDir(): string {\n return (\n process.env.HOME || process.env.USERPROFILE || os.homedir() || os.tmpdir()\n );\n}\n\n/** 配置目录:`$XDG_CONFIG_HOME/zqagent`、Windows `%APPDATA%\\zqagent`、其余 `~/.config/zqagent` */\nexport function configDir(): string {\n const base =\n process.env.XDG_CONFIG_HOME ||\n (IS_WINDOWS ? process.env.APPDATA : undefined) ||\n path.join(homeDir(), \".config\");\n return path.join(base, APP_DIR_NAME);\n}\n\n/** 状态目录:`$XDG_STATE_HOME/zqagent`、Windows `%LOCALAPPDATA%\\zqagent`、其余 `~/.local/state/zqagent` */\nexport function stateDir(): string {\n const base =\n process.env.XDG_STATE_HOME ||\n (IS_WINDOWS ? process.env.LOCALAPPDATA : undefined) ||\n path.join(homeDir(), \".local\", \"state\");\n return path.join(base, APP_DIR_NAME);\n}\n","/**\n * 工具结果预算\n *\n * 单次工具调用的结果可能远大于上下文预算(read_file 2000 行、git_diff 全量、\n * explore 报告……)。整条塞进对话历史会让上下文水位迅速触顶,而压缩只能在\n * user / assistant 边界切分,切不动挂在尾部的一条巨大 tool 消息——于是出现\n * “压完仍超阈值、下一轮继续压”的空转。\n *\n * 这里在工具结果进入历史之前统一裁剪:完整内容落盘,上下文中只保留头尾片段\n * 与文件路径,模型需要细节时可自行按路径重读。\n *\n * 预算按工具分级(见 {@link TOOL_MAX_CHARS}):噪声大、价值衰减快的输出\n * (grep / list_files / git_status)比例更小;中等体量结果即便未超预算也落盘,\n * 以便被淘汰(elision)后仍能按路径取回。\n */\nimport * as fs from \"fs\";\nimport * as path from \"path\";\nimport { stateDir } from \"../paths.js\";\n\n/** 单条工具结果进入上下文的最大字符数(约 6K token) */\nexport const DEFAULT_TOOL_RESULT_MAX_CHARS = 24_000;\n\n/**\n * 各工具的默认上下文预算(未列出的工具用 {@link DEFAULT_TOOL_RESULT_MAX_CHARS})。\n *\n * 统一按 24K 裁剪会让低价值高噪声的输出(grep / list_files / git_status)\n * 挤占同样多的上下文,而这些结果在历史里要随每次迭代重发。\n * 按「噪声越大、预算越小」分级,同时保留落盘路径,需要的细节可回读。\n */\nconst TOOL_MAX_CHARS: Record<string, number> = {\n read_file: 24_000,\n explore: 16_000,\n run_shell: 12_000,\n web_fetch: 12_000,\n git_diff: 12_000,\n grep: 10_000,\n web_search: 8_000,\n list_files: 8_000,\n shell_task_output: 8_000,\n /* repo_map 自带 16K 上限(tools/repoMap.ts 的 MAX_CHARS)。这里显式登记,\n * 而不是落到 24K 默认值 —— 免得将来工具上限调大时被二次裁剪,口径也保持一致。 */\n repo_map: 16_000,\n git_log: 6_000,\n git_status: 4_000,\n};\n\n/** 中等体量结果也落盘的阈值:淘汰后仍能按路径取回原文 */\nexport const DEFAULT_SAVE_THRESHOLD = 8_000;\n\n/** 头尾片段中头部占比 */\nconst HEAD_RATIO = 0.6;\n\n/** 解析 TOOL_RESULT_MAX_CHARS(全局覆盖);非法 / 非正数回退 undefined */\nfunction globalBudgetChars(): number | undefined {\n const n = Number(process.env.TOOL_RESULT_MAX_CHARS);\n return Number.isFinite(n) && n > 0 ? Math.floor(n) : undefined;\n}\n\n/** 解析 TOOL_RESULT_SAVE_THRESHOLD;0 或非法值表示关闭提前落盘 */\nfunction saveThreshold(): number {\n const raw = process.env.TOOL_RESULT_SAVE_THRESHOLD;\n if (raw === undefined || raw.trim() === \"\") return DEFAULT_SAVE_THRESHOLD;\n const n = Number(raw);\n return Number.isFinite(n) && n >= 0 ? Math.floor(n) : DEFAULT_SAVE_THRESHOLD;\n}\n\n/** 某个工具本次进入上下文的最大字符数 */\nexport function toolResultBudget(tool: string): number {\n return globalBudgetChars() ?? TOOL_MAX_CHARS[tool] ?? DEFAULT_TOOL_RESULT_MAX_CHARS;\n}\n\n/** 落盘目录:TOOL_RESULT_DIR > 状态目录下的 tool-results */\nexport function toolResultDir(): string {\n const env = process.env.TOOL_RESULT_DIR?.trim();\n return env || path.join(stateDir(), \"tool-results\");\n}\n\n/** 把字符串截成「头 + 省略提示 + 尾」,结果长度不超过 budget */\nfunction clipText(text: string, budget: number): string {\n if (text.length <= budget) return text;\n // 先按最大位数估算标记长度,再调整头尾,保证总长不超预算\n const room = Math.max(\n 0,\n budget - `\\n\\n… [${text.length} characters omitted] …\\n\\n`.length,\n );\n const headLen = Math.floor(room * HEAD_RATIO);\n const tailLen = room - headLen;\n const omitted = text.length - headLen - tailLen;\n return (\n text.slice(0, headLen) +\n `\\n\\n… [${omitted} characters omitted] …\\n\\n` +\n text.slice(-tailLen)\n );\n}\n\n/**\n * 字段级裁剪:保持 JSON 结构合法。\n *\n * run_shell / git_diff 等返回的是对象,直接截断字符串会破坏 JSON。\n * 逐字段裁剪后 exit_code、路径等元信息仍完整可读。\n */\nfunction clipDeep(value: unknown, perField: number): unknown {\n if (typeof value === \"string\") {\n return value.length <= perField ? value : clipText(value, perField);\n }\n if (Array.isArray(value)) {\n return value.map((v) => clipDeep(v, perField));\n }\n if (value && typeof value === \"object\") {\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value)) out[k] = clipDeep(v, perField);\n return out;\n }\n return value;\n}\n\n/** 落盘目录里保留的历史结果文件数上限(按文件名中的时间戳淘汰最旧的) */\nexport const MAX_SAVED_FILES = 300;\n\n/** 完整内容落盘,返回路径;失败时返回 undefined(不影响主流程) */\nfunction saveRaw(tool: string, raw: string, dir: string): string | undefined {\n try {\n fs.mkdirSync(dir, { recursive: true });\n const safe = tool.replace(/[^a-zA-Z0-9_-]/g, \"_\").slice(0, 40) || \"tool\";\n const name = `${Date.now()}-${process.pid}-${Math.random()\n .toString(36)\n .slice(2, 8)}-${safe}.txt`;\n const file = path.join(dir, name);\n fs.writeFileSync(file, raw, \"utf-8\");\n pruneSaved(dir);\n return file;\n } catch {\n return undefined;\n }\n}\n\n/**\n * 淘汰过旧的落盘结果。\n *\n * 文件名以毫秒时间戳开头,字典序即时间序。中等体量的工具结果也会落盘后,\n * 单会话就能攒出上百个文件,这里只保留最近 {@link MAX_SAVED_FILES} 个。\n */\nfunction pruneSaved(dir: string): void {\n try {\n const files = fs.readdirSync(dir).filter((f) => f.endsWith(\".txt\"));\n if (files.length <= MAX_SAVED_FILES) return;\n files.sort();\n for (const f of files.slice(0, files.length - MAX_SAVED_FILES)) {\n try {\n fs.unlinkSync(path.join(dir, f));\n } catch {\n // 单个文件删除失败(被占用等)不影响落盘\n }\n }\n } catch {\n // 目录不可读时跳过清理\n }\n}\n\n/** 工具结果的预算处理结果 */\nexport interface BudgetedToolResult {\n /** 进入对话历史的内容(字符串) */\n content: string;\n /** 是否发生了裁剪 */\n truncated: boolean;\n /** 完整内容的落盘路径(未落盘时为空) */\n savedPath?: string;\n}\n\n/**\n * 对工具结果应用预算。\n *\n * @param tool 工具名(用于落盘文件名)\n * @param result 工具原始返回值\n * @param opts.maxChars 覆盖默认预算(测试用)\n * @param opts.dir 覆盖落盘目录(测试用)\n */\nexport function applyToolResultBudget(\n tool: string,\n result: unknown,\n opts: { maxChars?: number; dir?: string } = {},\n): BudgetedToolResult {\n const budget = opts.maxChars ?? toolResultBudget(tool);\n const raw =\n typeof result === \"string\"\n ? result\n : (JSON.stringify(result, null, 2) ?? String(result));\n\n if (raw.length <= budget) {\n // 中等体量也落盘:被后续淘汰(elision)后,占位符里的路径仍能取回原文。\n // 不裁剪、只在末尾加一行短提示,避免模型重复执行同一个工具。\n const threshold = saveThreshold();\n if (threshold > 0 && raw.length >= threshold) {\n const savedPath = saveRaw(tool, raw, opts.dir ?? toolResultDir());\n if (savedPath) {\n return {\n content: `${raw}\\n\\n[full output saved to ${savedPath}]`,\n truncated: false,\n savedPath,\n };\n }\n }\n return { content: raw, truncated: false };\n }\n\n // 1) 对象优先按字段裁剪,保住 JSON 结构;\n // 多字段同时超长时逐步收紧单字段上限,直到整体落回预算\n let content = raw;\n if (typeof result === \"object\" && result !== null) {\n let perField = Math.floor(budget * 0.7);\n let clipped = JSON.stringify(clipDeep(result, perField), null, 2);\n for (let i = 0; i < 6 && clipped && clipped.length > budget && perField > 128; i++) {\n perField = Math.floor(perField / 2);\n clipped = JSON.stringify(clipDeep(result, perField), null, 2);\n }\n if (clipped && clipped.length <= budget) content = clipped;\n }\n // 2) 兜底:整体保留头尾\n if (content.length > budget) content = clipText(content, budget);\n\n // 3) 完整内容落盘,上下文里留下取回路径\n const savedPath = saveRaw(tool, raw, opts.dir ?? toolResultDir());\n if (savedPath) {\n content =\n `[tool output truncated: ${raw.length} chars, full output saved to ${savedPath}]\\n\\n` +\n content;\n }\n\n return { content, truncated: true, savedPath };\n}\n","/**\n * 目录遍历的忽略规则(`list_files` 与 repo map 共用)\n *\n * 抽成独立模块的理由是**一致性**:`list_files` 看到的文件集合,必须和\n * repo map 索引到的集合一致。两处各写一份,早晚会出现「grep 能搜到、\n * 但结构图里没有」这种让人困惑的偏差。\n *\n * 关于 `.gitignore`:这里只实现实际用得上的子集(注释、空行、`/` 锚定、\n * 末尾 `/` 表示目录、`*` / `?` 通配),**不支持** `!` 取反与 `**`。需要完整\n * 语义时应依赖 ripgrep —— `grep` 工具走 rg,自带原生支持。\n */\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\n\n/**\n * 永远跳过的目录名。\n *\n * 覆盖各生态的构建产物与依赖目录。实测过这个清单的价值:当前仓库有\n * 28.5 万个文件,其中 `node_modules` 9.2 万、Rust `target/` 7.5 万 ——\n * 漏掉任意一个,遍历规模就大一个数量级。\n */\nexport const ALWAYS_SKIP_DIRS = new Set([\n // JS / TS\n \"node_modules\",\n \"dist\",\n \"build\",\n \"out\",\n \".next\",\n \".nuxt\",\n \".cache\",\n // JVM\n \"target\",\n \".gradle\",\n \".mvn\",\n // .NET\n \"bin\",\n \"obj\",\n \".vs\",\n \"TestResults\",\n // Python / Ruby / PHP\n \"__pycache__\",\n \".venv\",\n \"venv\",\n \".pytest_cache\",\n \".mypy_cache\",\n \"vendor\",\n // 移动端\n \"Pods\",\n \".dart_tool\",\n \"DerivedData\",\n // 覆盖率 / 通用\n \"coverage\",\n // 版本库\n \".git\",\n \".svn\",\n \".hg\",\n]);\n\n/** gitignore 匹配器:给定相对路径(POSIX)与是否目录,返回是否被忽略 */\nexport type IgnoreMatcher = (rel: string, isDir: boolean) => boolean;\n\n/**\n * 解析某个目录下的 `.gitignore` 为匹配器列表。\n *\n * @param dir 通常是工作区根。子目录的 `.gitignore` 不解析 —— 大仓库里逐个\n * 读取子目录配置的收益远小于成本,而根配置已经覆盖绝大多数情况。\n */\nexport function loadGitignore(dir: string): IgnoreMatcher[] {\n const file = path.join(dir, \".gitignore\");\n let text: string;\n try {\n // .gitignore 一般很小;超过 64KB 基本是误用(或生成文件),不解析\n const stat = fs.statSync(file);\n if (stat.size > 64 * 1024) return [];\n text = fs.readFileSync(file, \"utf-8\");\n } catch {\n return [];\n }\n\n const matchers: IgnoreMatcher[] = [];\n for (const rawLine of text.split(\"\\n\")) {\n let line = rawLine.replace(/\\r$/, \"\").trim();\n if (!line || line.startsWith(\"#\")) continue;\n // 取反规则直接忽略:本实现只做「排除」,遇到取反就当没写,宁可多列也不误删\n if (line.startsWith(\"!\")) continue;\n\n const anchored = rawLine.trim().startsWith(\"/\");\n const dirOnly = /\\/\\s*$/.test(rawLine.trim());\n line = line.replace(/^\\/+/, \"\").replace(/\\/+$/, \"\");\n if (!line) continue;\n\n // 通配符转正则(先逐字符转义,再把 * / ? 还原为通配)\n const pattern = line\n .split(\"\")\n .map((ch) => {\n if (ch === \"*\") return \".*\";\n if (ch === \"?\") return \".\";\n return ch.replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\");\n })\n .join(\"\");\n // 不含 / 的模式按「任意层级的名字」匹配(git 的语义)\n const re = new RegExp(line.includes(\"/\") || anchored ? `^${pattern}$` : `(^|/)${pattern}$`);\n\n matchers.push((rel, isDir) => {\n if (dirOnly && !isDir) return false;\n return re.test(rel);\n });\n }\n return matchers;\n}\n\n/** 组装「是否忽略」判定:隐藏项 + 构建产物 + gitignore */\nexport function makeIgnoreFilter(root: string): IgnoreMatcher {\n const matchers = loadGitignore(root);\n return (rel, isDir) => {\n const name = rel.slice(rel.lastIndexOf(\"/\") + 1);\n if (name.startsWith(\".\")) return true;\n if (isDir && ALWAYS_SKIP_DIRS.has(name)) return true;\n return matchers.some((m) => m(rel, isDir));\n };\n}\n"],"mappings":"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","names":["fs","path","fs","path","filePath","results"]}
@@ -542,12 +542,61 @@ function resolveRipgrep() {
542
542
  }
543
543
 
544
544
  // src/tools/search.ts
545
- function parseRgOutput(result) {
546
- return result.split("\n").map((l) => l.replace(/\r$/, "")).filter((l) => l.length > 0).slice(0, 50).map((line) => {
547
- const m = /^(.+?):(\d+):(.*)$/.exec(line);
548
- if (!m) return { file: line.trim(), line: 0, text: "" };
549
- return { file: m[1], line: Number(m[2]), text: m[3].trim().slice(0, 200) };
550
- });
545
+ var DEFAULT_MAX_RESULTS = 50;
546
+ var MAX_RESULTS_CAP = 200;
547
+ var MAX_TEXT_CHARS = 200;
548
+ var RG_TIMEOUT_MS = 1e4;
549
+ function parseRgOutput(result, limit) {
550
+ const out = [];
551
+ for (const raw of result.split("\n")) {
552
+ if (out.length >= limit) break;
553
+ const line = raw.replace(/\r$/, "");
554
+ if (!line) continue;
555
+ const m = /^(.+?)[:-](\d+)[:-](.*)$/.exec(line);
556
+ if (!m) continue;
557
+ out.push({
558
+ file: m[1],
559
+ line: Number(m[2]),
560
+ text: m[3].trim().slice(0, MAX_TEXT_CHARS)
561
+ });
562
+ }
563
+ return out;
564
+ }
565
+ function parseRgFiles(result, limit) {
566
+ const files = [];
567
+ const seen = /* @__PURE__ */ new Set();
568
+ for (const raw of result.split("\n")) {
569
+ if (files.length >= limit) break;
570
+ const f = raw.replace(/\r$/, "").trim();
571
+ if (!f || seen.has(f)) continue;
572
+ seen.add(f);
573
+ files.push(f);
574
+ }
575
+ return files;
576
+ }
577
+ function rgArgs(opts) {
578
+ const args = [];
579
+ if (opts.filesOnly) {
580
+ args.push("--files-with-matches");
581
+ } else {
582
+ args.push("--with-filename", "--line-number", "--no-heading");
583
+ args.push("--max-count", String(Math.max(opts.maxResults, 1)));
584
+ }
585
+ if (opts.ignoreCase) args.push("--ignore-case");
586
+ if (opts.glob) args.push("--glob", opts.glob);
587
+ if (opts.contextLines > 0 && !opts.filesOnly) {
588
+ args.push("--context", String(opts.contextLines));
589
+ }
590
+ args.push("--", opts.pattern, opts.path);
591
+ return args;
592
+ }
593
+ function rgFailureHint(res) {
594
+ if (res.timedOut) {
595
+ return `\u641C\u7D22\u8D85\u65F6\uFF08${RG_TIMEOUT_MS / 1e3}s\uFF09\u3002\u5927\u4ED3\u5E93\u5168\u91CF\u641C\u7D22\u5F88\u5BB9\u6613\u8D85\u65F6\uFF1A\u8BF7\u7F29\u5C0F path \u5230\u5177\u4F53\u5B50\u76EE\u5F55\uFF0C\u6216\u7528 glob \u53C2\u6570\u9650\u5236\u6587\u4EF6\u7C7B\u578B\uFF08\u5982 "*.ts"\uFF09\u540E\u91CD\u8BD5`;
596
+ }
597
+ if (res.aborted) return "\u641C\u7D22\u5DF2\u4E2D\u65AD";
598
+ const stderr = decodeOutput(res.stderr).trim().slice(0, 300);
599
+ return `\u641C\u7D22\u5931\u8D25\uFF08rg \u9000\u51FA\u7801 ${String(res.status)}\uFF09${stderr ? `\uFF1A${stderr}` : ""}`;
551
600
  }
552
601
  function makeGrep(workspace) {
553
602
  return async function grep(args) {
@@ -556,37 +605,50 @@ function makeGrep(workspace) {
556
605
  if (!pattern.trim()) {
557
606
  return { error: "\u641C\u7D22\u6A21\u5F0F\u4E0D\u80FD\u4E3A\u7A7A" };
558
607
  }
608
+ const filesOnly = args.files_only === true;
609
+ const ignoreCase = args.ignore_case === true;
610
+ const glob = typeof args.glob === "string" && args.glob.trim() ? args.glob.trim() : void 0;
611
+ const contextLines = Math.max(
612
+ 0,
613
+ Math.min(Number(args.context_lines ?? 0) || 0, 5)
614
+ );
615
+ const maxResults = Math.max(
616
+ 1,
617
+ Math.min(Number(args.max_results ?? DEFAULT_MAX_RESULTS) || DEFAULT_MAX_RESULTS, MAX_RESULTS_CAP)
618
+ );
559
619
  const rg = resolveRipgrep();
560
620
  if (rg) {
561
621
  const res = await execAsync(
562
622
  rg,
563
- [
564
- // --with-filename:单文件模式下 rg 默认不输出文件名前缀,会导致
565
- // "行号:内容" 被误当成 "文件:行号:内容" 解析而错位;显式要求带文件名。
566
- "--with-filename",
567
- "--line-number",
568
- "--no-heading",
569
- "--max-count",
570
- "50",
571
- pattern,
572
- filePath
573
- ],
623
+ rgArgs({ pattern, path: filePath, filesOnly, ignoreCase, glob, contextLines, maxResults }),
574
624
  {
575
625
  cwd: workspace,
576
- timeoutMs: 1e4,
626
+ timeoutMs: RG_TIMEOUT_MS,
577
627
  maxBuffer: 1024 * 1024 * 2
578
628
  }
579
629
  );
580
630
  if (res.status === 0) {
581
- const matches = parseRgOutput(decodeOutput(res.stdout));
582
- return { query: pattern, engine: "ripgrep", matches, count: matches.length };
631
+ const out = decodeOutput(res.stdout);
632
+ return filesOnly ? { query: pattern, engine: "ripgrep", files: parseRgFiles(out, maxResults) } : (() => {
633
+ const matches = parseRgOutput(out, maxResults);
634
+ return { query: pattern, engine: "ripgrep", matches, count: matches.length };
635
+ })();
583
636
  }
584
637
  if (res.status === 1) {
585
- return { query: pattern, engine: "ripgrep", matches: [], count: 0 };
638
+ return filesOnly ? { query: pattern, engine: "ripgrep", files: [], count: 0 } : { query: pattern, engine: "ripgrep", matches: [], count: 0 };
586
639
  }
640
+ if (!res.error) {
641
+ return { query: pattern, error: rgFailureHint(res) };
642
+ }
643
+ }
644
+ if (filesOnly || glob || ignoreCase || contextLines > 0) {
645
+ return {
646
+ query: pattern,
647
+ error: "\u5F53\u524D\u8282\u70B9\u672A\u5B89\u88C5 ripgrep\uFF0Cfiles_only / glob / ignore_case / context_lines \u9700\u8981 ripgrep \u624D\u80FD\u751F\u6548\uFF1B\u8BF7\u53BB\u6389\u8FD9\u4E9B\u53C2\u6570\u540E\u91CD\u8BD5\uFF0C\u6216\u5148\u5B89\u88C5 ripgrep"
648
+ };
587
649
  }
588
650
  try {
589
- const matches = nodeGrep({ pattern, target: filePath, workspace });
651
+ const matches = nodeGrep({ pattern, target: filePath, workspace, maxMatches: maxResults });
590
652
  return { query: pattern, engine: "node", matches, count: matches.length };
591
653
  } catch (exc) {
592
654
  return { error: `\u641C\u7D22\u5931\u8D25: ${String(exc instanceof Error ? exc.message : exc)}` };
@@ -602,12 +664,29 @@ function registerSearchTool(registry, workspace) {
602
664
  type: "function",
603
665
  function: {
604
666
  name: "grep",
605
- description: "\u5728\u6587\u4EF6\u4E2D\u641C\u7D22\u6587\u672C\u6A21\u5F0F\uFF08\u652F\u6301\u5B8C\u6574 JS \u6B63\u5219\u8BED\u6CD5\uFF09\u3002\u8FD4\u56DE\u5339\u914D\u7684\u6587\u4EF6\u540D\u3001\u884C\u53F7\u548C\u5185\u5BB9\uFF0C\u6700\u591A 50 \u6761\u3002\u4F18\u5148\u4F7F\u7528 ripgrep (rg) \u4EE5\u83B7\u5F97\u66F4\u597D\u7684\u6027\u80FD\uFF1Brg \u4E0D\u53EF\u7528\u65F6\u81EA\u52A8\u56DE\u9000\u5230\u5185\u7F6E\u626B\u63CF\u5668\u3002",
667
+ description: "\u5728\u6587\u4EF6\u4E2D\u641C\u7D22\u6587\u672C\u6A21\u5F0F\uFF08\u652F\u6301\u5B8C\u6574 JS \u6B63\u5219\u8BED\u6CD5\uFF09\u3002\u9ED8\u8BA4\u8FD4\u56DE\u5339\u914D\u7684\u6587\u4EF6\u540D\u3001\u884C\u53F7\u548C\u5185\u5BB9\uFF08\u6700\u591A 50 \u6761\uFF09\u3002\u5B9A\u4F4D\u4EE3\u7801\u4F4D\u7F6E\u65F6\u4F18\u5148\u7528 files_only=true \u53EA\u53D6\u6587\u4EF6\u5217\u8868\uFF08\u4F53\u79EF\u5C0F\u4E00\u4E2A\u6570\u91CF\u7EA7\uFF09\uFF0C\u518D\u7528 read_file \u7CBE\u51C6\u8BFB\u53D6\uFF1B\u5927\u4ED3\u5E93\u8BF7\u914D\u5408 path \u7F29\u5C0F\u8303\u56F4\u6216 glob \u9650\u5236\u6587\u4EF6\u7C7B\u578B\uFF0C\u907F\u514D\u5168\u91CF\u626B\u63CF\u8D85\u65F6\u3002",
606
668
  parameters: {
607
669
  type: "object",
608
670
  properties: {
609
671
  pattern: { type: "string", description: "\u641C\u7D22\u6A21\u5F0F (\u6587\u672C\u6216\u6B63\u5219\u8868\u8FBE\u5F0F)" },
610
- path: { type: "string", description: "\u641C\u7D22\u8DEF\u5F84 (\u6587\u4EF6\u6216\u76EE\u5F55\uFF0C\u9ED8\u8BA4 '.')" }
672
+ path: { type: "string", description: "\u641C\u7D22\u8DEF\u5F84 (\u6587\u4EF6\u6216\u76EE\u5F55\uFF0C\u9ED8\u8BA4 '.'\uFF1B\u5927\u4ED3\u5E93\u5EFA\u8BAE\u5148\u7F29\u5C0F\u5230\u5B50\u76EE\u5F55)" },
673
+ files_only: {
674
+ type: "boolean",
675
+ description: "\u53EA\u8FD4\u56DE\u5339\u914D\u7684\u6587\u4EF6\u8DEF\u5F84\uFF0C\u4E0D\u8FD4\u56DE\u884C\u5185\u5BB9\uFF08\u63A8\u8350\u7528\u4E8E\u5B9A\u4F4D\uFF0C\u663E\u8457\u8282\u7701\u4E0A\u4E0B\u6587\uFF09"
676
+ },
677
+ glob: {
678
+ type: "string",
679
+ description: '\u6587\u4EF6\u8FC7\u6EE4\uFF0C\u5982 "*.ts" \u6216 "!*.min.js"\uFF08\u53EF\u591A\u6B21\u6392\u9664\u9700\u81EA\u884C\u7EC4\u5408\u4E3A {a,b} \u5F62\u5F0F\uFF09'
680
+ },
681
+ ignore_case: { type: "boolean", description: "\u5FFD\u7565\u5927\u5C0F\u5199" },
682
+ context_lines: {
683
+ type: "integer",
684
+ description: "\u6BCF\u6761\u547D\u4E2D\u9644\u5E26\u7684\u524D\u540E\u4E0A\u4E0B\u6587\u884C\u6570\uFF080-5\uFF0C\u9ED8\u8BA4 0\uFF09"
685
+ },
686
+ max_results: {
687
+ type: "integer",
688
+ description: `\u6700\u591A\u8FD4\u56DE\u6761\u6570\uFF08\u9ED8\u8BA4 ${DEFAULT_MAX_RESULTS}\uFF0C\u4E0A\u9650 ${MAX_RESULTS_CAP}\uFF09`
689
+ }
611
690
  },
612
691
  required: ["pattern"]
613
692
  }