- 添加 `FileReadTool`,支持文件内容读取与安全验证 - 引入 `hasToolMessages` 逻辑,优化工具历史上下文处理 - 修改工具选项逻辑,支持禁用工具时的动态调整 - 增加消息序列化逻辑,优化 Redis 序列管理与数据同步 - 扩展测试覆盖,验证序列化与工具调用场景 - 增强 Docker Compose 脚本,支持应用重置与日志清理 - 调整工具调用超时设置,提升运行时用户体验
218 lines
6.2 KiB
PHP
218 lines
6.2 KiB
PHP
<?php
|
||
|
||
namespace App\Services\Tool\Tools;
|
||
|
||
use App\Services\Tool\Tool;
|
||
use InvalidArgumentException;
|
||
|
||
class FileReadTool implements Tool
|
||
{
|
||
public function name(): string
|
||
{
|
||
return 'file_read';
|
||
}
|
||
|
||
public function description(): string
|
||
{
|
||
return '读取文件内容,支持指定行范围、编码和大文件分段读取。';
|
||
}
|
||
|
||
/**
|
||
* @return array<string, mixed>
|
||
*/
|
||
public function parameters(): array
|
||
{
|
||
return [
|
||
'type' => 'object',
|
||
'properties' => [
|
||
'path' => [
|
||
'type' => 'string',
|
||
'description' => '要读取的文件路径(相对或绝对路径)。',
|
||
],
|
||
'start_line' => [
|
||
'type' => 'integer',
|
||
'description' => '起始行号(从1开始),默认从第一行开始。',
|
||
'minimum' => 1,
|
||
'default' => 1,
|
||
],
|
||
'end_line' => [
|
||
'type' => 'integer',
|
||
'description' => '结束行号(包含),默认读取到文件末尾。',
|
||
'minimum' => 1,
|
||
],
|
||
'max_size' => [
|
||
'type' => 'integer',
|
||
'description' => '最大读取字节数(1-10MB),默认1MB,防止读取过大文件。',
|
||
'minimum' => 1,
|
||
'maximum' => 10485760,
|
||
'default' => 1048576,
|
||
],
|
||
'encoding' => [
|
||
'type' => 'string',
|
||
'description' => '文件编码,默认UTF-8。',
|
||
'enum' => ['UTF-8', 'GBK', 'GB2312', 'ISO-8859-1'],
|
||
'default' => 'UTF-8',
|
||
],
|
||
],
|
||
'required' => ['path'],
|
||
];
|
||
}
|
||
|
||
/**
|
||
* @param array<string, mixed> $arguments
|
||
* @return array<string, mixed>
|
||
*/
|
||
public function execute(array $arguments): array
|
||
{
|
||
$path = $arguments['path'] ?? '';
|
||
|
||
// 验证路径
|
||
if (empty($path)) {
|
||
throw new InvalidArgumentException('文件路径不能为空。');
|
||
}
|
||
|
||
// 安全检查:防止路径遍历攻击
|
||
$realPath = realpath($path);
|
||
if ($realPath === false) {
|
||
throw new InvalidArgumentException("文件不存在:{$path}");
|
||
}
|
||
|
||
if (!is_file($realPath)) {
|
||
throw new InvalidArgumentException("路径不是文件:{$path}");
|
||
}
|
||
|
||
if (!is_readable($realPath)) {
|
||
throw new InvalidArgumentException("文件不可读:{$path}");
|
||
}
|
||
|
||
// 获取参数
|
||
$startLine = max(1, (int)($arguments['start_line'] ?? 1));
|
||
$endLine = isset($arguments['end_line']) ? max(1, (int)$arguments['end_line']) : null;
|
||
$maxSize = min(10485760, max(1, (int)($arguments['max_size'] ?? 1048576)));
|
||
$encoding = $arguments['encoding'] ?? 'UTF-8';
|
||
|
||
// 检查文件大小
|
||
$fileSize = filesize($realPath);
|
||
if ($fileSize === false) {
|
||
throw new InvalidArgumentException("无法获取文件大小:{$path}");
|
||
}
|
||
|
||
return $this->readFileContent($realPath, $startLine, $endLine, $maxSize, $encoding, $fileSize);
|
||
}
|
||
|
||
/**
|
||
* 读取文件内容
|
||
*
|
||
* @param string $path
|
||
* @param int $startLine
|
||
* @param int|null $endLine
|
||
* @param int $maxSize
|
||
* @param string $encoding
|
||
* @param int $fileSize
|
||
* @return array<string, mixed>
|
||
*/
|
||
private function readFileContent(
|
||
string $path,
|
||
int $startLine,
|
||
?int $endLine,
|
||
int $maxSize,
|
||
string $encoding,
|
||
int $fileSize
|
||
): array {
|
||
$result = [
|
||
'path' => $path,
|
||
'size' => $fileSize,
|
||
'encoding' => $encoding,
|
||
];
|
||
|
||
// 如果文件为空
|
||
if ($fileSize === 0) {
|
||
$result['content'] = '';
|
||
$result['lines_read'] = 0;
|
||
$result['truncated'] = false;
|
||
return $result;
|
||
}
|
||
|
||
// 读取文件
|
||
$handle = fopen($path, 'r');
|
||
if ($handle === false) {
|
||
throw new InvalidArgumentException("无法打开文件:{$path}");
|
||
}
|
||
|
||
try {
|
||
return $this->readLines($handle, $startLine, $endLine, $maxSize, $encoding, $result);
|
||
} finally {
|
||
fclose($handle);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 按行读取文件
|
||
*
|
||
* @param resource $handle
|
||
* @param int $startLine
|
||
* @param int|null $endLine
|
||
* @param int $maxSize
|
||
* @param string $encoding
|
||
* @param array<string, mixed> $result
|
||
* @return array<string, mixed>
|
||
*/
|
||
private function readLines(
|
||
$handle,
|
||
int $startLine,
|
||
?int $endLine,
|
||
int $maxSize,
|
||
string $encoding,
|
||
array $result
|
||
): array {
|
||
$lines = [];
|
||
$currentLine = 0;
|
||
$bytesRead = 0;
|
||
$truncated = false;
|
||
|
||
while (($line = fgets($handle)) !== false) {
|
||
$currentLine++;
|
||
|
||
// 跳过起始行之前的内容
|
||
if ($currentLine < $startLine) {
|
||
continue;
|
||
}
|
||
|
||
// 检查是否超过结束行
|
||
if ($endLine !== null && $currentLine > $endLine) {
|
||
break;
|
||
}
|
||
|
||
// 检查大小限制
|
||
$lineLength = strlen($line);
|
||
if ($bytesRead + $lineLength > $maxSize) {
|
||
$truncated = true;
|
||
break;
|
||
}
|
||
|
||
$lines[] = $line;
|
||
$bytesRead += $lineLength;
|
||
}
|
||
|
||
$content = implode('', $lines);
|
||
|
||
// 编码转换
|
||
if ($encoding !== 'UTF-8' && function_exists('mb_convert_encoding')) {
|
||
$content = mb_convert_encoding($content, 'UTF-8', $encoding);
|
||
}
|
||
|
||
$result['content'] = $content;
|
||
$result['lines_read'] = count($lines);
|
||
$result['start_line'] = $startLine;
|
||
$result['end_line'] = $endLine ?? $currentLine;
|
||
$result['truncated'] = $truncated;
|
||
$result['bytes_read'] = $bytesRead;
|
||
|
||
if ($truncated) {
|
||
$result['warning'] = "内容已截断,已读取 {$bytesRead} 字节(限制:{$maxSize} 字节)";
|
||
}
|
||
|
||
return $result;
|
||
}
|
||
}
|