Browse Source

Фаза 3a: maxBytes/ifMoreThan - оценка размера в команде, при превышении возврат меты вместо данных

master
Serge Shutov 3 days ago
parent
commit
80bcb1b7d7
  1. 43
      bridge/tools.js

43
bridge/tools.js

@ -43,11 +43,11 @@ function withLineNumbers(lines, startLine) { @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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) { @@ -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) {

Loading…
Cancel
Save