diff --git a/bridge/tools.js b/bridge/tools.js index 6b66bf4..26f4a03 100644 --- a/bridge/tools.js +++ b/bridge/tools.js @@ -43,11 +43,11 @@ function withLineNumbers(lines, startLine) { // 工具元数据(GET /tools 的数据源;扩展据此生成协议消息,防两端清单漂移) // --------------------------------------------------------------------------- const TOOL_DEFS = [ - { name: 'dsh_tree', description: 'List directory tree (skips node_modules/.git)', params: { path: 'string?', depth: 'int? default 3' } }, - { name: 'dsh_read', description: 'Read a text file fragment (1-based lines with line numbers; returns sha256 for edit verification)', params: { path: 'string', offset: 'int? default 1', limit: 'int? default 200' } }, + { name: 'dsh_tree', description: 'List directory tree (skips node_modules/.git). ifMoreThan: return meta only if entries exceed this.', params: { path: 'string?', depth: 'int? default 3', ifMoreThan: 'int? if entries > N, return meta instead of tree' } }, + { name: 'dsh_read', description: 'Read a text file fragment (1-based lines with line numbers; returns sha256 for edit verification). maxBytes: return meta only if file exceeds this.', params: { path: 'string', offset: 'int? default 1', limit: 'int? default 200', maxBytes: 'int? if file size > N, return meta instead of content' } }, { name: 'dsh_write', description: 'Write file (creates parent dirs). mode: overwrite (default) | create (fail if exists) | append (append to existing)', params: { path: 'string', content: 'string', mode: 'string? overwrite|create|append, default overwrite', expectedSha256: 'string? reject if current content differs (stale-write guard)' } }, { name: 'dsh_edit', description: 'Replace a unique fragment in a file', params: { path: 'string', oldText: 'string', newText: 'string', expectedSha256: 'string? sha256 from dsh_read; reject if file changed' } }, - { name: 'dsh_search', description: 'Search file contents (plain or regex)', params: { query: 'string', path: 'string?', regex: 'bool?', caseSensitive: 'bool?', maxResults: 'int? default 50' } }, + { name: 'dsh_search', description: 'Search file contents (plain or regex). ifMoreThan: return meta only if matches exceed this.', params: { query: 'string', path: 'string?', regex: 'bool?', caseSensitive: 'bool?', maxResults: 'int? default 50', ifMoreThan: 'int? if matches > N, return meta instead of matches' } }, { name: 'dsh_bash', description: 'Run a whitelisted command in the workspace', params: { command: 'string', timeoutMs: 'int? default 20000' } }, { name: 'dsh_script', description: 'Run a named python/mjs script with declared inputs/outputs. Requires user confirm. inputs are sha-checked before/after, outputs are verified after.', params: { name: 'string snake_case', language: 'python|mjs', code: 'string source', inputs: 'string[]? relative paths that must exist before run', outputs: 'string[]? relative paths verified after run', timeoutMs: 'int? default bashTimeoutMs' } }, { name: 'dsh_git_status', description: 'git status (read-only porcelain)', params: { path: 'string?' } }, @@ -213,6 +213,15 @@ function toolTree(ctx, args) { const base = st.isDirectory() ? relPath : path.dirname(relPath); const out = { lines: [], count: 0, maxDepth: Math.min(Number(args.depth) || 3, 8), truncated: false }; walkTree(root, base === '.' ? '' : base, 0, out); + const ifMoreThan = args.ifMoreThan !== undefined ? Math.max(1, Number(args.ifMoreThan)) : null; + if (ifMoreThan !== null && out.count > ifMoreThan) { + return { + tooMany: true, + count: out.count, + ifMoreThan, + hint: `Tree has ${out.count} entries (> ifMoreThan ${ifMoreThan}). Reduce depth, narrow path, or raise ifMoreThan.`, + }; + } return { tree: out.lines.join('\n') || '(empty dir)', fileCount: out.count, @@ -229,6 +238,17 @@ function toolRead(ctx, args) { throw new ToolError('E_ARGS_INVALID', `File not found: ${posixRel}`); } if (st.isDirectory()) throw new ToolError('E_ARGS_INVALID', `Target is a directory, use dsh_tree: ${posixRel}`); + // Explicit size estimate: if model declared maxBytes and file exceeds it, return meta only. + const maxBytes = args.maxBytes !== undefined ? Math.max(1, Number(args.maxBytes)) : null; + if (maxBytes !== null && st.size > maxBytes) { + return { + file: posixRel, + tooLarge: true, + size: st.size, + maxBytes, + hint: `File is ${st.size} bytes (> maxBytes ${maxBytes}). Use offset/limit for a range, dsh_search for content, or raise maxBytes.`, + }; + } // 整读上限 READ_MAX_BYTES;带 offset/limit 范围读取时放宽到 RANGE_SCAN_MAX_BYTES(CodexPro 模式) const hasRange = args.offset !== undefined || args.limit !== undefined; if (st.size > READ_MAX_BYTES && !(hasRange && st.size <= RANGE_SCAN_MAX_BYTES)) { @@ -363,6 +383,8 @@ function toolSearch(ctx, args) { const baseRel = st.isDirectory() ? (relPath === '.' ? '' : relPath.replace(/\\/g, '/')) : path.dirname(relPath).replace(/\\/g, '/'); const maxResults = Math.min(Math.max(1, Number(args.maxResults) || 50), 200); + const ifMoreThan = args.ifMoreThan !== undefined ? Math.max(1, Number(args.ifMoreThan)) : null; + const effMax = ifMoreThan !== null ? Math.min(maxResults, ifMoreThan + 1) : maxResults; let matcher; if (args.regex) { let re; @@ -376,13 +398,13 @@ function toolSearch(ctx, args) { const needle = args.caseSensitive ? args.query : args.query.toLowerCase(); matcher = (line) => (args.caseSensitive ? line.includes(needle) : line.toLowerCase().includes(needle)); } - const out = { matches: [], maxResults, filesScanned: 0 }; + const out = { matches: [], maxResults: effMax, filesScanned: 0 }; if (st.isFile()) { // 单文件搜索 const buf = fs.readFileSync(abs); if (!sniffBinary(buf)) { buf.toString('utf8').split('\n').forEach((line, i) => { - if (out.matches.length < maxResults && matcher(line)) { + if (out.matches.length < effMax && matcher(line)) { out.matches.push({ file: baseRel ? `${baseRel}/${path.basename(abs)}` : path.basename(abs), line: i + 1, text: line.slice(0, 240) }); } }); @@ -391,7 +413,16 @@ function toolSearch(ctx, args) { } else { searchWalk(baseDir, baseRel, matcher, out); } - return { matches: out.matches, truncated: out.matches.length >= maxResults, filesScanned: out.filesScanned }; + if (ifMoreThan !== null && out.matches.length > ifMoreThan) { + return { + tooMany: true, + count: out.matches.length, + ifMoreThan, + filesScanned: out.filesScanned, + hint: `Search returned > ${ifMoreThan} matches. Refine query, narrow path, or raise ifMoreThan.`, + }; + } + return { matches: out.matches, truncated: out.matches.length >= effMax, filesScanned: out.filesScanned }; } function runCaptured(cmd, cmdArgs, cwd, timeoutMs, maxOut, opts) {