@zqagent/core 0.2.16 → 0.3.6
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/README.md +1 -1
- package/README.oss.en.md +15 -2
- package/README.oss.md +18 -2
- package/README.oss.zh-TW.md +13 -2
- package/dist/McpClient-CEQBUPuC.d.cts +145 -0
- package/dist/McpClient-CEQBUPuC.d.ts +145 -0
- package/dist/{ToolRegistry-C3pTFiaJ.d.cts → ToolRegistry-Cy0fQxrb.d.ts} +2 -111
- package/dist/{ToolRegistry-C3pTFiaJ.d.ts → ToolRegistry-DouDvn9E.d.cts} +2 -111
- package/dist/{explore-RtZtb-V6.d.ts → TurnEngine-BPKhWuSI.d.cts} +108 -73
- package/dist/{explore-BgRhj6SP.d.cts → TurnEngine-DtGEuVLT.d.ts} +108 -73
- package/dist/conflictGuard-BBOOIxxK.d.cts +42 -0
- package/dist/conflictGuard-BBOOIxxK.d.ts +42 -0
- package/dist/index.cjs +5477 -1140
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +577 -195
- package/dist/index.d.ts +577 -195
- package/dist/index.js +5305 -1002
- package/dist/index.js.map +1 -1
- package/dist/internal/crypto/secrets.cjs +25 -0
- package/dist/internal/crypto/secrets.cjs.map +1 -0
- package/dist/internal/crypto/secrets.d.cts +2 -0
- package/dist/internal/crypto/secrets.d.ts +2 -0
- package/dist/internal/crypto/secrets.js +3 -0
- package/dist/internal/crypto/secrets.js.map +1 -0
- package/dist/internal/engine/FileLogHook.cjs +179 -0
- package/dist/internal/engine/FileLogHook.cjs.map +1 -0
- package/dist/internal/engine/FileLogHook.d.cts +67 -0
- package/dist/internal/engine/FileLogHook.d.ts +67 -0
- package/dist/internal/engine/FileLogHook.js +144 -0
- package/dist/internal/engine/FileLogHook.js.map +1 -0
- package/dist/internal/engine/approvalView.cjs +25 -0
- package/dist/internal/engine/approvalView.cjs.map +1 -0
- package/dist/internal/engine/approvalView.d.cts +1 -0
- package/dist/internal/engine/approvalView.d.ts +1 -0
- package/dist/internal/engine/approvalView.js +3 -0
- package/dist/internal/engine/approvalView.js.map +1 -0
- package/dist/internal/engine/limits.cjs +59 -0
- package/dist/internal/engine/limits.cjs.map +1 -0
- package/dist/internal/engine/limits.d.cts +32 -0
- package/dist/internal/engine/limits.d.ts +32 -0
- package/dist/internal/engine/limits.js +31 -0
- package/dist/internal/engine/limits.js.map +1 -0
- package/dist/internal/engine/sessionNaming.cjs +47 -0
- package/dist/internal/engine/sessionNaming.cjs.map +1 -0
- package/dist/internal/engine/sessionNaming.d.cts +25 -0
- package/dist/internal/engine/sessionNaming.d.ts +25 -0
- package/dist/internal/engine/sessionNaming.js +20 -0
- package/dist/internal/engine/sessionNaming.js.map +1 -0
- package/dist/internal/llmConfig.cjs +561 -0
- package/dist/internal/llmConfig.cjs.map +1 -0
- package/dist/internal/llmConfig.d.cts +75 -0
- package/dist/internal/llmConfig.d.ts +75 -0
- package/dist/internal/llmConfig.js +535 -0
- package/dist/internal/llmConfig.js.map +1 -0
- package/dist/internal/mcp/builtin/index.cjs +685 -0
- package/dist/internal/mcp/builtin/index.cjs.map +1 -0
- package/dist/internal/mcp/builtin/index.d.cts +21 -0
- package/dist/internal/mcp/builtin/index.d.ts +21 -0
- package/dist/internal/mcp/builtin/index.js +652 -0
- package/dist/internal/mcp/builtin/index.js.map +1 -0
- package/dist/internal/mcp/config.cjs +278 -0
- package/dist/internal/mcp/config.cjs.map +1 -0
- package/dist/internal/mcp/config.d.cts +61 -0
- package/dist/internal/mcp/config.d.ts +61 -0
- package/dist/internal/mcp/config.js +235 -0
- package/dist/internal/mcp/config.js.map +1 -0
- package/dist/internal/mcp/presets.cjs +184 -0
- package/dist/internal/mcp/presets.cjs.map +1 -0
- package/dist/internal/mcp/presets.d.cts +39 -0
- package/dist/internal/mcp/presets.d.ts +39 -0
- package/dist/internal/mcp/presets.js +158 -0
- package/dist/internal/mcp/presets.js.map +1 -0
- package/dist/internal/node-name.cjs +45 -0
- package/dist/internal/node-name.cjs.map +1 -0
- package/dist/internal/node-name.d.cts +3 -0
- package/dist/internal/node-name.d.ts +3 -0
- package/dist/internal/node-name.js +10 -0
- package/dist/internal/node-name.js.map +1 -0
- package/dist/internal/paths.cjs +63 -0
- package/dist/internal/paths.cjs.map +1 -0
- package/dist/internal/paths.d.cts +17 -0
- package/dist/internal/paths.d.ts +17 -0
- package/dist/internal/paths.js +25 -0
- package/dist/internal/paths.js.map +1 -0
- package/dist/internal/providers/diagnostics.cjs +288 -0
- package/dist/internal/providers/diagnostics.cjs.map +1 -0
- package/dist/internal/providers/diagnostics.d.cts +103 -0
- package/dist/internal/providers/diagnostics.d.ts +103 -0
- package/dist/internal/providers/diagnostics.js +250 -0
- package/dist/internal/providers/diagnostics.js.map +1 -0
- package/dist/internal/providers/models.cjs +699 -0
- package/dist/internal/providers/models.cjs.map +1 -0
- package/dist/internal/providers/models.d.cts +136 -0
- package/dist/internal/providers/models.d.ts +136 -0
- package/dist/internal/providers/models.js +654 -0
- package/dist/internal/providers/models.js.map +1 -0
- package/dist/internal/terminal.cjs +161 -0
- package/dist/internal/terminal.cjs.map +1 -0
- package/dist/internal/terminal.d.cts +58 -0
- package/dist/internal/terminal.d.ts +58 -0
- package/dist/internal/terminal.js +128 -0
- package/dist/internal/terminal.js.map +1 -0
- package/dist/internal/tools/execAsync.cjs +187 -0
- package/dist/internal/tools/execAsync.cjs.map +1 -0
- package/dist/internal/tools/execAsync.d.cts +30 -0
- package/dist/internal/tools/execAsync.d.ts +30 -0
- package/dist/internal/tools/execAsync.js +162 -0
- package/dist/internal/tools/execAsync.js.map +1 -0
- package/dist/internal/tools/git.cjs +935 -0
- package/dist/internal/tools/git.cjs.map +1 -0
- package/dist/internal/tools/git.d.cts +206 -0
- package/dist/internal/tools/git.d.ts +206 -0
- package/dist/internal/tools/git.js +883 -0
- package/dist/internal/tools/git.js.map +1 -0
- package/dist/internal/tools/pathSafety.cjs +99 -0
- package/dist/internal/tools/pathSafety.cjs.map +1 -0
- package/dist/internal/tools/pathSafety.d.cts +44 -0
- package/dist/internal/tools/pathSafety.d.ts +44 -0
- package/dist/internal/tools/pathSafety.js +61 -0
- package/dist/internal/tools/pathSafety.js.map +1 -0
- package/dist/internal/tools/plan.cjs +146 -0
- package/dist/internal/tools/plan.cjs.map +1 -0
- package/dist/internal/tools/plan.d.cts +48 -0
- package/dist/internal/tools/plan.d.ts +48 -0
- package/dist/internal/tools/plan.js +117 -0
- package/dist/internal/tools/plan.js.map +1 -0
- package/dist/internal/tools/ripgrep.cjs +118 -0
- package/dist/internal/tools/ripgrep.cjs.map +1 -0
- package/dist/internal/tools/ripgrep.d.cts +6 -0
- package/dist/internal/tools/ripgrep.d.ts +6 -0
- package/dist/internal/tools/ripgrep.js +89 -0
- package/dist/internal/tools/ripgrep.js.map +1 -0
- package/dist/internal/tools/sandbox.cjs +253 -0
- package/dist/internal/tools/sandbox.cjs.map +1 -0
- package/dist/internal/tools/sandbox.d.cts +67 -0
- package/dist/internal/tools/sandbox.d.ts +67 -0
- package/dist/internal/tools/sandbox.js +213 -0
- package/dist/internal/tools/sandbox.js.map +1 -0
- package/dist/internal/tools/shell.cjs +771 -0
- package/dist/internal/tools/shell.cjs.map +1 -0
- package/dist/internal/tools/shell.d.cts +47 -0
- package/dist/internal/tools/shell.d.ts +47 -0
- package/dist/internal/tools/shell.js +731 -0
- package/dist/internal/tools/shell.js.map +1 -0
- package/dist/internal/tools/shellRuntime.cjs +220 -0
- package/dist/internal/tools/shellRuntime.cjs.map +1 -0
- package/dist/internal/tools/shellRuntime.d.cts +67 -0
- package/dist/internal/tools/shellRuntime.d.ts +67 -0
- package/dist/internal/tools/shellRuntime.js +181 -0
- package/dist/internal/tools/shellRuntime.js.map +1 -0
- package/dist/internal/update/controller.cjs +1035 -0
- package/dist/internal/update/controller.cjs.map +1 -0
- package/dist/internal/update/controller.d.cts +150 -0
- package/dist/internal/update/controller.d.ts +150 -0
- package/dist/internal/update/controller.js +991 -0
- package/dist/internal/update/controller.js.map +1 -0
- package/dist/internal/update/platform.cjs +234 -0
- package/dist/internal/update/platform.cjs.map +1 -0
- package/dist/internal/update/platform.d.cts +77 -0
- package/dist/internal/update/platform.d.ts +77 -0
- package/dist/internal/update/platform.js +184 -0
- package/dist/internal/update/platform.js.map +1 -0
- package/dist/tools/ask.d.cts +3 -1
- package/dist/tools/ask.d.ts +3 -1
- package/dist/tools/diff.cjs +17 -6
- package/dist/tools/diff.cjs.map +1 -1
- package/dist/tools/diff.d.cts +5 -2
- package/dist/tools/diff.d.ts +5 -2
- package/dist/tools/diff.js +17 -6
- package/dist/tools/diff.js.map +1 -1
- package/dist/tools/directories.d.cts +3 -1
- package/dist/tools/directories.d.ts +3 -1
- package/dist/tools/explore.cjs +1778 -204
- package/dist/tools/explore.cjs.map +1 -1
- package/dist/tools/explore.d.cts +82 -2
- package/dist/tools/explore.d.ts +82 -2
- package/dist/tools/explore.js +1780 -204
- package/dist/tools/explore.js.map +1 -1
- package/dist/tools/files.cjs +56 -21
- package/dist/tools/files.cjs.map +1 -1
- package/dist/tools/files.d.cts +6 -3
- package/dist/tools/files.d.ts +6 -3
- package/dist/tools/files.js +56 -21
- package/dist/tools/files.js.map +1 -1
- package/dist/tools/git.cjs +34 -25
- package/dist/tools/git.cjs.map +1 -1
- package/dist/tools/git.d.cts +4 -204
- package/dist/tools/git.d.ts +4 -204
- package/dist/tools/git.js +34 -25
- package/dist/tools/git.js.map +1 -1
- package/dist/tools/media.cjs +280 -3
- package/dist/tools/media.cjs.map +1 -1
- package/dist/tools/media.d.cts +3 -1
- package/dist/tools/media.d.ts +3 -1
- package/dist/tools/media.js +278 -1
- package/dist/tools/media.js.map +1 -1
- package/dist/tools/plan.cjs +62 -0
- package/dist/tools/plan.cjs.map +1 -1
- package/dist/tools/plan.d.cts +4 -17
- package/dist/tools/plan.d.ts +4 -17
- package/dist/tools/plan.js +59 -0
- package/dist/tools/plan.js.map +1 -1
- package/dist/tools/search.cjs +3 -0
- package/dist/tools/search.cjs.map +1 -1
- package/dist/tools/search.d.cts +3 -1
- package/dist/tools/search.d.ts +3 -1
- package/dist/tools/search.js +3 -0
- package/dist/tools/search.js.map +1 -1
- package/dist/tools/shell.cjs +76 -11
- package/dist/tools/shell.cjs.map +1 -1
- package/dist/tools/shell.d.cts +5 -39
- package/dist/tools/shell.d.ts +5 -39
- package/dist/tools/shell.js +75 -11
- package/dist/tools/shell.js.map +1 -1
- package/dist/tools/todo.d.cts +3 -1
- package/dist/tools/todo.d.ts +3 -1
- package/dist/tools/web.cjs +279 -5
- package/dist/tools/web.cjs.map +1 -1
- package/dist/tools/web.d.cts +3 -1
- package/dist/tools/web.d.ts +3 -1
- package/dist/tools/web.js +275 -3
- package/dist/tools/web.js.map +1 -1
- package/dist/types-BYyXWWPW.d.cts +237 -0
- package/dist/types-BYyXWWPW.d.ts +237 -0
- package/package.json +26 -37
- package/bin/zqagent +0 -2
- package/dist/cli/index.cjs +0 -24507
- package/dist/cli/index.cjs.map +0 -1
- package/dist/cli/index.d.cts +0 -2
- package/dist/cli/index.d.ts +0 -2
- package/dist/cli/index.js +0 -24490
- package/dist/cli/index.js.map +0 -1
- package/dist/server/index.cjs +0 -19824
- package/dist/server/index.cjs.map +0 -1
- package/dist/server/index.d.cts +0 -2
- package/dist/server/index.d.ts +0 -2
- package/dist/server/index.js +0 -19807
- package/dist/server/index.js.map +0 -1
package/dist/tools/files.cjs
CHANGED
|
@@ -85,25 +85,24 @@ function resolveWithinRoot(workspace, target) {
|
|
|
85
85
|
|
|
86
86
|
// src/tools/files.ts
|
|
87
87
|
var DEFAULT_MAX_LINES = 2e3;
|
|
88
|
+
var MULTI_READ_MAX_TOTAL_LINES = 1500;
|
|
88
89
|
var MAX_LINE_CHARS = 500;
|
|
89
|
-
function makeReadFile(workspace) {
|
|
90
|
+
function makeReadFile(workspace, guard) {
|
|
90
91
|
const root = path2.resolve(workspace);
|
|
91
|
-
|
|
92
|
-
const filePath = String(args.path ?? "");
|
|
93
|
-
const startLine = Math.max(1, Number(args.start_line ?? 1));
|
|
94
|
-
const maxLines = Math.min(Number(args.max_lines ?? DEFAULT_MAX_LINES), DEFAULT_MAX_LINES);
|
|
92
|
+
function readOne(filePath, startLine, maxLines) {
|
|
95
93
|
const resolved = resolveWithinRoot(root, filePath);
|
|
96
94
|
if (!resolved) {
|
|
97
|
-
return { error: "\u8DEF\u5F84\u5728\u5DE5\u4F5C\u533A\u4E4B\u5916" };
|
|
95
|
+
return { path: filePath, error: "\u8DEF\u5F84\u5728\u5DE5\u4F5C\u533A\u4E4B\u5916" };
|
|
98
96
|
}
|
|
99
97
|
const target = resolved.real;
|
|
100
98
|
if (!fs2.existsSync(target) || !fs2.statSync(target).isFile()) {
|
|
101
|
-
return { error: `\u4E0D\u662F\u6587\u4EF6: ${filePath}` };
|
|
99
|
+
return { path: filePath, error: `\u4E0D\u662F\u6587\u4EF6: ${filePath}` };
|
|
102
100
|
}
|
|
103
101
|
const selected = [];
|
|
104
102
|
let total = 0;
|
|
105
103
|
try {
|
|
106
104
|
const content = fs2.readFileSync(target, "utf-8");
|
|
105
|
+
guard?.noteRead(target, content);
|
|
107
106
|
const lines = content.split("\n");
|
|
108
107
|
total = lines.length;
|
|
109
108
|
for (let i = startLine - 1; i < Math.min(startLine - 1 + maxLines, lines.length); i++) {
|
|
@@ -111,10 +110,10 @@ function makeReadFile(workspace) {
|
|
|
111
110
|
if (text.length > MAX_LINE_CHARS) {
|
|
112
111
|
text = text.slice(0, MAX_LINE_CHARS) + "\u2026 (line truncated)";
|
|
113
112
|
}
|
|
114
|
-
selected.push(`${
|
|
113
|
+
selected.push(`${i + 1}|${text}`);
|
|
115
114
|
}
|
|
116
115
|
} catch (exc) {
|
|
117
|
-
return { error: `\u8BFB\u53D6\u5931\u8D25: ${String(exc)}` };
|
|
116
|
+
return { path: filePath, error: `\u8BFB\u53D6\u5931\u8D25: ${String(exc)}` };
|
|
118
117
|
}
|
|
119
118
|
const end = startLine + selected.length - 1;
|
|
120
119
|
const result = {
|
|
@@ -128,9 +127,35 @@ function makeReadFile(workspace) {
|
|
|
128
127
|
result.note = `\u663E\u793A\u7B2C ${startLine}-${end} \u884C\uFF0C\u5171 ${total} \u884C\uFF1B\u8C03\u7528 read_file(start_line=${end + 1}) \u7EE7\u7EED\u9605\u8BFB`;
|
|
129
128
|
}
|
|
130
129
|
return result;
|
|
130
|
+
}
|
|
131
|
+
return function read_file(args) {
|
|
132
|
+
const startLine = Math.max(1, Number(args.start_line ?? 1));
|
|
133
|
+
const maxLines = Math.min(Number(args.max_lines ?? DEFAULT_MAX_LINES), DEFAULT_MAX_LINES);
|
|
134
|
+
const paths = Array.isArray(args.paths) ? args.paths.map((p) => String(p ?? "")).filter(Boolean) : [];
|
|
135
|
+
if (paths.length > 0) {
|
|
136
|
+
const files = [];
|
|
137
|
+
let remainingLines = MULTI_READ_MAX_TOTAL_LINES;
|
|
138
|
+
for (const filePath2 of paths) {
|
|
139
|
+
const perFile = Math.min(maxLines, Math.max(1, remainingLines));
|
|
140
|
+
const one = readOne(filePath2, startLine, perFile);
|
|
141
|
+
files.push(one);
|
|
142
|
+
if (!one.error) remainingLines -= one.content.split("\n").length;
|
|
143
|
+
if (remainingLines <= 0) break;
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
files,
|
|
147
|
+
count: files.length,
|
|
148
|
+
...files.length < paths.length ? { note: `\u5DF2\u8FBE\u5230\u5355\u6B21\u591A\u6587\u4EF6\u8BFB\u53D6\u4E0A\u9650\uFF08\u5171 ${MULTI_READ_MAX_TOTAL_LINES} \u884C\uFF09\uFF0C\u5176\u4F59\u8DEF\u5F84\u672A\u8BFB\u53D6` } : {}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
const filePath = String(args.path ?? "");
|
|
152
|
+
if (!filePath) {
|
|
153
|
+
return { error: "\u7F3A\u5C11 path\uFF08\u6216 paths\uFF09\u53C2\u6570" };
|
|
154
|
+
}
|
|
155
|
+
return readOne(filePath, startLine, maxLines);
|
|
131
156
|
};
|
|
132
157
|
}
|
|
133
|
-
function makeWriteFile(workspace) {
|
|
158
|
+
function makeWriteFile(workspace, guard) {
|
|
134
159
|
const root = path2.resolve(workspace);
|
|
135
160
|
return function write_file(args) {
|
|
136
161
|
const filePath = String(args.path ?? "");
|
|
@@ -147,9 +172,12 @@ function makeWriteFile(workspace) {
|
|
|
147
172
|
if (fs2.existsSync(target) && !overwrite) {
|
|
148
173
|
return { error: "\u6587\u4EF6\u5DF2\u5B58\u5728 (overwrite=false)" };
|
|
149
174
|
}
|
|
175
|
+
const conflict = guard?.checkWrite(target, filePath);
|
|
176
|
+
if (conflict) return { error: conflict.message };
|
|
150
177
|
try {
|
|
151
178
|
fs2.mkdirSync(path2.dirname(target), { recursive: true });
|
|
152
179
|
fs2.writeFileSync(target, content, "utf-8");
|
|
180
|
+
guard?.noteWrite(target, content);
|
|
153
181
|
} catch (exc) {
|
|
154
182
|
return { error: `\u5199\u5165\u5931\u8D25: ${String(exc)}` };
|
|
155
183
|
}
|
|
@@ -183,7 +211,7 @@ function makeListFiles(workspace) {
|
|
|
183
211
|
}
|
|
184
212
|
};
|
|
185
213
|
}
|
|
186
|
-
function makeReplaceInFile(workspace) {
|
|
214
|
+
function makeReplaceInFile(workspace, guard) {
|
|
187
215
|
const root = path2.resolve(workspace);
|
|
188
216
|
return function replace_in_file(args) {
|
|
189
217
|
const filePath = String(args.path ?? "");
|
|
@@ -197,6 +225,8 @@ function makeReplaceInFile(workspace) {
|
|
|
197
225
|
if (!fs2.existsSync(target) || !fs2.statSync(target).isFile()) {
|
|
198
226
|
return { error: `\u4E0D\u662F\u6587\u4EF6: ${filePath}` };
|
|
199
227
|
}
|
|
228
|
+
const conflict = guard?.checkWrite(target, filePath);
|
|
229
|
+
if (conflict) return { error: conflict.message };
|
|
200
230
|
try {
|
|
201
231
|
const content = fs2.readFileSync(target, "utf-8");
|
|
202
232
|
const count = content.split(oldStr).length - 1;
|
|
@@ -205,6 +235,7 @@ function makeReplaceInFile(workspace) {
|
|
|
205
235
|
}
|
|
206
236
|
const updated = content.replaceAll(oldStr, newStr);
|
|
207
237
|
fs2.writeFileSync(target, updated, "utf-8");
|
|
238
|
+
guard?.noteWrite(target, updated);
|
|
208
239
|
return {
|
|
209
240
|
path: path2.relative(resolved.root, target),
|
|
210
241
|
replacements: count,
|
|
@@ -216,8 +247,8 @@ function makeReplaceInFile(workspace) {
|
|
|
216
247
|
}
|
|
217
248
|
};
|
|
218
249
|
}
|
|
219
|
-
function registerReadOnlyFileTools(registry, workspace) {
|
|
220
|
-
const readFile = makeReadFile(workspace);
|
|
250
|
+
function registerReadOnlyFileTools(registry, workspace, guard) {
|
|
251
|
+
const readFile = makeReadFile(workspace, guard);
|
|
221
252
|
registry.register(
|
|
222
253
|
(args) => readFile(args),
|
|
223
254
|
{ category: "filesystem", riskLevel: "low", capabilities: ["read_file"], requiresApproval: false },
|
|
@@ -225,15 +256,19 @@ function registerReadOnlyFileTools(registry, workspace) {
|
|
|
225
256
|
type: "function",
|
|
226
257
|
function: {
|
|
227
258
|
name: "read_file",
|
|
228
|
-
description: "\u8BFB\u53D6\u6587\u672C\u6587\u4EF6\uFF0C\u8FD4\u56DE\u5E26\u884C\u53F7\u7684\u5185\u5BB9 (\u683C\u5F0F: '
|
|
259
|
+
description: "\u8BFB\u53D6\u6587\u672C\u6587\u4EF6\uFF0C\u8FD4\u56DE\u5E26\u884C\u53F7\u7684\u5185\u5BB9 (\u683C\u5F0F: '12|text')\uFF0C\u4EE5\u4FBF\u5F15\u7528\u4E3A path:line\u3002path \u4E0E paths \u81F3\u5C11\u4F20\u4E00\u4E2A\u3002\u5927\u6587\u4EF6\u7A97\u53E3\u5316\uFF1A\u4F20\u9012 start_line \u4ECE\u4E0A\u4E00\u6B21\u8BFB\u53D6\u7684\u4F4D\u7F6E\u7EE7\u7EED\u3002\u9700\u8981\u540C\u65F6\u67E5\u770B\u591A\u4E2A\u6587\u4EF6/\u533A\u95F4\u65F6\u7528 paths \u4E00\u6B21\u8BFB\u53D6\uFF0C\u51CF\u5C11\u5F80\u8FD4\u3002\u53EA\u8BFB\u3002",
|
|
229
260
|
parameters: {
|
|
230
261
|
type: "object",
|
|
231
262
|
properties: {
|
|
232
263
|
path: { type: "string", description: "\u6587\u4EF6\u8DEF\u5F84 (\u76F8\u5BF9\u4E8E\u5DE5\u4F5C\u533A)" },
|
|
264
|
+
paths: {
|
|
265
|
+
type: "array",
|
|
266
|
+
items: { type: "string" },
|
|
267
|
+
description: "\u4E00\u6B21\u8BFB\u53D6\u591A\u4E2A\u6587\u4EF6 (\u4E0E path \u4E8C\u9009\u4E00)\uFF1B\u591A\u4E2A\u5019\u9009\u6587\u4EF6\u5E94\u4E00\u6B21\u8BFB\u5B8C\uFF0C\u4E0D\u8981\u9010\u4E2A\u53D1\u8BF7\u6C42"
|
|
268
|
+
},
|
|
233
269
|
start_line: { type: "integer", description: "\u8D77\u59CB\u884C\u53F7 (\u4ECE1\u5F00\u59CB\uFF0C\u9ED8\u8BA41)" },
|
|
234
|
-
max_lines: { type: "integer", description: `\u6700\u5927\u884C\u6570 (\u9ED8\u8BA4 ${DEFAULT_MAX_LINES})` }
|
|
235
|
-
}
|
|
236
|
-
required: ["path"]
|
|
270
|
+
max_lines: { type: "integer", description: `\u6700\u5927\u884C\u6570 (\u9ED8\u8BA4 ${DEFAULT_MAX_LINES}\uFF0C\u591A\u6587\u4EF6\u5408\u8BA1\u4E0A\u9650 ${MULTI_READ_MAX_TOTAL_LINES})` }
|
|
271
|
+
}
|
|
237
272
|
}
|
|
238
273
|
}
|
|
239
274
|
}
|
|
@@ -258,9 +293,9 @@ function registerReadOnlyFileTools(registry, workspace) {
|
|
|
258
293
|
}
|
|
259
294
|
);
|
|
260
295
|
}
|
|
261
|
-
function registerFileTools(registry, workspace) {
|
|
262
|
-
registerReadOnlyFileTools(registry, workspace);
|
|
263
|
-
const writeFile = makeWriteFile(workspace);
|
|
296
|
+
function registerFileTools(registry, workspace, guard) {
|
|
297
|
+
registerReadOnlyFileTools(registry, workspace, guard);
|
|
298
|
+
const writeFile = makeWriteFile(workspace, guard);
|
|
264
299
|
registry.register(
|
|
265
300
|
(args) => writeFile(args),
|
|
266
301
|
{ category: "filesystem", riskLevel: "medium", capabilities: ["write_file"], requiresApproval: true },
|
|
@@ -281,7 +316,7 @@ function registerFileTools(registry, workspace) {
|
|
|
281
316
|
}
|
|
282
317
|
}
|
|
283
318
|
);
|
|
284
|
-
const replaceFile = makeReplaceInFile(workspace);
|
|
319
|
+
const replaceFile = makeReplaceInFile(workspace, guard);
|
|
285
320
|
registry.register(
|
|
286
321
|
(args) => replaceFile(args),
|
|
287
322
|
{ category: "filesystem", riskLevel: "medium", capabilities: ["edit_file"], requiresApproval: true },
|
package/dist/tools/files.cjs.map
CHANGED
|
@@ -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\";\n\n/** read_file 默认最大读取行数 */\nconst DEFAULT_MAX_LINES = 2000;\n/** 单行最大字符数(超长行截断,防止输出爆炸) */\nconst MAX_LINE_CHARS = 500;\n\n/**\n * read_file — 行号标注版 (对齐 Python 版)\n *\n * 返回带行号的内容,格式 ' 12\\ttext',便于引用为 path:line;\n * 大文件通过 start_line / max_lines 窗口化阅读。\n */\nfunction makeReadFile(workspace: string) {\n const root = path.resolve(workspace);\n\n return function read_file(args: Record<string, unknown>) {\n const filePath = String(args.path ?? \"\");\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 // 用解析后的真实路径做后续 fs 操作,避免校验后 symlink 组件被替换。\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).isFile()) {\n return { error: `不是文件: ${filePath}` };\n }\n\n // 按窗口截取行,超长行截断并标注\n const selected: string[] = [];\n let total = 0;\n try {\n const content = fs.readFileSync(target, \"utf-8\");\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(`${String(i + 1).padStart(6)}\\t${text}`);\n }\n } catch (exc) {\n return { 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\n/**\n * write_file — 路径范围检查 + UTF-8 写入\n *\n * 自动创建父目录;overwrite=false 时拒绝覆盖已存在文件。\n */\nfunction makeWriteFile(workspace: string) {\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 try {\n fs.mkdirSync(path.dirname(target), { recursive: true });\n fs.writeFileSync(target, content, \"utf-8\");\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) {\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 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 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(registry: ToolRegistry, workspace: string): void {\n const readFile = makeReadFile(workspace);\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\\\\\\\\ttext'),以便引用为 path:line。大文件窗口化:传递 start_line 从上一次读取的位置继续。只读。\",\n parameters: {\n type: \"object\",\n properties: {\n path: { type: \"string\", description: \"文件路径 (相对于工作区)\" },\n start_line: { type: \"integer\", description: \"起始行号 (从1开始,默认1)\" },\n max_lines: { type: \"integer\", description: `最大行数 (默认 ${DEFAULT_MAX_LINES})` },\n },\n required: [\"path\"],\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(registry: ToolRegistry, workspace: string): void {\n registerReadOnlyFileTools(registry, workspace);\n\n const writeFile = makeWriteFile(workspace);\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);\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 */\nfunction 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"]}
|
|
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"]}
|
package/dist/tools/files.d.cts
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
import { T as ToolRegistry } from '../ToolRegistry-
|
|
1
|
+
import { T as ToolRegistry } from '../ToolRegistry-DouDvn9E.cjs';
|
|
2
|
+
import { C as ConflictGuard } from '../conflictGuard-BBOOIxxK.cjs';
|
|
3
|
+
import '../types-BYyXWWPW.cjs';
|
|
4
|
+
import '@zqagent/shared/model-capabilities';
|
|
2
5
|
|
|
3
6
|
/**
|
|
4
7
|
* 注册只读文件工具(read_file / list_files)。
|
|
5
8
|
*
|
|
6
9
|
* 供 explore 子代理等「只读」场景使用,使声明的 schema 与实际能力一致。
|
|
7
10
|
*/
|
|
8
|
-
declare function registerReadOnlyFileTools(registry: ToolRegistry, workspace: string): void;
|
|
11
|
+
declare function registerReadOnlyFileTools(registry: ToolRegistry, workspace: string, guard?: ConflictGuard): void;
|
|
9
12
|
/**
|
|
10
13
|
* 注册文件工具到 ToolRegistry
|
|
11
14
|
*
|
|
12
15
|
* read_file / list_files 为低风险(无需审批);write_file / replace_in_file
|
|
13
16
|
* 为中风险且 requiresApproval=true(由权限引擎决定是否需要审批)。
|
|
14
17
|
*/
|
|
15
|
-
declare function registerFileTools(registry: ToolRegistry, workspace: string): void;
|
|
18
|
+
declare function registerFileTools(registry: ToolRegistry, workspace: string, guard?: ConflictGuard): void;
|
|
16
19
|
|
|
17
20
|
export { registerFileTools, registerReadOnlyFileTools };
|
package/dist/tools/files.d.ts
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
import { T as ToolRegistry } from '../ToolRegistry-
|
|
1
|
+
import { T as ToolRegistry } from '../ToolRegistry-Cy0fQxrb.js';
|
|
2
|
+
import { C as ConflictGuard } from '../conflictGuard-BBOOIxxK.js';
|
|
3
|
+
import '../types-BYyXWWPW.js';
|
|
4
|
+
import '@zqagent/shared/model-capabilities';
|
|
2
5
|
|
|
3
6
|
/**
|
|
4
7
|
* 注册只读文件工具(read_file / list_files)。
|
|
5
8
|
*
|
|
6
9
|
* 供 explore 子代理等「只读」场景使用,使声明的 schema 与实际能力一致。
|
|
7
10
|
*/
|
|
8
|
-
declare function registerReadOnlyFileTools(registry: ToolRegistry, workspace: string): void;
|
|
11
|
+
declare function registerReadOnlyFileTools(registry: ToolRegistry, workspace: string, guard?: ConflictGuard): void;
|
|
9
12
|
/**
|
|
10
13
|
* 注册文件工具到 ToolRegistry
|
|
11
14
|
*
|
|
12
15
|
* read_file / list_files 为低风险(无需审批);write_file / replace_in_file
|
|
13
16
|
* 为中风险且 requiresApproval=true(由权限引擎决定是否需要审批)。
|
|
14
17
|
*/
|
|
15
|
-
declare function registerFileTools(registry: ToolRegistry, workspace: string): void;
|
|
18
|
+
declare function registerFileTools(registry: ToolRegistry, workspace: string, guard?: ConflictGuard): void;
|
|
16
19
|
|
|
17
20
|
export { registerFileTools, registerReadOnlyFileTools };
|
package/dist/tools/files.js
CHANGED
|
@@ -50,25 +50,24 @@ function resolveWithinRoot(workspace, target) {
|
|
|
50
50
|
|
|
51
51
|
// src/tools/files.ts
|
|
52
52
|
var DEFAULT_MAX_LINES = 2e3;
|
|
53
|
+
var MULTI_READ_MAX_TOTAL_LINES = 1500;
|
|
53
54
|
var MAX_LINE_CHARS = 500;
|
|
54
|
-
function makeReadFile(workspace) {
|
|
55
|
+
function makeReadFile(workspace, guard) {
|
|
55
56
|
const root = path2.resolve(workspace);
|
|
56
|
-
|
|
57
|
-
const filePath = String(args.path ?? "");
|
|
58
|
-
const startLine = Math.max(1, Number(args.start_line ?? 1));
|
|
59
|
-
const maxLines = Math.min(Number(args.max_lines ?? DEFAULT_MAX_LINES), DEFAULT_MAX_LINES);
|
|
57
|
+
function readOne(filePath, startLine, maxLines) {
|
|
60
58
|
const resolved = resolveWithinRoot(root, filePath);
|
|
61
59
|
if (!resolved) {
|
|
62
|
-
return { error: "\u8DEF\u5F84\u5728\u5DE5\u4F5C\u533A\u4E4B\u5916" };
|
|
60
|
+
return { path: filePath, error: "\u8DEF\u5F84\u5728\u5DE5\u4F5C\u533A\u4E4B\u5916" };
|
|
63
61
|
}
|
|
64
62
|
const target = resolved.real;
|
|
65
63
|
if (!fs2.existsSync(target) || !fs2.statSync(target).isFile()) {
|
|
66
|
-
return { error: `\u4E0D\u662F\u6587\u4EF6: ${filePath}` };
|
|
64
|
+
return { path: filePath, error: `\u4E0D\u662F\u6587\u4EF6: ${filePath}` };
|
|
67
65
|
}
|
|
68
66
|
const selected = [];
|
|
69
67
|
let total = 0;
|
|
70
68
|
try {
|
|
71
69
|
const content = fs2.readFileSync(target, "utf-8");
|
|
70
|
+
guard?.noteRead(target, content);
|
|
72
71
|
const lines = content.split("\n");
|
|
73
72
|
total = lines.length;
|
|
74
73
|
for (let i = startLine - 1; i < Math.min(startLine - 1 + maxLines, lines.length); i++) {
|
|
@@ -76,10 +75,10 @@ function makeReadFile(workspace) {
|
|
|
76
75
|
if (text.length > MAX_LINE_CHARS) {
|
|
77
76
|
text = text.slice(0, MAX_LINE_CHARS) + "\u2026 (line truncated)";
|
|
78
77
|
}
|
|
79
|
-
selected.push(`${
|
|
78
|
+
selected.push(`${i + 1}|${text}`);
|
|
80
79
|
}
|
|
81
80
|
} catch (exc) {
|
|
82
|
-
return { error: `\u8BFB\u53D6\u5931\u8D25: ${String(exc)}` };
|
|
81
|
+
return { path: filePath, error: `\u8BFB\u53D6\u5931\u8D25: ${String(exc)}` };
|
|
83
82
|
}
|
|
84
83
|
const end = startLine + selected.length - 1;
|
|
85
84
|
const result = {
|
|
@@ -93,9 +92,35 @@ function makeReadFile(workspace) {
|
|
|
93
92
|
result.note = `\u663E\u793A\u7B2C ${startLine}-${end} \u884C\uFF0C\u5171 ${total} \u884C\uFF1B\u8C03\u7528 read_file(start_line=${end + 1}) \u7EE7\u7EED\u9605\u8BFB`;
|
|
94
93
|
}
|
|
95
94
|
return result;
|
|
95
|
+
}
|
|
96
|
+
return function read_file(args) {
|
|
97
|
+
const startLine = Math.max(1, Number(args.start_line ?? 1));
|
|
98
|
+
const maxLines = Math.min(Number(args.max_lines ?? DEFAULT_MAX_LINES), DEFAULT_MAX_LINES);
|
|
99
|
+
const paths = Array.isArray(args.paths) ? args.paths.map((p) => String(p ?? "")).filter(Boolean) : [];
|
|
100
|
+
if (paths.length > 0) {
|
|
101
|
+
const files = [];
|
|
102
|
+
let remainingLines = MULTI_READ_MAX_TOTAL_LINES;
|
|
103
|
+
for (const filePath2 of paths) {
|
|
104
|
+
const perFile = Math.min(maxLines, Math.max(1, remainingLines));
|
|
105
|
+
const one = readOne(filePath2, startLine, perFile);
|
|
106
|
+
files.push(one);
|
|
107
|
+
if (!one.error) remainingLines -= one.content.split("\n").length;
|
|
108
|
+
if (remainingLines <= 0) break;
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
files,
|
|
112
|
+
count: files.length,
|
|
113
|
+
...files.length < paths.length ? { note: `\u5DF2\u8FBE\u5230\u5355\u6B21\u591A\u6587\u4EF6\u8BFB\u53D6\u4E0A\u9650\uFF08\u5171 ${MULTI_READ_MAX_TOTAL_LINES} \u884C\uFF09\uFF0C\u5176\u4F59\u8DEF\u5F84\u672A\u8BFB\u53D6` } : {}
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const filePath = String(args.path ?? "");
|
|
117
|
+
if (!filePath) {
|
|
118
|
+
return { error: "\u7F3A\u5C11 path\uFF08\u6216 paths\uFF09\u53C2\u6570" };
|
|
119
|
+
}
|
|
120
|
+
return readOne(filePath, startLine, maxLines);
|
|
96
121
|
};
|
|
97
122
|
}
|
|
98
|
-
function makeWriteFile(workspace) {
|
|
123
|
+
function makeWriteFile(workspace, guard) {
|
|
99
124
|
const root = path2.resolve(workspace);
|
|
100
125
|
return function write_file(args) {
|
|
101
126
|
const filePath = String(args.path ?? "");
|
|
@@ -112,9 +137,12 @@ function makeWriteFile(workspace) {
|
|
|
112
137
|
if (fs2.existsSync(target) && !overwrite) {
|
|
113
138
|
return { error: "\u6587\u4EF6\u5DF2\u5B58\u5728 (overwrite=false)" };
|
|
114
139
|
}
|
|
140
|
+
const conflict = guard?.checkWrite(target, filePath);
|
|
141
|
+
if (conflict) return { error: conflict.message };
|
|
115
142
|
try {
|
|
116
143
|
fs2.mkdirSync(path2.dirname(target), { recursive: true });
|
|
117
144
|
fs2.writeFileSync(target, content, "utf-8");
|
|
145
|
+
guard?.noteWrite(target, content);
|
|
118
146
|
} catch (exc) {
|
|
119
147
|
return { error: `\u5199\u5165\u5931\u8D25: ${String(exc)}` };
|
|
120
148
|
}
|
|
@@ -148,7 +176,7 @@ function makeListFiles(workspace) {
|
|
|
148
176
|
}
|
|
149
177
|
};
|
|
150
178
|
}
|
|
151
|
-
function makeReplaceInFile(workspace) {
|
|
179
|
+
function makeReplaceInFile(workspace, guard) {
|
|
152
180
|
const root = path2.resolve(workspace);
|
|
153
181
|
return function replace_in_file(args) {
|
|
154
182
|
const filePath = String(args.path ?? "");
|
|
@@ -162,6 +190,8 @@ function makeReplaceInFile(workspace) {
|
|
|
162
190
|
if (!fs2.existsSync(target) || !fs2.statSync(target).isFile()) {
|
|
163
191
|
return { error: `\u4E0D\u662F\u6587\u4EF6: ${filePath}` };
|
|
164
192
|
}
|
|
193
|
+
const conflict = guard?.checkWrite(target, filePath);
|
|
194
|
+
if (conflict) return { error: conflict.message };
|
|
165
195
|
try {
|
|
166
196
|
const content = fs2.readFileSync(target, "utf-8");
|
|
167
197
|
const count = content.split(oldStr).length - 1;
|
|
@@ -170,6 +200,7 @@ function makeReplaceInFile(workspace) {
|
|
|
170
200
|
}
|
|
171
201
|
const updated = content.replaceAll(oldStr, newStr);
|
|
172
202
|
fs2.writeFileSync(target, updated, "utf-8");
|
|
203
|
+
guard?.noteWrite(target, updated);
|
|
173
204
|
return {
|
|
174
205
|
path: path2.relative(resolved.root, target),
|
|
175
206
|
replacements: count,
|
|
@@ -181,8 +212,8 @@ function makeReplaceInFile(workspace) {
|
|
|
181
212
|
}
|
|
182
213
|
};
|
|
183
214
|
}
|
|
184
|
-
function registerReadOnlyFileTools(registry, workspace) {
|
|
185
|
-
const readFile = makeReadFile(workspace);
|
|
215
|
+
function registerReadOnlyFileTools(registry, workspace, guard) {
|
|
216
|
+
const readFile = makeReadFile(workspace, guard);
|
|
186
217
|
registry.register(
|
|
187
218
|
(args) => readFile(args),
|
|
188
219
|
{ category: "filesystem", riskLevel: "low", capabilities: ["read_file"], requiresApproval: false },
|
|
@@ -190,15 +221,19 @@ function registerReadOnlyFileTools(registry, workspace) {
|
|
|
190
221
|
type: "function",
|
|
191
222
|
function: {
|
|
192
223
|
name: "read_file",
|
|
193
|
-
description: "\u8BFB\u53D6\u6587\u672C\u6587\u4EF6\uFF0C\u8FD4\u56DE\u5E26\u884C\u53F7\u7684\u5185\u5BB9 (\u683C\u5F0F: '
|
|
224
|
+
description: "\u8BFB\u53D6\u6587\u672C\u6587\u4EF6\uFF0C\u8FD4\u56DE\u5E26\u884C\u53F7\u7684\u5185\u5BB9 (\u683C\u5F0F: '12|text')\uFF0C\u4EE5\u4FBF\u5F15\u7528\u4E3A path:line\u3002path \u4E0E paths \u81F3\u5C11\u4F20\u4E00\u4E2A\u3002\u5927\u6587\u4EF6\u7A97\u53E3\u5316\uFF1A\u4F20\u9012 start_line \u4ECE\u4E0A\u4E00\u6B21\u8BFB\u53D6\u7684\u4F4D\u7F6E\u7EE7\u7EED\u3002\u9700\u8981\u540C\u65F6\u67E5\u770B\u591A\u4E2A\u6587\u4EF6/\u533A\u95F4\u65F6\u7528 paths \u4E00\u6B21\u8BFB\u53D6\uFF0C\u51CF\u5C11\u5F80\u8FD4\u3002\u53EA\u8BFB\u3002",
|
|
194
225
|
parameters: {
|
|
195
226
|
type: "object",
|
|
196
227
|
properties: {
|
|
197
228
|
path: { type: "string", description: "\u6587\u4EF6\u8DEF\u5F84 (\u76F8\u5BF9\u4E8E\u5DE5\u4F5C\u533A)" },
|
|
229
|
+
paths: {
|
|
230
|
+
type: "array",
|
|
231
|
+
items: { type: "string" },
|
|
232
|
+
description: "\u4E00\u6B21\u8BFB\u53D6\u591A\u4E2A\u6587\u4EF6 (\u4E0E path \u4E8C\u9009\u4E00)\uFF1B\u591A\u4E2A\u5019\u9009\u6587\u4EF6\u5E94\u4E00\u6B21\u8BFB\u5B8C\uFF0C\u4E0D\u8981\u9010\u4E2A\u53D1\u8BF7\u6C42"
|
|
233
|
+
},
|
|
198
234
|
start_line: { type: "integer", description: "\u8D77\u59CB\u884C\u53F7 (\u4ECE1\u5F00\u59CB\uFF0C\u9ED8\u8BA41)" },
|
|
199
|
-
max_lines: { type: "integer", description: `\u6700\u5927\u884C\u6570 (\u9ED8\u8BA4 ${DEFAULT_MAX_LINES})` }
|
|
200
|
-
}
|
|
201
|
-
required: ["path"]
|
|
235
|
+
max_lines: { type: "integer", description: `\u6700\u5927\u884C\u6570 (\u9ED8\u8BA4 ${DEFAULT_MAX_LINES}\uFF0C\u591A\u6587\u4EF6\u5408\u8BA1\u4E0A\u9650 ${MULTI_READ_MAX_TOTAL_LINES})` }
|
|
236
|
+
}
|
|
202
237
|
}
|
|
203
238
|
}
|
|
204
239
|
}
|
|
@@ -223,9 +258,9 @@ function registerReadOnlyFileTools(registry, workspace) {
|
|
|
223
258
|
}
|
|
224
259
|
);
|
|
225
260
|
}
|
|
226
|
-
function registerFileTools(registry, workspace) {
|
|
227
|
-
registerReadOnlyFileTools(registry, workspace);
|
|
228
|
-
const writeFile = makeWriteFile(workspace);
|
|
261
|
+
function registerFileTools(registry, workspace, guard) {
|
|
262
|
+
registerReadOnlyFileTools(registry, workspace, guard);
|
|
263
|
+
const writeFile = makeWriteFile(workspace, guard);
|
|
229
264
|
registry.register(
|
|
230
265
|
(args) => writeFile(args),
|
|
231
266
|
{ category: "filesystem", riskLevel: "medium", capabilities: ["write_file"], requiresApproval: true },
|
|
@@ -246,7 +281,7 @@ function registerFileTools(registry, workspace) {
|
|
|
246
281
|
}
|
|
247
282
|
}
|
|
248
283
|
);
|
|
249
|
-
const replaceFile = makeReplaceInFile(workspace);
|
|
284
|
+
const replaceFile = makeReplaceInFile(workspace, guard);
|
|
250
285
|
registry.register(
|
|
251
286
|
(args) => replaceFile(args),
|
|
252
287
|
{ category: "filesystem", riskLevel: "medium", capabilities: ["edit_file"], requiresApproval: true },
|