minimal-agent 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -1
- package/plugins/workflow-runner/plugin.js +27 -15
- package/plugins/workflow-runner/plugin.ts +34 -20
- package/plugins/workflow-runner/src/agentCommon.js +13 -12
- package/plugins/workflow-runner/src/agentCommon.ts +22 -13
- package/plugins/workflow-runner/src/index.js +5 -3
- package/plugins/workflow-runner/src/index.ts +31 -13
- package/plugins/workflow-runner/src/multimodal.js +2 -1
- package/plugins/workflow-runner/src/multimodal.ts +2 -1
- package/plugins/workflow-runner/src/stepExecutors/skill.js +5 -6
- package/plugins/workflow-runner/src/stepExecutors/skill.ts +12 -7
- package/plugins/workflow-runner/src/stepExecutors/tool.js +68 -57
- package/plugins/workflow-runner/src/stepExecutors/tool.ts +88 -66
- package/plugins/workflow-runner/src/toolRuntime.js +48 -0
- package/plugins/workflow-runner/src/toolRuntime.ts +64 -0
- package/plugins/workflow-runner/src/types.ts +26 -7
- package/plugins/workflow-runner/test/context-files.test.ts +47 -16
- package/plugins/workflow-runner/test/hostCapability.test.ts +9 -3
- package/plugins/workflow-runner/test/multimodal.test.ts +61 -10
- package/plugins/workflow-runner/test/toolRuntimeIntegration.test.ts +168 -0
- package/src/cli/print.js +19 -4
- package/src/cli/print.ts +17 -4
- package/src/cli/streamJson.js +19 -4
- package/src/cli/streamJson.ts +17 -4
- package/src/config/configDefaults.js +25 -0
- package/src/config/configDefaults.ts +33 -0
- package/src/config/configFile.js +2 -21
- package/src/config/configFile.ts +11 -24
- package/src/config/configManagement.js +1 -1
- package/src/config/configManagement.ts +2 -2
- package/src/context/compact.js +4 -0
- package/src/context/compact.ts +4 -0
- package/src/context/contextBundle.js +38 -13
- package/src/context/contextBundle.ts +47 -13
- package/src/harness/stallGuard.js +19 -4
- package/src/harness/stallGuard.ts +30 -7
- package/src/loop.js +95 -11
- package/src/loop.ts +125 -16
- package/src/main.js +8 -1
- package/src/main.tsx +7 -1
- package/src/memory/constants.js +10 -0
- package/src/memory/constants.ts +13 -0
- package/src/memory/episodicMemory.js +274 -2
- package/src/memory/episodicMemory.ts +306 -1
- package/src/memory/fileStore.js +24 -4
- package/src/memory/fileStore.ts +22 -2
- package/src/memory/memoryService.js +79 -12
- package/src/memory/memoryService.ts +108 -9
- package/src/memory/repository.js +415 -85
- package/src/memory/repository.ts +437 -86
- package/src/memory/turnJournal.js +95 -0
- package/src/memory/turnJournal.ts +120 -0
- package/src/memory/types.ts +16 -0
- package/src/plugins/pluginApi.ts +30 -6
- package/src/plugins/pluginRunner.js +6 -2
- package/src/plugins/pluginRunner.ts +12 -7
- package/src/prompts/policyRegistry.js +27 -0
- package/src/prompts/policyRegistry.ts +27 -0
- package/src/prompts/promptAssembly.js +39 -7
- package/src/prompts/promptAssembly.ts +145 -59
- package/src/session/runAgentTurn.js +43 -21
- package/src/session/runAgentTurn.ts +68 -26
- package/src/session/runTurn.js +11 -46
- package/src/session/runTurn.ts +12 -46
- package/src/session/runtimeStore.js +149 -0
- package/src/session/runtimeStore.ts +197 -0
- package/src/session/sessionState.js +5 -0
- package/src/session/sessionState.ts +14 -0
- package/src/tools/agent/agent.js +143 -14
- package/src/tools/agent/agent.ts +149 -14
- package/src/tools/agent/agentTaskService.js +60 -0
- package/src/tools/agent/agentTaskService.ts +113 -0
- package/src/tools/agent/agentTypes.js +5 -0
- package/src/tools/agent/agentTypes.ts +5 -0
- package/src/tools/bash/bash.js +31 -11
- package/src/tools/bash/bash.ts +29 -10
- package/src/tools/capabilities.js +5 -0
- package/src/tools/capabilities.ts +11 -1
- package/src/tools/edit/edit.js +7 -4
- package/src/tools/edit/edit.ts +7 -4
- package/src/tools/edit/multi-edit.js +4 -2
- package/src/tools/edit/multi-edit.ts +4 -2
- package/src/tools/glob/glob.js +156 -40
- package/src/tools/glob/glob.ts +185 -46
- package/src/tools/index.js +35 -17
- package/src/tools/index.ts +81 -20
- package/src/tools/lsp/lsp.js +165 -22
- package/src/tools/lsp/lsp.ts +206 -36
- package/src/tools/powershell/powershell.js +116 -26
- package/src/tools/powershell/powershell.ts +155 -30
- package/src/tools/presentresult/presentresult.js +10 -0
- package/src/tools/presentresult/presentresult.ts +10 -0
- package/src/tools/read/read.js +244 -29
- package/src/tools/read/read.ts +391 -59
- package/src/tools/shared/diffSummary.js +44 -0
- package/src/tools/shared/diffSummary.ts +63 -0
- package/src/tools/shared/fileActivity.js +2 -1
- package/src/tools/shared/fileActivity.ts +3 -0
- package/src/tools/shared/permanentDeleteGuard.js +26 -0
- package/src/tools/shared/permanentDeleteGuard.ts +34 -0
- package/src/tools/shared/toolOutputStore.js +97 -0
- package/src/tools/shared/toolOutputStore.ts +110 -0
- package/src/tools/skill/skill.js +86 -0
- package/src/tools/skill/skill.ts +94 -0
- package/src/tools/snip/snip.js +67 -0
- package/src/tools/snip/snip.ts +80 -0
- package/src/tools/tasks/backgroundTaskService.js +79 -28
- package/src/tools/tasks/backgroundTaskService.ts +165 -49
- package/src/tools/tasks/taskManagement.js +163 -0
- package/src/tools/tasks/taskManagement.ts +180 -0
- package/src/tools/tasks/taskOutput.js +64 -14
- package/src/tools/tasks/taskOutput.ts +102 -23
- package/src/tools/tasks/taskStop.js +48 -8
- package/src/tools/tasks/taskStop.ts +69 -9
- package/src/tools/tasks/taskStore.js +46 -0
- package/src/tools/tasks/taskStore.ts +77 -0
- package/src/tools/terminal/terminalCapture.js +167 -0
- package/src/tools/terminal/terminalCapture.ts +179 -0
- package/src/tools/trash/trash.js +233 -0
- package/src/tools/trash/trash.ts +283 -0
- package/src/tools/worktree/sessionWorktree.js +128 -0
- package/src/tools/worktree/sessionWorktree.ts +158 -0
- package/src/tools/write/write.js +6 -2
- package/src/tools/write/write.ts +6 -2
- package/src/types.ts +12 -0
- package/src/ui/hooks/useChat.js +31 -11
- package/src/ui/hooks/useChat.ts +31 -13
- package/src/web/protocol.ts +78 -3
package/src/tools/read/read.ts
CHANGED
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
import { createReadStream } from 'node:fs';
|
|
2
|
-
import { open, readFile, stat } from 'node:fs/promises';
|
|
2
|
+
import { open, readFile, stat, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { extname } from 'node:path';
|
|
4
4
|
import { createInterface } from 'node:readline';
|
|
5
|
+
import { execFile } from 'node:child_process';
|
|
6
|
+
import { mkdir } from 'node:fs/promises';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { promisify } from 'node:util';
|
|
9
|
+
import { randomUUID } from 'node:crypto';
|
|
5
10
|
import { z } from 'zod';
|
|
6
11
|
|
|
7
12
|
import type { Tool, ToolCallContext, ToolResult } from '../../types.js';
|
|
8
13
|
import { toToolParameters } from '../../utils/zodToJson.js';
|
|
9
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
DEFAULT_MAX_RESULT_SIZE_CHARS,
|
|
16
|
+
MAX_FILE_SIZE_BYTES,
|
|
17
|
+
MAX_LINES_TO_READ,
|
|
18
|
+
} from '../types.js';
|
|
10
19
|
import { fileActivityMetadata } from '../shared/fileActivity.js';
|
|
11
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
hashFileBytes,
|
|
22
|
+
recordRead,
|
|
23
|
+
type FileReadState,
|
|
24
|
+
} from '../shared/fileState.js';
|
|
12
25
|
import {
|
|
13
26
|
hasBinaryExtension,
|
|
14
27
|
isBlockedDevicePath,
|
|
@@ -16,19 +29,48 @@ import {
|
|
|
16
29
|
resolveToolPath,
|
|
17
30
|
} from '../shared/fileUtils.js';
|
|
18
31
|
import { filePathField } from '../shared/schemas.js';
|
|
32
|
+
import {
|
|
33
|
+
pruneRuntimeOutputs,
|
|
34
|
+
sessionRuntimeDir,
|
|
35
|
+
type RuntimeIdentity,
|
|
36
|
+
} from '../../session/runtimeStore.js';
|
|
19
37
|
|
|
20
38
|
const inputSchema = z.object({
|
|
21
39
|
file_path: filePathField('读取'),
|
|
22
|
-
offset: z
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
40
|
+
offset: z
|
|
41
|
+
.number()
|
|
42
|
+
.int()
|
|
43
|
+
.positive()
|
|
44
|
+
.optional()
|
|
45
|
+
.describe(
|
|
46
|
+
'文本文件为 1-indexed 起始行;Jupyter Notebook 为 1-indexed 起始 cell',
|
|
47
|
+
),
|
|
48
|
+
limit: z
|
|
49
|
+
.number()
|
|
50
|
+
.int()
|
|
51
|
+
.positive()
|
|
52
|
+
.optional()
|
|
53
|
+
.describe(`最多读取的文本行或 Notebook cells;默认 ${MAX_LINES_TO_READ}`),
|
|
54
|
+
pages: z
|
|
55
|
+
.string()
|
|
56
|
+
.regex(/^\d+(?:-\d+)?(?:,\d+(?:-\d+)?)*$/)
|
|
57
|
+
.optional()
|
|
58
|
+
.describe('PDF 页码,例如 "1-5,8";每次最多 20 页'),
|
|
59
|
+
pdf_mode: z
|
|
60
|
+
.enum(['auto', 'text', 'visual', 'both'])
|
|
61
|
+
.default('auto')
|
|
62
|
+
.optional(),
|
|
28
63
|
});
|
|
29
64
|
type ReadInput = z.infer<typeof inputSchema>;
|
|
30
65
|
|
|
31
|
-
const IMAGE_EXTENSIONS = new Set([
|
|
66
|
+
const IMAGE_EXTENSIONS = new Set([
|
|
67
|
+
'.png',
|
|
68
|
+
'.jpg',
|
|
69
|
+
'.jpeg',
|
|
70
|
+
'.gif',
|
|
71
|
+
'.webp',
|
|
72
|
+
'.bmp',
|
|
73
|
+
]);
|
|
32
74
|
const MAX_IMAGE_BYTES = 25 * 1024 * 1024;
|
|
33
75
|
const MAX_NOTEBOOK_BYTES = 32 * 1024 * 1024;
|
|
34
76
|
const SMALL_TEXT_BYTES = 16 * 1024 * 1024;
|
|
@@ -38,36 +80,55 @@ const description = `Reads local text files, images, and Jupyter notebooks.
|
|
|
38
80
|
Usage:
|
|
39
81
|
- Relative paths resolve against the current working directory.
|
|
40
82
|
- Text results use 1-based line numbers. Use offset/limit for targeted ranges.
|
|
41
|
-
- PNG, JPEG, WebP, GIF,
|
|
83
|
+
- PNG, JPEG, WebP, GIF, and BMP files are attached for visual inspection.
|
|
42
84
|
- For .ipynb files, offset/limit select cells; results include cell id, type, source, and bounded outputs.
|
|
43
|
-
- PDF
|
|
85
|
+
- PDF supports page-selected text extraction and optional visual page rendering; office files use the matching project Skill.
|
|
44
86
|
- This tool reads files, not directories; use Glob for directory discovery.`;
|
|
45
87
|
|
|
46
88
|
function notebookSource(value: unknown): string {
|
|
47
|
-
if (Array.isArray(value))
|
|
89
|
+
if (Array.isArray(value))
|
|
90
|
+
return value
|
|
91
|
+
.filter((item): item is string => typeof item === 'string')
|
|
92
|
+
.join('');
|
|
48
93
|
return typeof value === 'string' ? value : '';
|
|
49
94
|
}
|
|
50
95
|
|
|
51
|
-
function renderNotebook(
|
|
96
|
+
function renderNotebook(
|
|
97
|
+
raw: string,
|
|
98
|
+
offset: number,
|
|
99
|
+
limit: number,
|
|
100
|
+
): {
|
|
52
101
|
content: string;
|
|
53
102
|
total: number;
|
|
54
103
|
partial: boolean;
|
|
55
104
|
} {
|
|
56
|
-
const parsed = JSON.parse(raw) as {
|
|
105
|
+
const parsed = JSON.parse(raw) as {
|
|
106
|
+
cells?: unknown[];
|
|
107
|
+
metadata?: Record<string, unknown>;
|
|
108
|
+
};
|
|
57
109
|
if (!Array.isArray(parsed.cells)) throw new Error('Notebook 缺少 cells 数组');
|
|
58
110
|
const start = offset - 1;
|
|
59
111
|
const cells = parsed.cells.slice(start, start + limit);
|
|
60
112
|
const parts = cells.map((value, index) => {
|
|
61
|
-
const cell =
|
|
113
|
+
const cell =
|
|
114
|
+
value && typeof value === 'object'
|
|
115
|
+
? (value as Record<string, unknown>)
|
|
116
|
+
: {};
|
|
62
117
|
const outputs = Array.isArray(cell.outputs) ? cell.outputs : [];
|
|
63
|
-
const renderedOutputs = outputs
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
118
|
+
const renderedOutputs = outputs
|
|
119
|
+
.slice(0, 8)
|
|
120
|
+
.map((output) => {
|
|
121
|
+
if (!output || typeof output !== 'object') return String(output ?? '');
|
|
122
|
+
const item = output as Record<string, unknown>;
|
|
123
|
+
const text =
|
|
124
|
+
item.text ??
|
|
125
|
+
(item.data && typeof item.data === 'object'
|
|
126
|
+
? (item.data as Record<string, unknown>)['text/plain']
|
|
127
|
+
: undefined);
|
|
128
|
+
return notebookSource(text).slice(0, 2_000);
|
|
129
|
+
})
|
|
130
|
+
.filter(Boolean)
|
|
131
|
+
.join('\n');
|
|
71
132
|
return [
|
|
72
133
|
`## Cell ${start + index + 1} [selector=cell-${start + index + 1}${typeof cell.id === 'string' ? `; id=${cell.id}` : ''}; type=${String(cell.cell_type ?? 'unknown')}]`,
|
|
73
134
|
notebookSource(cell.source),
|
|
@@ -75,8 +136,15 @@ function renderNotebook(raw: string, offset: number, limit: number): {
|
|
|
75
136
|
].join('\n');
|
|
76
137
|
});
|
|
77
138
|
const end = start + cells.length;
|
|
78
|
-
if (end < parsed.cells.length)
|
|
79
|
-
|
|
139
|
+
if (end < parsed.cells.length)
|
|
140
|
+
parts.push(
|
|
141
|
+
`... (${parsed.cells.length - end} cells remain; continue with offset=${end + 1})`,
|
|
142
|
+
);
|
|
143
|
+
return {
|
|
144
|
+
content: parts.join('\n\n'),
|
|
145
|
+
total: parsed.cells.length,
|
|
146
|
+
partial: start > 0 || end < parsed.cells.length,
|
|
147
|
+
};
|
|
80
148
|
}
|
|
81
149
|
|
|
82
150
|
async function readStreamedText(
|
|
@@ -91,7 +159,14 @@ async function readStreamedText(
|
|
|
91
159
|
const utf16 = bytesRead >= 2 && prefix[0] === 0xff && prefix[1] === 0xfe;
|
|
92
160
|
const stream = createReadStream(filePath, {
|
|
93
161
|
encoding: utf16 ? 'utf16le' : 'utf8',
|
|
94
|
-
start: utf16
|
|
162
|
+
start: utf16
|
|
163
|
+
? 2
|
|
164
|
+
: bytesRead >= 3 &&
|
|
165
|
+
prefix[0] === 0xef &&
|
|
166
|
+
prefix[1] === 0xbb &&
|
|
167
|
+
prefix[2] === 0xbf
|
|
168
|
+
? 3
|
|
169
|
+
: 0,
|
|
95
170
|
});
|
|
96
171
|
const rl = createInterface({ input: stream, crlfDelay: Infinity });
|
|
97
172
|
const lines: string[] = [];
|
|
@@ -110,61 +185,193 @@ async function readStreamedText(
|
|
|
110
185
|
return { numbered: lines.join('\n'), totalKnown: line, hasMore };
|
|
111
186
|
}
|
|
112
187
|
|
|
113
|
-
async function call(
|
|
188
|
+
async function call(
|
|
189
|
+
input: ReadInput,
|
|
190
|
+
state?: FileReadState,
|
|
191
|
+
cwd?: string,
|
|
192
|
+
runtimeIdentity?: RuntimeIdentity,
|
|
193
|
+
): Promise<ToolResult> {
|
|
114
194
|
const offset = input.offset ?? 1;
|
|
115
195
|
const limit = input.limit ?? MAX_LINES_TO_READ;
|
|
116
196
|
const resolved = resolveToolPath(input.file_path, cwd);
|
|
117
197
|
if (!resolved.ok) return resolved;
|
|
118
198
|
const filePath = resolved.resolvedPath;
|
|
119
|
-
if (isBlockedDevicePath(filePath))
|
|
199
|
+
if (isBlockedDevicePath(filePath))
|
|
200
|
+
return { ok: false, error: `不允许读取设备文件:${filePath}` };
|
|
120
201
|
|
|
121
202
|
const info = await stat(filePath).catch((error: Error) => ({ error }));
|
|
122
|
-
if ('error' in info)
|
|
123
|
-
|
|
203
|
+
if ('error' in info)
|
|
204
|
+
return {
|
|
205
|
+
ok: false,
|
|
206
|
+
error: `文件不存在或无法读取:${filePath}(${info.error.message})`,
|
|
207
|
+
};
|
|
208
|
+
if (!info.isFile())
|
|
209
|
+
return { ok: false, error: `${filePath} 不是文件;目录请用 Glob。` };
|
|
124
210
|
const ext = extname(filePath).toLowerCase();
|
|
125
211
|
|
|
212
|
+
if (ext === '.pdf') {
|
|
213
|
+
try {
|
|
214
|
+
const bytes = await readFile(filePath);
|
|
215
|
+
if (bytes.subarray(0, 5).toString() !== '%PDF-') {
|
|
216
|
+
return {
|
|
217
|
+
ok: false,
|
|
218
|
+
error: '文件扩展名是 PDF,但缺少有效 %PDF- 文件头',
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
const pdfjs = await import('pdfjs-dist/legacy/build/pdf.mjs');
|
|
222
|
+
const document = await pdfjs.getDocument({
|
|
223
|
+
data: new Uint8Array(bytes),
|
|
224
|
+
useSystemFonts: true,
|
|
225
|
+
}).promise;
|
|
226
|
+
try {
|
|
227
|
+
const requested = parsePdfPages(input.pages, document.numPages);
|
|
228
|
+
if (requested.length > 20)
|
|
229
|
+
return { ok: false, error: '一次最多读取 20 个 PDF 页面' };
|
|
230
|
+
const chunks: string[] = [];
|
|
231
|
+
for (const pageNumber of requested) {
|
|
232
|
+
const page = await document.getPage(pageNumber);
|
|
233
|
+
const text = await page.getTextContent();
|
|
234
|
+
chunks.push(
|
|
235
|
+
`## Page ${pageNumber}\n${text.items
|
|
236
|
+
.map((item) =>
|
|
237
|
+
'str' in item && typeof item.str === 'string' ? item.str : '',
|
|
238
|
+
)
|
|
239
|
+
.filter(Boolean)
|
|
240
|
+
.join(' ')}`,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
const mode = input.pdf_mode ?? 'auto';
|
|
244
|
+
const sparse =
|
|
245
|
+
chunks.join('').replace(/\s/g, '').length < requested.length * 80;
|
|
246
|
+
const wantVisual =
|
|
247
|
+
mode === 'visual' || mode === 'both' || (mode === 'auto' && sparse);
|
|
248
|
+
const images = wantVisual
|
|
249
|
+
? await renderPdfPages(
|
|
250
|
+
document,
|
|
251
|
+
filePath,
|
|
252
|
+
requested.slice(0, 10),
|
|
253
|
+
runtimeIdentity,
|
|
254
|
+
)
|
|
255
|
+
: [];
|
|
256
|
+
recordRead(filePath, { contentHash: hashFileBytes(bytes) }, state);
|
|
257
|
+
const rawText =
|
|
258
|
+
mode === 'visual' ? '[visual-only mode]' : chunks.join('\n\n');
|
|
259
|
+
const text =
|
|
260
|
+
rawText.length > DEFAULT_MAX_RESULT_SIZE_CHARS
|
|
261
|
+
? `${rawText.slice(0, DEFAULT_MAX_RESULT_SIZE_CHARS)}\n... (PDF 文本输出已截断;请缩小 pages 范围继续读取)`
|
|
262
|
+
: rawText;
|
|
263
|
+
return {
|
|
264
|
+
ok: true,
|
|
265
|
+
content: `PDF ${document.numPages} pages; selected ${requested.join(', ')}.\n\n${text}${wantVisual && !images.length ? '\n\n视觉渲染不可用(Canvas 与 pdftoppm 均不可用);文本提取结果仍可用。' : ''}`,
|
|
266
|
+
metadata: await fileActivityMetadata(
|
|
267
|
+
filePath,
|
|
268
|
+
'read',
|
|
269
|
+
'not_run',
|
|
270
|
+
'source',
|
|
271
|
+
cwd,
|
|
272
|
+
),
|
|
273
|
+
...(images.length
|
|
274
|
+
? {
|
|
275
|
+
visionImages: images.map((source) => ({ source })),
|
|
276
|
+
visionNote:
|
|
277
|
+
'这是所选 PDF 页面的视觉渲染,请检查版式、图表和扫描内容。',
|
|
278
|
+
}
|
|
279
|
+
: {}),
|
|
280
|
+
};
|
|
281
|
+
} finally {
|
|
282
|
+
await document.destroy().catch(() => undefined);
|
|
283
|
+
}
|
|
284
|
+
} catch (error) {
|
|
285
|
+
const message = (error as Error).message;
|
|
286
|
+
return {
|
|
287
|
+
ok: false,
|
|
288
|
+
error: `PDF 解析失败${/password/i.test(message) ? '(文件可能已加密)' : ''}:${message}`,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
126
293
|
if (IMAGE_EXTENSIONS.has(ext)) {
|
|
127
|
-
if (info.size > MAX_IMAGE_BYTES)
|
|
294
|
+
if (info.size > MAX_IMAGE_BYTES)
|
|
295
|
+
return { ok: false, error: `图片超过 ${MAX_IMAGE_BYTES} 字节上限` };
|
|
128
296
|
const bytes = await readFile(filePath);
|
|
129
297
|
recordRead(filePath, { contentHash: hashFileBytes(bytes) }, state);
|
|
130
298
|
return {
|
|
131
299
|
ok: true,
|
|
132
300
|
content: `已读取图片:${filePath}(${info.size} 字节)。图像已附加供视觉模型检查;非视觉模型只能确认文件元数据,不能解读像素内容。`,
|
|
133
|
-
metadata: await fileActivityMetadata(
|
|
301
|
+
metadata: await fileActivityMetadata(
|
|
302
|
+
filePath,
|
|
303
|
+
'read',
|
|
304
|
+
'not_run',
|
|
305
|
+
'source',
|
|
306
|
+
cwd,
|
|
307
|
+
),
|
|
134
308
|
visionImages: [{ source: filePath }],
|
|
135
309
|
visionNote: '这是 Read 工具读取的本地图片,请直接检查图像内容。',
|
|
136
310
|
};
|
|
137
311
|
}
|
|
138
312
|
|
|
139
313
|
if (ext === '.ipynb') {
|
|
140
|
-
if (info.size > MAX_NOTEBOOK_BYTES)
|
|
314
|
+
if (info.size > MAX_NOTEBOOK_BYTES)
|
|
315
|
+
return {
|
|
316
|
+
ok: false,
|
|
317
|
+
error: `Notebook 超过 ${MAX_NOTEBOOK_BYTES} 字节上限`,
|
|
318
|
+
};
|
|
141
319
|
try {
|
|
142
320
|
const snapshot = await readTextFile(filePath);
|
|
143
|
-
const rendered = renderNotebook(
|
|
321
|
+
const rendered = renderNotebook(
|
|
322
|
+
snapshot.content,
|
|
323
|
+
offset,
|
|
324
|
+
Math.min(limit, 200),
|
|
325
|
+
);
|
|
144
326
|
let content = rendered.content;
|
|
145
327
|
let truncated = false;
|
|
146
328
|
if (content.length > DEFAULT_MAX_RESULT_SIZE_CHARS) {
|
|
147
329
|
content = `${content.slice(0, DEFAULT_MAX_RESULT_SIZE_CHARS)}\n\n... (Notebook 输出已截断)`;
|
|
148
330
|
truncated = true;
|
|
149
331
|
}
|
|
150
|
-
recordRead(
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
332
|
+
recordRead(
|
|
333
|
+
filePath,
|
|
334
|
+
{
|
|
335
|
+
offset,
|
|
336
|
+
limit,
|
|
337
|
+
partial: rendered.partial,
|
|
338
|
+
truncated,
|
|
339
|
+
...(!rendered.partial && !truncated
|
|
340
|
+
? { contentHash: hashFileBytes(snapshot.bytes) }
|
|
341
|
+
: {}),
|
|
342
|
+
},
|
|
343
|
+
state,
|
|
344
|
+
);
|
|
345
|
+
return {
|
|
346
|
+
ok: true,
|
|
347
|
+
content,
|
|
348
|
+
metadata: await fileActivityMetadata(
|
|
349
|
+
filePath,
|
|
350
|
+
'read',
|
|
351
|
+
'not_run',
|
|
352
|
+
'source',
|
|
353
|
+
cwd,
|
|
354
|
+
),
|
|
355
|
+
};
|
|
158
356
|
} catch (error) {
|
|
159
|
-
return {
|
|
357
|
+
return {
|
|
358
|
+
ok: false,
|
|
359
|
+
error: `Notebook 解析失败:${(error as Error).message}`,
|
|
360
|
+
};
|
|
160
361
|
}
|
|
161
362
|
}
|
|
162
363
|
|
|
163
364
|
if (hasBinaryExtension(filePath)) {
|
|
164
|
-
return {
|
|
365
|
+
return {
|
|
366
|
+
ok: false,
|
|
367
|
+
error: `不支持直接读取 ${ext || '该二进制格式'};PDF/Office 文件请使用对应 Skill。`,
|
|
368
|
+
};
|
|
165
369
|
}
|
|
166
370
|
if (info.size > MAX_FILE_SIZE_BYTES) {
|
|
167
|
-
return {
|
|
371
|
+
return {
|
|
372
|
+
ok: false,
|
|
373
|
+
error: `文件超过 ${MAX_FILE_SIZE_BYTES} 字节硬上限。请用 Grep 定位,或用 Bash 做流式处理。`,
|
|
374
|
+
};
|
|
168
375
|
}
|
|
169
376
|
|
|
170
377
|
try {
|
|
@@ -175,10 +382,13 @@ async function call(input: ReadInput, state?: FileReadState, cwd?: string): Prom
|
|
|
175
382
|
if (info.size <= SMALL_TEXT_BYTES) {
|
|
176
383
|
const snapshot = await readTextFile(filePath);
|
|
177
384
|
completeBytes = snapshot.bytes;
|
|
178
|
-
const all =
|
|
385
|
+
const all =
|
|
386
|
+
snapshot.content.length === 0 ? [] : snapshot.content.split('\n');
|
|
179
387
|
const start = offset - 1;
|
|
180
388
|
const selected = all.slice(start, start + limit);
|
|
181
|
-
content = selected
|
|
389
|
+
content = selected
|
|
390
|
+
.map((line, index) => `${start + index + 1}\t${line}`)
|
|
391
|
+
.join('\n');
|
|
182
392
|
totalLines = all.length;
|
|
183
393
|
hasMore = start + selected.length < all.length;
|
|
184
394
|
} else {
|
|
@@ -194,24 +404,145 @@ async function call(input: ReadInput, state?: FileReadState, cwd?: string): Prom
|
|
|
194
404
|
truncated = true;
|
|
195
405
|
}
|
|
196
406
|
const partial = offset !== 1 || hasMore;
|
|
197
|
-
if (hasMore)
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
407
|
+
if (hasMore)
|
|
408
|
+
content += `\n\n... (还有更多内容;继续使用 offset=${offset + limit})`;
|
|
409
|
+
const reads = recordRead(
|
|
410
|
+
filePath,
|
|
411
|
+
{
|
|
412
|
+
offset,
|
|
413
|
+
limit,
|
|
414
|
+
partial,
|
|
415
|
+
truncated,
|
|
416
|
+
...(!partial && !truncated && completeBytes
|
|
417
|
+
? { contentHash: hashFileBytes(completeBytes) }
|
|
418
|
+
: {}),
|
|
419
|
+
},
|
|
420
|
+
state,
|
|
421
|
+
);
|
|
205
422
|
if (info.size > 100 * 1024 && offset === 1) {
|
|
206
423
|
content = `⚠️ 大文件:结果受行数和输出字符上限约束,请按需分段读取。\n\n${content}`;
|
|
207
424
|
}
|
|
208
|
-
if (reads >= 2)
|
|
209
|
-
|
|
425
|
+
if (reads >= 2)
|
|
426
|
+
content = `⚠️ 重复读取空转:该区间未变化,前一次 Read 结果仍然有效。\n\n${content}`;
|
|
427
|
+
return {
|
|
428
|
+
ok: true,
|
|
429
|
+
content,
|
|
430
|
+
metadata: await fileActivityMetadata(
|
|
431
|
+
filePath,
|
|
432
|
+
'read',
|
|
433
|
+
'not_run',
|
|
434
|
+
'source',
|
|
435
|
+
cwd,
|
|
436
|
+
),
|
|
437
|
+
};
|
|
210
438
|
} catch (error) {
|
|
211
439
|
return { ok: false, error: `读取失败:${(error as Error).message}` };
|
|
212
440
|
}
|
|
213
441
|
}
|
|
214
442
|
|
|
443
|
+
async function renderPdfPages(
|
|
444
|
+
document: Awaited<
|
|
445
|
+
ReturnType<
|
|
446
|
+
(typeof import('pdfjs-dist/legacy/build/pdf.mjs'))['getDocument']
|
|
447
|
+
>
|
|
448
|
+
>['promise'] extends Promise<infer T>
|
|
449
|
+
? T
|
|
450
|
+
: never,
|
|
451
|
+
filePath: string,
|
|
452
|
+
pages: number[],
|
|
453
|
+
identity?: RuntimeIdentity,
|
|
454
|
+
): Promise<string[]> {
|
|
455
|
+
const outputDir = join(sessionRuntimeDir(identity ?? {}), 'outputs');
|
|
456
|
+
const outputPrefix = `pdf_${randomUUID()}`;
|
|
457
|
+
await mkdir(outputDir, { recursive: true, mode: 0o700 });
|
|
458
|
+
const images: string[] = [];
|
|
459
|
+
const dynamicImport = new Function(
|
|
460
|
+
'specifier',
|
|
461
|
+
'return import(specifier)',
|
|
462
|
+
) as (specifier: string) => Promise<{
|
|
463
|
+
createCanvas?: (
|
|
464
|
+
width: number,
|
|
465
|
+
height: number,
|
|
466
|
+
) => {
|
|
467
|
+
getContext(type: '2d'): unknown;
|
|
468
|
+
toBuffer(type: 'image/png'): Buffer;
|
|
469
|
+
};
|
|
470
|
+
}>;
|
|
471
|
+
let createCanvas: Awaited<ReturnType<typeof dynamicImport>>['createCanvas'];
|
|
472
|
+
try {
|
|
473
|
+
({ createCanvas } = await dynamicImport('@napi-rs/canvas'));
|
|
474
|
+
} catch {
|
|
475
|
+
/* optional adapter */
|
|
476
|
+
}
|
|
477
|
+
if (createCanvas) {
|
|
478
|
+
try {
|
|
479
|
+
for (const pageNumber of pages) {
|
|
480
|
+
const page = await document.getPage(pageNumber);
|
|
481
|
+
const viewport = page.getViewport({ scale: 2 });
|
|
482
|
+
const canvas = createCanvas(
|
|
483
|
+
Math.ceil(viewport.width),
|
|
484
|
+
Math.ceil(viewport.height),
|
|
485
|
+
);
|
|
486
|
+
await page.render({
|
|
487
|
+
canvasContext: canvas.getContext('2d') as never,
|
|
488
|
+
viewport,
|
|
489
|
+
}).promise;
|
|
490
|
+
const target = join(
|
|
491
|
+
outputDir,
|
|
492
|
+
`${outputPrefix}-page-${pageNumber}.png`,
|
|
493
|
+
);
|
|
494
|
+
await writeFile(target, canvas.toBuffer('image/png'), { mode: 0o600 });
|
|
495
|
+
images.push(target);
|
|
496
|
+
}
|
|
497
|
+
void pruneRuntimeOutputs();
|
|
498
|
+
return images;
|
|
499
|
+
} catch {
|
|
500
|
+
images.length = 0;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
for (const pageNumber of pages) {
|
|
504
|
+
const prefix = join(outputDir, `${outputPrefix}-page-${pageNumber}`);
|
|
505
|
+
try {
|
|
506
|
+
await promisify(execFile)(
|
|
507
|
+
'pdftoppm',
|
|
508
|
+
[
|
|
509
|
+
'-f',
|
|
510
|
+
String(pageNumber),
|
|
511
|
+
'-l',
|
|
512
|
+
String(pageNumber),
|
|
513
|
+
'-singlefile',
|
|
514
|
+
'-png',
|
|
515
|
+
'-r',
|
|
516
|
+
'144',
|
|
517
|
+
filePath,
|
|
518
|
+
prefix,
|
|
519
|
+
],
|
|
520
|
+
{ maxBuffer: 1024 * 1024 },
|
|
521
|
+
);
|
|
522
|
+
images.push(`${prefix}.png`);
|
|
523
|
+
} catch {
|
|
524
|
+
break;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
void pruneRuntimeOutputs();
|
|
528
|
+
return images;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function parsePdfPages(spec: string | undefined, total: number): number[] {
|
|
532
|
+
if (!spec)
|
|
533
|
+
return Array.from({ length: Math.min(total, 20) }, (_, index) => index + 1);
|
|
534
|
+
const pages = new Set<number>();
|
|
535
|
+
for (const part of spec.split(',')) {
|
|
536
|
+
const [first, last] = part.split('-').map(Number);
|
|
537
|
+
for (let page = first!; page <= (last ?? first!); page += 1) {
|
|
538
|
+
if (page < 1 || page > total)
|
|
539
|
+
throw new Error(`PDF 页码超出范围:${page}(共 ${total} 页)`);
|
|
540
|
+
pages.add(page);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return [...pages].sort((left, right) => left - right);
|
|
544
|
+
}
|
|
545
|
+
|
|
215
546
|
export const readTool: Tool<ReadInput> = {
|
|
216
547
|
name: 'Read',
|
|
217
548
|
description,
|
|
@@ -222,5 +553,6 @@ export const readTool: Tool<ReadInput> = {
|
|
|
222
553
|
maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
|
|
223
554
|
interruptBehavior: () => 'cancel',
|
|
224
555
|
call: (input) => call(input),
|
|
225
|
-
callWithContext: (input, _signal, context: ToolCallContext) =>
|
|
556
|
+
callWithContext: (input, _signal, context: ToolCallContext) =>
|
|
557
|
+
call(input, context.sessionState?.fileReads, context.cwd, context),
|
|
226
558
|
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { createTwoFilesPatch, diffLines } from 'diff';
|
|
3
|
+
const MAX_BYTES = 5 * 1024 * 1024;
|
|
4
|
+
const MAX_LINES = 200_000;
|
|
5
|
+
const MAX_DIFF_CHARS = 16_000;
|
|
6
|
+
function hash(value) {
|
|
7
|
+
return createHash('sha256').update(value).digest('hex');
|
|
8
|
+
}
|
|
9
|
+
export function buildPatchSummary(before, after, label) {
|
|
10
|
+
const beforeHash = hash(before);
|
|
11
|
+
const afterHash = hash(after);
|
|
12
|
+
const totalBytes = Buffer.byteLength(before) + Buffer.byteLength(after);
|
|
13
|
+
const totalLines = (before.match(/\n/g)?.length ?? 0) + (after.match(/\n/g)?.length ?? 0) + 2;
|
|
14
|
+
if (totalBytes > MAX_BYTES || totalLines > MAX_LINES) {
|
|
15
|
+
return {
|
|
16
|
+
beforeHash,
|
|
17
|
+
afterHash,
|
|
18
|
+
additions: 0,
|
|
19
|
+
deletions: 0,
|
|
20
|
+
truncated: true,
|
|
21
|
+
omittedReason: 'too_large',
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const changes = diffLines(before, after);
|
|
25
|
+
const additions = changes
|
|
26
|
+
.filter((change) => change.added)
|
|
27
|
+
.reduce((sum, change) => sum + (change.count ?? 0), 0);
|
|
28
|
+
const deletions = changes
|
|
29
|
+
.filter((change) => change.removed)
|
|
30
|
+
.reduce((sum, change) => sum + (change.count ?? 0), 0);
|
|
31
|
+
let unifiedDiff = createTwoFilesPatch(`a/${label}`, `b/${label}`, before, after, undefined, undefined, { context: 3 });
|
|
32
|
+
const truncated = unifiedDiff.length > MAX_DIFF_CHARS;
|
|
33
|
+
if (truncated) {
|
|
34
|
+
unifiedDiff = `${unifiedDiff.slice(0, MAX_DIFF_CHARS)}\n... (diff preview truncated)`;
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
unifiedDiff,
|
|
38
|
+
beforeHash,
|
|
39
|
+
afterHash,
|
|
40
|
+
additions,
|
|
41
|
+
deletions,
|
|
42
|
+
truncated,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { createTwoFilesPatch, diffLines } from 'diff';
|
|
3
|
+
|
|
4
|
+
import type { FilePatchSummary } from '../../types.js';
|
|
5
|
+
|
|
6
|
+
const MAX_BYTES = 5 * 1024 * 1024;
|
|
7
|
+
const MAX_LINES = 200_000;
|
|
8
|
+
const MAX_DIFF_CHARS = 16_000;
|
|
9
|
+
|
|
10
|
+
function hash(value: string): string {
|
|
11
|
+
return createHash('sha256').update(value).digest('hex');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function buildPatchSummary(
|
|
15
|
+
before: string,
|
|
16
|
+
after: string,
|
|
17
|
+
label: string,
|
|
18
|
+
): FilePatchSummary {
|
|
19
|
+
const beforeHash = hash(before);
|
|
20
|
+
const afterHash = hash(after);
|
|
21
|
+
const totalBytes = Buffer.byteLength(before) + Buffer.byteLength(after);
|
|
22
|
+
const totalLines =
|
|
23
|
+
(before.match(/\n/g)?.length ?? 0) + (after.match(/\n/g)?.length ?? 0) + 2;
|
|
24
|
+
if (totalBytes > MAX_BYTES || totalLines > MAX_LINES) {
|
|
25
|
+
return {
|
|
26
|
+
beforeHash,
|
|
27
|
+
afterHash,
|
|
28
|
+
additions: 0,
|
|
29
|
+
deletions: 0,
|
|
30
|
+
truncated: true,
|
|
31
|
+
omittedReason: 'too_large',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const changes = diffLines(before, after);
|
|
36
|
+
const additions = changes
|
|
37
|
+
.filter((change) => change.added)
|
|
38
|
+
.reduce((sum, change) => sum + (change.count ?? 0), 0);
|
|
39
|
+
const deletions = changes
|
|
40
|
+
.filter((change) => change.removed)
|
|
41
|
+
.reduce((sum, change) => sum + (change.count ?? 0), 0);
|
|
42
|
+
let unifiedDiff = createTwoFilesPatch(
|
|
43
|
+
`a/${label}`,
|
|
44
|
+
`b/${label}`,
|
|
45
|
+
before,
|
|
46
|
+
after,
|
|
47
|
+
undefined,
|
|
48
|
+
undefined,
|
|
49
|
+
{ context: 3 },
|
|
50
|
+
);
|
|
51
|
+
const truncated = unifiedDiff.length > MAX_DIFF_CHARS;
|
|
52
|
+
if (truncated) {
|
|
53
|
+
unifiedDiff = `${unifiedDiff.slice(0, MAX_DIFF_CHARS)}\n... (diff preview truncated)`;
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
unifiedDiff,
|
|
57
|
+
beforeHash,
|
|
58
|
+
afterHash,
|
|
59
|
+
additions,
|
|
60
|
+
deletions,
|
|
61
|
+
truncated,
|
|
62
|
+
};
|
|
63
|
+
}
|