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.js
CHANGED
|
@@ -1,20 +1,52 @@
|
|
|
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
|
import { toToolParameters } from '../../utils/zodToJson.js';
|
|
7
|
-
import { DEFAULT_MAX_RESULT_SIZE_CHARS, MAX_FILE_SIZE_BYTES, MAX_LINES_TO_READ } from '../types.js';
|
|
12
|
+
import { DEFAULT_MAX_RESULT_SIZE_CHARS, MAX_FILE_SIZE_BYTES, MAX_LINES_TO_READ, } from '../types.js';
|
|
8
13
|
import { fileActivityMetadata } from '../shared/fileActivity.js';
|
|
9
|
-
import { hashFileBytes, recordRead } from '../shared/fileState.js';
|
|
14
|
+
import { hashFileBytes, recordRead, } from '../shared/fileState.js';
|
|
10
15
|
import { hasBinaryExtension, isBlockedDevicePath, readTextFile, resolveToolPath, } from '../shared/fileUtils.js';
|
|
11
16
|
import { filePathField } from '../shared/schemas.js';
|
|
17
|
+
import { pruneRuntimeOutputs, sessionRuntimeDir, } from '../../session/runtimeStore.js';
|
|
12
18
|
const inputSchema = z.object({
|
|
13
19
|
file_path: filePathField('读取'),
|
|
14
|
-
offset: z
|
|
15
|
-
|
|
20
|
+
offset: z
|
|
21
|
+
.number()
|
|
22
|
+
.int()
|
|
23
|
+
.positive()
|
|
24
|
+
.optional()
|
|
25
|
+
.describe('文本文件为 1-indexed 起始行;Jupyter Notebook 为 1-indexed 起始 cell'),
|
|
26
|
+
limit: z
|
|
27
|
+
.number()
|
|
28
|
+
.int()
|
|
29
|
+
.positive()
|
|
30
|
+
.optional()
|
|
31
|
+
.describe(`最多读取的文本行或 Notebook cells;默认 ${MAX_LINES_TO_READ}`),
|
|
32
|
+
pages: z
|
|
33
|
+
.string()
|
|
34
|
+
.regex(/^\d+(?:-\d+)?(?:,\d+(?:-\d+)?)*$/)
|
|
35
|
+
.optional()
|
|
36
|
+
.describe('PDF 页码,例如 "1-5,8";每次最多 20 页'),
|
|
37
|
+
pdf_mode: z
|
|
38
|
+
.enum(['auto', 'text', 'visual', 'both'])
|
|
39
|
+
.default('auto')
|
|
40
|
+
.optional(),
|
|
16
41
|
});
|
|
17
|
-
const IMAGE_EXTENSIONS = new Set([
|
|
42
|
+
const IMAGE_EXTENSIONS = new Set([
|
|
43
|
+
'.png',
|
|
44
|
+
'.jpg',
|
|
45
|
+
'.jpeg',
|
|
46
|
+
'.gif',
|
|
47
|
+
'.webp',
|
|
48
|
+
'.bmp',
|
|
49
|
+
]);
|
|
18
50
|
const MAX_IMAGE_BYTES = 25 * 1024 * 1024;
|
|
19
51
|
const MAX_NOTEBOOK_BYTES = 32 * 1024 * 1024;
|
|
20
52
|
const SMALL_TEXT_BYTES = 16 * 1024 * 1024;
|
|
@@ -23,13 +55,15 @@ const description = `Reads local text files, images, and Jupyter notebooks.
|
|
|
23
55
|
Usage:
|
|
24
56
|
- Relative paths resolve against the current working directory.
|
|
25
57
|
- Text results use 1-based line numbers. Use offset/limit for targeted ranges.
|
|
26
|
-
- PNG, JPEG, WebP, GIF,
|
|
58
|
+
- PNG, JPEG, WebP, GIF, and BMP files are attached for visual inspection.
|
|
27
59
|
- For .ipynb files, offset/limit select cells; results include cell id, type, source, and bounded outputs.
|
|
28
|
-
- PDF
|
|
60
|
+
- PDF supports page-selected text extraction and optional visual page rendering; office files use the matching project Skill.
|
|
29
61
|
- This tool reads files, not directories; use Glob for directory discovery.`;
|
|
30
62
|
function notebookSource(value) {
|
|
31
63
|
if (Array.isArray(value))
|
|
32
|
-
return value
|
|
64
|
+
return value
|
|
65
|
+
.filter((item) => typeof item === 'string')
|
|
66
|
+
.join('');
|
|
33
67
|
return typeof value === 'string' ? value : '';
|
|
34
68
|
}
|
|
35
69
|
function renderNotebook(raw, offset, limit) {
|
|
@@ -39,17 +73,24 @@ function renderNotebook(raw, offset, limit) {
|
|
|
39
73
|
const start = offset - 1;
|
|
40
74
|
const cells = parsed.cells.slice(start, start + limit);
|
|
41
75
|
const parts = cells.map((value, index) => {
|
|
42
|
-
const cell = value && typeof value === 'object'
|
|
76
|
+
const cell = value && typeof value === 'object'
|
|
77
|
+
? value
|
|
78
|
+
: {};
|
|
43
79
|
const outputs = Array.isArray(cell.outputs) ? cell.outputs : [];
|
|
44
|
-
const renderedOutputs = outputs
|
|
80
|
+
const renderedOutputs = outputs
|
|
81
|
+
.slice(0, 8)
|
|
82
|
+
.map((output) => {
|
|
45
83
|
if (!output || typeof output !== 'object')
|
|
46
84
|
return String(output ?? '');
|
|
47
85
|
const item = output;
|
|
48
|
-
const text = item.text ??
|
|
49
|
-
|
|
50
|
-
|
|
86
|
+
const text = item.text ??
|
|
87
|
+
(item.data && typeof item.data === 'object'
|
|
88
|
+
? item.data['text/plain']
|
|
89
|
+
: undefined);
|
|
51
90
|
return notebookSource(text).slice(0, 2_000);
|
|
52
|
-
})
|
|
91
|
+
})
|
|
92
|
+
.filter(Boolean)
|
|
93
|
+
.join('\n');
|
|
53
94
|
return [
|
|
54
95
|
`## Cell ${start + index + 1} [selector=cell-${start + index + 1}${typeof cell.id === 'string' ? `; id=${cell.id}` : ''}; type=${String(cell.cell_type ?? 'unknown')}]`,
|
|
55
96
|
notebookSource(cell.source),
|
|
@@ -59,7 +100,11 @@ function renderNotebook(raw, offset, limit) {
|
|
|
59
100
|
const end = start + cells.length;
|
|
60
101
|
if (end < parsed.cells.length)
|
|
61
102
|
parts.push(`... (${parsed.cells.length - end} cells remain; continue with offset=${end + 1})`);
|
|
62
|
-
return {
|
|
103
|
+
return {
|
|
104
|
+
content: parts.join('\n\n'),
|
|
105
|
+
total: parsed.cells.length,
|
|
106
|
+
partial: start > 0 || end < parsed.cells.length,
|
|
107
|
+
};
|
|
63
108
|
}
|
|
64
109
|
async function readStreamedText(filePath, offset, limit) {
|
|
65
110
|
const handle = await open(filePath, 'r');
|
|
@@ -69,7 +114,14 @@ async function readStreamedText(filePath, offset, limit) {
|
|
|
69
114
|
const utf16 = bytesRead >= 2 && prefix[0] === 0xff && prefix[1] === 0xfe;
|
|
70
115
|
const stream = createReadStream(filePath, {
|
|
71
116
|
encoding: utf16 ? 'utf16le' : 'utf8',
|
|
72
|
-
start: utf16
|
|
117
|
+
start: utf16
|
|
118
|
+
? 2
|
|
119
|
+
: bytesRead >= 3 &&
|
|
120
|
+
prefix[0] === 0xef &&
|
|
121
|
+
prefix[1] === 0xbb &&
|
|
122
|
+
prefix[2] === 0xbf
|
|
123
|
+
? 3
|
|
124
|
+
: 0,
|
|
73
125
|
});
|
|
74
126
|
const rl = createInterface({ input: stream, crlfDelay: Infinity });
|
|
75
127
|
const lines = [];
|
|
@@ -88,7 +140,7 @@ async function readStreamedText(filePath, offset, limit) {
|
|
|
88
140
|
}
|
|
89
141
|
return { numbered: lines.join('\n'), totalKnown: line, hasMore };
|
|
90
142
|
}
|
|
91
|
-
async function call(input, state, cwd) {
|
|
143
|
+
async function call(input, state, cwd, runtimeIdentity) {
|
|
92
144
|
const offset = input.offset ?? 1;
|
|
93
145
|
const limit = input.limit ?? MAX_LINES_TO_READ;
|
|
94
146
|
const resolved = resolveToolPath(input.file_path, cwd);
|
|
@@ -99,10 +151,75 @@ async function call(input, state, cwd) {
|
|
|
99
151
|
return { ok: false, error: `不允许读取设备文件:${filePath}` };
|
|
100
152
|
const info = await stat(filePath).catch((error) => ({ error }));
|
|
101
153
|
if ('error' in info)
|
|
102
|
-
return {
|
|
154
|
+
return {
|
|
155
|
+
ok: false,
|
|
156
|
+
error: `文件不存在或无法读取:${filePath}(${info.error.message})`,
|
|
157
|
+
};
|
|
103
158
|
if (!info.isFile())
|
|
104
159
|
return { ok: false, error: `${filePath} 不是文件;目录请用 Glob。` };
|
|
105
160
|
const ext = extname(filePath).toLowerCase();
|
|
161
|
+
if (ext === '.pdf') {
|
|
162
|
+
try {
|
|
163
|
+
const bytes = await readFile(filePath);
|
|
164
|
+
if (bytes.subarray(0, 5).toString() !== '%PDF-') {
|
|
165
|
+
return {
|
|
166
|
+
ok: false,
|
|
167
|
+
error: '文件扩展名是 PDF,但缺少有效 %PDF- 文件头',
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
const pdfjs = await import('pdfjs-dist/legacy/build/pdf.mjs');
|
|
171
|
+
const document = await pdfjs.getDocument({
|
|
172
|
+
data: new Uint8Array(bytes),
|
|
173
|
+
useSystemFonts: true,
|
|
174
|
+
}).promise;
|
|
175
|
+
try {
|
|
176
|
+
const requested = parsePdfPages(input.pages, document.numPages);
|
|
177
|
+
if (requested.length > 20)
|
|
178
|
+
return { ok: false, error: '一次最多读取 20 个 PDF 页面' };
|
|
179
|
+
const chunks = [];
|
|
180
|
+
for (const pageNumber of requested) {
|
|
181
|
+
const page = await document.getPage(pageNumber);
|
|
182
|
+
const text = await page.getTextContent();
|
|
183
|
+
chunks.push(`## Page ${pageNumber}\n${text.items
|
|
184
|
+
.map((item) => 'str' in item && typeof item.str === 'string' ? item.str : '')
|
|
185
|
+
.filter(Boolean)
|
|
186
|
+
.join(' ')}`);
|
|
187
|
+
}
|
|
188
|
+
const mode = input.pdf_mode ?? 'auto';
|
|
189
|
+
const sparse = chunks.join('').replace(/\s/g, '').length < requested.length * 80;
|
|
190
|
+
const wantVisual = mode === 'visual' || mode === 'both' || (mode === 'auto' && sparse);
|
|
191
|
+
const images = wantVisual
|
|
192
|
+
? await renderPdfPages(document, filePath, requested.slice(0, 10), runtimeIdentity)
|
|
193
|
+
: [];
|
|
194
|
+
recordRead(filePath, { contentHash: hashFileBytes(bytes) }, state);
|
|
195
|
+
const rawText = mode === 'visual' ? '[visual-only mode]' : chunks.join('\n\n');
|
|
196
|
+
const text = rawText.length > DEFAULT_MAX_RESULT_SIZE_CHARS
|
|
197
|
+
? `${rawText.slice(0, DEFAULT_MAX_RESULT_SIZE_CHARS)}\n... (PDF 文本输出已截断;请缩小 pages 范围继续读取)`
|
|
198
|
+
: rawText;
|
|
199
|
+
return {
|
|
200
|
+
ok: true,
|
|
201
|
+
content: `PDF ${document.numPages} pages; selected ${requested.join(', ')}.\n\n${text}${wantVisual && !images.length ? '\n\n视觉渲染不可用(Canvas 与 pdftoppm 均不可用);文本提取结果仍可用。' : ''}`,
|
|
202
|
+
metadata: await fileActivityMetadata(filePath, 'read', 'not_run', 'source', cwd),
|
|
203
|
+
...(images.length
|
|
204
|
+
? {
|
|
205
|
+
visionImages: images.map((source) => ({ source })),
|
|
206
|
+
visionNote: '这是所选 PDF 页面的视觉渲染,请检查版式、图表和扫描内容。',
|
|
207
|
+
}
|
|
208
|
+
: {}),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
finally {
|
|
212
|
+
await document.destroy().catch(() => undefined);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
const message = error.message;
|
|
217
|
+
return {
|
|
218
|
+
ok: false,
|
|
219
|
+
error: `PDF 解析失败${/password/i.test(message) ? '(文件可能已加密)' : ''}:${message}`,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
106
223
|
if (IMAGE_EXTENSIONS.has(ext)) {
|
|
107
224
|
if (info.size > MAX_IMAGE_BYTES)
|
|
108
225
|
return { ok: false, error: `图片超过 ${MAX_IMAGE_BYTES} 字节上限` };
|
|
@@ -118,7 +235,10 @@ async function call(input, state, cwd) {
|
|
|
118
235
|
}
|
|
119
236
|
if (ext === '.ipynb') {
|
|
120
237
|
if (info.size > MAX_NOTEBOOK_BYTES)
|
|
121
|
-
return {
|
|
238
|
+
return {
|
|
239
|
+
ok: false,
|
|
240
|
+
error: `Notebook 超过 ${MAX_NOTEBOOK_BYTES} 字节上限`,
|
|
241
|
+
};
|
|
122
242
|
try {
|
|
123
243
|
const snapshot = await readTextFile(filePath);
|
|
124
244
|
const rendered = renderNotebook(snapshot.content, offset, Math.min(limit, 200));
|
|
@@ -133,19 +253,34 @@ async function call(input, state, cwd) {
|
|
|
133
253
|
limit,
|
|
134
254
|
partial: rendered.partial,
|
|
135
255
|
truncated,
|
|
136
|
-
...(!rendered.partial && !truncated
|
|
256
|
+
...(!rendered.partial && !truncated
|
|
257
|
+
? { contentHash: hashFileBytes(snapshot.bytes) }
|
|
258
|
+
: {}),
|
|
137
259
|
}, state);
|
|
138
|
-
return {
|
|
260
|
+
return {
|
|
261
|
+
ok: true,
|
|
262
|
+
content,
|
|
263
|
+
metadata: await fileActivityMetadata(filePath, 'read', 'not_run', 'source', cwd),
|
|
264
|
+
};
|
|
139
265
|
}
|
|
140
266
|
catch (error) {
|
|
141
|
-
return {
|
|
267
|
+
return {
|
|
268
|
+
ok: false,
|
|
269
|
+
error: `Notebook 解析失败:${error.message}`,
|
|
270
|
+
};
|
|
142
271
|
}
|
|
143
272
|
}
|
|
144
273
|
if (hasBinaryExtension(filePath)) {
|
|
145
|
-
return {
|
|
274
|
+
return {
|
|
275
|
+
ok: false,
|
|
276
|
+
error: `不支持直接读取 ${ext || '该二进制格式'};PDF/Office 文件请使用对应 Skill。`,
|
|
277
|
+
};
|
|
146
278
|
}
|
|
147
279
|
if (info.size > MAX_FILE_SIZE_BYTES) {
|
|
148
|
-
return {
|
|
280
|
+
return {
|
|
281
|
+
ok: false,
|
|
282
|
+
error: `文件超过 ${MAX_FILE_SIZE_BYTES} 字节硬上限。请用 Grep 定位,或用 Bash 做流式处理。`,
|
|
283
|
+
};
|
|
149
284
|
}
|
|
150
285
|
try {
|
|
151
286
|
let content;
|
|
@@ -158,7 +293,9 @@ async function call(input, state, cwd) {
|
|
|
158
293
|
const all = snapshot.content.length === 0 ? [] : snapshot.content.split('\n');
|
|
159
294
|
const start = offset - 1;
|
|
160
295
|
const selected = all.slice(start, start + limit);
|
|
161
|
-
content = selected
|
|
296
|
+
content = selected
|
|
297
|
+
.map((line, index) => `${start + index + 1}\t${line}`)
|
|
298
|
+
.join('\n');
|
|
162
299
|
totalLines = all.length;
|
|
163
300
|
hasMore = start + selected.length < all.length;
|
|
164
301
|
}
|
|
@@ -183,19 +320,97 @@ async function call(input, state, cwd) {
|
|
|
183
320
|
limit,
|
|
184
321
|
partial,
|
|
185
322
|
truncated,
|
|
186
|
-
...(!partial && !truncated && completeBytes
|
|
323
|
+
...(!partial && !truncated && completeBytes
|
|
324
|
+
? { contentHash: hashFileBytes(completeBytes) }
|
|
325
|
+
: {}),
|
|
187
326
|
}, state);
|
|
188
327
|
if (info.size > 100 * 1024 && offset === 1) {
|
|
189
328
|
content = `⚠️ 大文件:结果受行数和输出字符上限约束,请按需分段读取。\n\n${content}`;
|
|
190
329
|
}
|
|
191
330
|
if (reads >= 2)
|
|
192
331
|
content = `⚠️ 重复读取空转:该区间未变化,前一次 Read 结果仍然有效。\n\n${content}`;
|
|
193
|
-
return {
|
|
332
|
+
return {
|
|
333
|
+
ok: true,
|
|
334
|
+
content,
|
|
335
|
+
metadata: await fileActivityMetadata(filePath, 'read', 'not_run', 'source', cwd),
|
|
336
|
+
};
|
|
194
337
|
}
|
|
195
338
|
catch (error) {
|
|
196
339
|
return { ok: false, error: `读取失败:${error.message}` };
|
|
197
340
|
}
|
|
198
341
|
}
|
|
342
|
+
async function renderPdfPages(document, filePath, pages, identity) {
|
|
343
|
+
const outputDir = join(sessionRuntimeDir(identity ?? {}), 'outputs');
|
|
344
|
+
const outputPrefix = `pdf_${randomUUID()}`;
|
|
345
|
+
await mkdir(outputDir, { recursive: true, mode: 0o700 });
|
|
346
|
+
const images = [];
|
|
347
|
+
const dynamicImport = new Function('specifier', 'return import(specifier)');
|
|
348
|
+
let createCanvas;
|
|
349
|
+
try {
|
|
350
|
+
({ createCanvas } = await dynamicImport('@napi-rs/canvas'));
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
/* optional adapter */
|
|
354
|
+
}
|
|
355
|
+
if (createCanvas) {
|
|
356
|
+
try {
|
|
357
|
+
for (const pageNumber of pages) {
|
|
358
|
+
const page = await document.getPage(pageNumber);
|
|
359
|
+
const viewport = page.getViewport({ scale: 2 });
|
|
360
|
+
const canvas = createCanvas(Math.ceil(viewport.width), Math.ceil(viewport.height));
|
|
361
|
+
await page.render({
|
|
362
|
+
canvasContext: canvas.getContext('2d'),
|
|
363
|
+
viewport,
|
|
364
|
+
}).promise;
|
|
365
|
+
const target = join(outputDir, `${outputPrefix}-page-${pageNumber}.png`);
|
|
366
|
+
await writeFile(target, canvas.toBuffer('image/png'), { mode: 0o600 });
|
|
367
|
+
images.push(target);
|
|
368
|
+
}
|
|
369
|
+
void pruneRuntimeOutputs();
|
|
370
|
+
return images;
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
images.length = 0;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
for (const pageNumber of pages) {
|
|
377
|
+
const prefix = join(outputDir, `${outputPrefix}-page-${pageNumber}`);
|
|
378
|
+
try {
|
|
379
|
+
await promisify(execFile)('pdftoppm', [
|
|
380
|
+
'-f',
|
|
381
|
+
String(pageNumber),
|
|
382
|
+
'-l',
|
|
383
|
+
String(pageNumber),
|
|
384
|
+
'-singlefile',
|
|
385
|
+
'-png',
|
|
386
|
+
'-r',
|
|
387
|
+
'144',
|
|
388
|
+
filePath,
|
|
389
|
+
prefix,
|
|
390
|
+
], { maxBuffer: 1024 * 1024 });
|
|
391
|
+
images.push(`${prefix}.png`);
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
void pruneRuntimeOutputs();
|
|
398
|
+
return images;
|
|
399
|
+
}
|
|
400
|
+
function parsePdfPages(spec, total) {
|
|
401
|
+
if (!spec)
|
|
402
|
+
return Array.from({ length: Math.min(total, 20) }, (_, index) => index + 1);
|
|
403
|
+
const pages = new Set();
|
|
404
|
+
for (const part of spec.split(',')) {
|
|
405
|
+
const [first, last] = part.split('-').map(Number);
|
|
406
|
+
for (let page = first; page <= (last ?? first); page += 1) {
|
|
407
|
+
if (page < 1 || page > total)
|
|
408
|
+
throw new Error(`PDF 页码超出范围:${page}(共 ${total} 页)`);
|
|
409
|
+
pages.add(page);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return [...pages].sort((left, right) => left - right);
|
|
413
|
+
}
|
|
199
414
|
export const readTool = {
|
|
200
415
|
name: 'Read',
|
|
201
416
|
description,
|
|
@@ -206,5 +421,5 @@ export const readTool = {
|
|
|
206
421
|
maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
|
|
207
422
|
interruptBehavior: () => 'cancel',
|
|
208
423
|
call: (input) => call(input),
|
|
209
|
-
callWithContext: (input, _signal, context) => call(input, context.sessionState?.fileReads, context.cwd),
|
|
424
|
+
callWithContext: (input, _signal, context) => call(input, context.sessionState?.fileReads, context.cwd, context),
|
|
210
425
|
};
|