self::readPlainText($path), $ext === 'docx' || str_contains($mime, 'wordprocessingml') => self::extractDocx($path), $ext === 'pdf' || str_contains($mime, 'pdf') => self::extractPdf($path), default => null, }; if ($text === null) { return null; } $text = self::normalize($text); if ($text === '') { if (self::$lastFailure === null) { self::$lastFailure = 'empty_content'; } return null; } return mb_substr($text, 0, self::MAX_CHARS); } public static function unsupportedReason(string $filename, string $mime = ''): string { $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); if (self::$lastFailure === 'exec_disabled') { return 'PHP 禁用了 exec 函数,无法调用 pdftotext。请在 php.ini 的 disable_functions 中移除 exec'; } if (self::$lastFailure === 'pdftotext_missing') { return '未找到 pdftotext 命令。请在**运行 PHP 的服务器**(不是 Dify 容器)执行:apt install poppler-utils'; } if (self::$lastFailure === 'pdftotext_failed') { return 'pdftotext 执行失败,请检查 PDF 是否损坏或 uploads 目录是否可读'; } if (self::$lastFailure === 'empty_content' && ($ext === 'pdf' || str_contains($mime, 'pdf'))) { return 'PDF 未提取到文字,可能是扫描版图片 PDF,请改用可复制文字的 PDF 或粘贴文字'; } if ($ext === 'doc' || str_contains($mime, 'msword')) { return '旧版 .doc 暂不支持自动解析,请另存为 .docx 或复制文字发送'; } if ($ext === 'pdf' || str_contains($mime, 'pdf')) { return 'PDF 文字提取失败,请在运行 PHP 的服务器安装 poppler-utils(pdftotext)'; } if (!class_exists(\ZipArchive::class) && ($ext === 'docx' || str_contains($mime, 'wordprocessingml'))) { return 'PHP 未启用 zip 扩展,无法解析 .docx,请安装 php-zip'; } return '未能提取文档文字,请改用 .txt / .docx 或粘贴文字内容'; } private static function readPlainText(string $path): ?string { $content = @file_get_contents($path); if ($content === false) { return null; } if (!mb_check_encoding($content, 'UTF-8')) { $content = mb_convert_encoding($content, 'UTF-8', 'GB18030,UTF-8,ASCII'); } return $content; } private static function extractDocx(string $path): ?string { if (!class_exists(\ZipArchive::class)) { return null; } $zip = new \ZipArchive(); if ($zip->open($path) !== true) { return null; } $xml = $zip->getFromName('word/document.xml'); $zip->close(); if (!$xml) { return null; } $xml = preg_replace('/]*\/>/', "\t", $xml); $xml = preg_replace('/<\/w:p>/', "\n", $xml); $xml = preg_replace('/<\/w:tr>/', "\n", $xml); $text = strip_tags($xml); return html_entity_decode($text, ENT_QUOTES | ENT_XML1, 'UTF-8'); } private static function extractPdf(string $path): ?string { if (!function_exists('exec')) { self::$lastFailure = 'exec_disabled'; return null; } $binary = self::resolvePdftotextBinary(); if ($binary === null) { self::$lastFailure = 'pdftotext_missing'; return null; } $out = tempnam(sys_get_temp_dir(), 'pdftxt_'); if (!$out) { self::$lastFailure = 'pdftotext_failed'; return null; } $cmd = escapeshellarg($binary) . ' -enc UTF-8 -layout ' . escapeshellarg($path) . ' ' . escapeshellarg($out) . ' 2>&1'; exec($cmd, $_, $code); $text = ($code === 0 && is_file($out)) ? @file_get_contents($out) : false; @unlink($out); if ($text === false) { self::$lastFailure = 'pdftotext_failed'; return null; } return $text; } /** * PHP-FPM 进程的 PATH 常不含 /usr/bin,需显式探测可执行文件路径。 */ private static function resolvePdftotextBinary(): ?string { $candidates = [ '/usr/bin/pdftotext', '/usr/local/bin/pdftotext', 'pdftotext', ]; foreach ($candidates as $bin) { if (str_starts_with($bin, '/')) { if (is_executable($bin)) { return $bin; } continue; } if (self::commandExists($bin)) { return $bin; } } return null; } private static function commandExists(string $command): bool { if (!function_exists('exec')) { return false; } $check = stripos(PHP_OS, 'WIN') === 0 ? 'where' : 'command -v'; exec($check . ' ' . escapeshellarg($command) . ' 2>/dev/null', $output, $code); return $code === 0 && !empty($output); } private static function normalize(string $text): string { $text = str_replace(["\r\n", "\r"], "\n", $text); $text = preg_replace("/[ \t]+\n/", "\n", $text); $text = preg_replace("/\n{3,}/", "\n\n", $text); return trim($text); } }