mirror of
https://gitcode.com/ageerle/ruoyi-ai.git
synced 2026-04-11 18:47:20 +00:00
Merge remote-tracking branch 'origin/mineru/dev'
# Conflicts: # ruoyi-admin/src/main/resources/application-dev.yml # ruoyi-admin/src/main/resources/application-prod.yml # ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/chain/loader/ResourceLoaderFactory.java
This commit is contained in:
@@ -92,6 +92,13 @@ pdf:
|
|||||||
ai-api:
|
ai-api:
|
||||||
url: https://api.pandarobot.chat/v1/chat/completions
|
url: https://api.pandarobot.chat/v1/chat/completions
|
||||||
key: sk-xxxx
|
key: sk-xxxx
|
||||||
|
transition:
|
||||||
|
# 是否开启mineru
|
||||||
|
enable-minerU: true
|
||||||
|
# mineru conda环境路径
|
||||||
|
conda-env-path: "F:\\ProgramData\\Computer\\Anaconda\\envs\\mineru"
|
||||||
|
# 是否开启图片OCR
|
||||||
|
enable-ocr: true
|
||||||
#百炼模型配置
|
#百炼模型配置
|
||||||
dashscope:
|
dashscope:
|
||||||
key: sk-xxxx
|
key: sk-xxxx
|
||||||
|
|||||||
87
ruoyi-admin/src/main/resources/application-prod.yml
Normal file
87
ruoyi-admin/src/main/resources/application-prod.yml
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
|
||||||
|
--- # redis 单机配置(单机与集群只能开启一个另一个需要注释掉)
|
||||||
|
spring.data:
|
||||||
|
redis:
|
||||||
|
# 地址
|
||||||
|
host: localhost
|
||||||
|
# 端口,默认为6379
|
||||||
|
port: 6379
|
||||||
|
# 数据库索引
|
||||||
|
database: 0
|
||||||
|
# 密码(如没有密码请注释掉)
|
||||||
|
# password:
|
||||||
|
# 连接超时时间
|
||||||
|
timeout: 10s
|
||||||
|
# 是否开启ssl
|
||||||
|
ssl: false
|
||||||
|
|
||||||
|
redisson:
|
||||||
|
# redis key前缀
|
||||||
|
keyPrefix:
|
||||||
|
# 线程池数量
|
||||||
|
threads: 16
|
||||||
|
# Netty线程池数量
|
||||||
|
nettyThreads: 32
|
||||||
|
# 单节点配置
|
||||||
|
singleServerConfig:
|
||||||
|
# 客户端名称
|
||||||
|
clientName: ${ruoyi.name}
|
||||||
|
# 最小空闲连接数
|
||||||
|
connectionMinimumIdleSize: 32
|
||||||
|
# 连接池大小
|
||||||
|
connectionPoolSize: 64
|
||||||
|
# 连接空闲超时,单位:毫秒
|
||||||
|
idleConnectionTimeout: 10000
|
||||||
|
# 命令等待超时,单位:毫秒
|
||||||
|
timeout: 3000
|
||||||
|
# 发布和订阅连接池大小
|
||||||
|
subscriptionConnectionPoolSize: 50
|
||||||
|
|
||||||
|
--- # mail 邮件发送
|
||||||
|
mail:
|
||||||
|
enabled: false
|
||||||
|
host: smtp.163.com
|
||||||
|
port: 465
|
||||||
|
# 是否需要用户名密码验证
|
||||||
|
auth: true
|
||||||
|
# 发送方,遵循RFC-822标准
|
||||||
|
from: xxx@163.com
|
||||||
|
# 用户名(注意:如果使用foxmail邮箱,此处user为qq号)
|
||||||
|
user: xxx@163.com
|
||||||
|
# 密码(注意,某些邮箱需要为SMTP服务单独设置密码,详情查看相关帮助)
|
||||||
|
pass: xxxxxxxxxx
|
||||||
|
# 使用 STARTTLS安全连接,STARTTLS是对纯文本通信协议的扩展。
|
||||||
|
starttlsEnable: true
|
||||||
|
# 使用SSL安全连接
|
||||||
|
sslEnable: true
|
||||||
|
# SMTP超时时长,单位毫秒,缺省值不超时
|
||||||
|
timeout: 0
|
||||||
|
# Socket连接超时值,单位毫秒,缺省值不超时
|
||||||
|
connectionTimeout: 0
|
||||||
|
|
||||||
|
--- # sms 短信
|
||||||
|
sms:
|
||||||
|
enabled: false
|
||||||
|
# 阿里云 dysmsapi.aliyuncs.com
|
||||||
|
# 腾讯云 sms.tencentcloudapi.com
|
||||||
|
endpoint: "dysmsapi.aliyuncs.com"
|
||||||
|
accessKeyId: xxxxxxx
|
||||||
|
accessKeySecret: xxxxxx
|
||||||
|
signName: 测试
|
||||||
|
# 腾讯专用
|
||||||
|
sdkAppId:
|
||||||
|
|
||||||
|
pdf:
|
||||||
|
extract:
|
||||||
|
service:
|
||||||
|
url: http://localhost:8080
|
||||||
|
ai-api:
|
||||||
|
url: https://api.pandarobot.chat/v1/chat/completions
|
||||||
|
key: sk-XXXXXX
|
||||||
|
transition:
|
||||||
|
# 是否开启mineru
|
||||||
|
enable-minerU: true
|
||||||
|
# mineru conda环境路径
|
||||||
|
conda-env-path: "F:\\ProgramData\\Computer\\Anaconda\\envs\\mineru"
|
||||||
|
# 是否开启图片OCR
|
||||||
|
enable-ocr: true
|
||||||
@@ -0,0 +1,449 @@
|
|||||||
|
package org.ruoyi.chain.loader;
|
||||||
|
|
||||||
|
import dev.langchain4j.data.document.Document;
|
||||||
|
import dev.langchain4j.data.document.DocumentParser;
|
||||||
|
import dev.langchain4j.data.document.loader.FileSystemDocumentLoader;
|
||||||
|
import dev.langchain4j.data.document.parser.apache.tika.ApacheTikaDocumentParser;
|
||||||
|
import dev.langchain4j.data.message.AiMessage;
|
||||||
|
import dev.langchain4j.data.message.ImageContent;
|
||||||
|
import dev.langchain4j.data.message.TextContent;
|
||||||
|
import dev.langchain4j.data.message.UserMessage;
|
||||||
|
import dev.langchain4j.model.chat.response.ChatResponse;
|
||||||
|
import dev.langchain4j.model.openai.OpenAiChatModel;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.io.FilenameUtils;
|
||||||
|
import org.ruoyi.chain.split.TextSplitter;
|
||||||
|
import org.ruoyi.common.core.utils.StringUtils;
|
||||||
|
import org.ruoyi.common.core.utils.file.FileUtils;
|
||||||
|
import org.ruoyi.common.oss.core.OssClient;
|
||||||
|
import org.ruoyi.common.oss.entity.UploadResult;
|
||||||
|
import org.ruoyi.common.oss.factory.OssFactory;
|
||||||
|
import org.ruoyi.config.properties.PdfProperties;
|
||||||
|
import org.ruoyi.system.domain.SysOss;
|
||||||
|
import org.ruoyi.system.mapper.SysOssMapper;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.*;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pdf mineru文件加载器
|
||||||
|
*
|
||||||
|
* @author zpx
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class PdfMinerUFileLoader implements ResourceLoader {
|
||||||
|
private final TextSplitter characterTextSplitter;
|
||||||
|
private final PdfProperties properties;
|
||||||
|
private final SysOssMapper sysOssMapper;
|
||||||
|
// 预编译正则表达式
|
||||||
|
private static final Pattern MD_IMAGE_PATTERN = Pattern.compile("!\\[(.*?)]\\((.*?)(\\s*=\\d+)?\\)");
|
||||||
|
// OCR图片识别线程池
|
||||||
|
private final ThreadPoolExecutor ocrExecutor = new ThreadPoolExecutor(
|
||||||
|
// 核心线程数
|
||||||
|
5,
|
||||||
|
// 最大线程数
|
||||||
|
10,
|
||||||
|
// 空闲线程存活时间
|
||||||
|
60L, TimeUnit.SECONDS,
|
||||||
|
// 任务队列容量
|
||||||
|
new LinkedBlockingQueue<>(100),
|
||||||
|
// 拒绝策略
|
||||||
|
new ThreadPoolExecutor.CallerRunsPolicy()
|
||||||
|
);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getContent(InputStream inputStream) {
|
||||||
|
String content = "";
|
||||||
|
File tempPdf = null;
|
||||||
|
Path outputPath = null;
|
||||||
|
try {
|
||||||
|
// 创建临时文件
|
||||||
|
tempPdf = createTempFile(inputStream);
|
||||||
|
//构建输出路径
|
||||||
|
outputPath = buildOutputPath();
|
||||||
|
// 执行转换命令
|
||||||
|
Process process = buildProcess(properties.getTransition().getCondaEnvPath(), tempPdf, outputPath);
|
||||||
|
//打印执行日志
|
||||||
|
logProcessOutput(process);
|
||||||
|
int exitCode = process.waitFor();
|
||||||
|
//验证转换结果
|
||||||
|
String verifyResult = verifyResult(tempPdf, outputPath, exitCode);
|
||||||
|
|
||||||
|
// 获取生成的.md文件路径
|
||||||
|
Path mdFilePath = Paths.get(verifyResult);
|
||||||
|
if (Files.exists(mdFilePath)) {
|
||||||
|
log.info("找到Markdown文件: " + mdFilePath);
|
||||||
|
DocumentParser documentParser = new ApacheTikaDocumentParser();
|
||||||
|
Document document = FileSystemDocumentLoader.loadDocument(mdFilePath.toString(), documentParser);
|
||||||
|
if (null != document) {
|
||||||
|
content = document.text();
|
||||||
|
// 判断是否md文档
|
||||||
|
String fileType = FilenameUtils.getExtension(mdFilePath.getFileName().toString());
|
||||||
|
//判断是否需要进行图片OCR识别
|
||||||
|
if ("md".contains(fileType) && properties.getTransition().isEnableOcr()) {
|
||||||
|
// 如果是md文件,查找所有图片语法,如果是本地图片,替换成网络图片
|
||||||
|
StringBuffer sb = replaceImageUrl(content, mdFilePath);
|
||||||
|
content = sb.toString();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn("无法解析文档内容");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn("未找到预期的 .md 文件");
|
||||||
|
}
|
||||||
|
return content;
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
} finally {
|
||||||
|
if (tempPdf != null) {
|
||||||
|
try {
|
||||||
|
// 清理临时文件
|
||||||
|
Files.deleteIfExists(tempPdf.toPath());
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("删除临时文件失败: {}", tempPdf.getAbsolutePath(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//清理输出目录
|
||||||
|
if (outputPath != null) {
|
||||||
|
cleanOutputDirectory(outputPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> getChunkList(String content, String kid) {
|
||||||
|
return characterTextSplitter.split(content, kid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建临时PDF文件
|
||||||
|
*
|
||||||
|
* @param is 输入流
|
||||||
|
* @return
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
private static File createTempFile(InputStream is) throws IOException {
|
||||||
|
File tempFile = File.createTempFile("upload_", ".pdf");
|
||||||
|
Files.copy(is, tempFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
return tempFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建跨平台文件输出路径
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
private static Path buildOutputPath() throws IOException {
|
||||||
|
Path basePath = isWindows() ?
|
||||||
|
// Windows C盘用户路径下 minerUOutPut,避免其他盘符权限问题
|
||||||
|
Paths.get(System.getProperty("user.home")).resolve("minerUOutPut") :
|
||||||
|
Paths.get("/var/minerUOutPut");
|
||||||
|
|
||||||
|
if (!Files.exists(basePath)) {
|
||||||
|
Files.createDirectories(basePath);
|
||||||
|
}
|
||||||
|
return basePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断当前操作系统是否为Windows
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private static boolean isWindows() {
|
||||||
|
return System.getProperty("os.name").toLowerCase().contains("win");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行命令
|
||||||
|
*
|
||||||
|
* @param condaEnv conda环境路径
|
||||||
|
* @param inputFile 输入文件
|
||||||
|
* @param outputPath 输出路径
|
||||||
|
* @return
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
private static Process buildProcess(String condaEnv, File inputFile, Path outputPath) throws IOException {
|
||||||
|
ProcessBuilder pb = new ProcessBuilder();
|
||||||
|
String[] command;
|
||||||
|
|
||||||
|
if (isWindows()) {
|
||||||
|
command = new String[]{
|
||||||
|
"cmd", "/c",
|
||||||
|
"call", "conda", "activate",
|
||||||
|
condaEnv.replace("\"", ""),
|
||||||
|
"&&", "magic-pdf",
|
||||||
|
"-p", inputFile.getAbsolutePath(),
|
||||||
|
"-o", outputPath.toString()
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
command = new String[]{
|
||||||
|
"bash", "-c",
|
||||||
|
String.format("source '%s/bin/activate' && magic-pdf -p '%s' -o '%s'",
|
||||||
|
condaEnv,
|
||||||
|
inputFile.getAbsolutePath(),
|
||||||
|
outputPath.toString())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return pb.command(command)
|
||||||
|
.redirectErrorStream(true)
|
||||||
|
.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 实时日志输出
|
||||||
|
*
|
||||||
|
* @param process 进程
|
||||||
|
*/
|
||||||
|
private static void logProcessOutput(Process process) {
|
||||||
|
Executors.newSingleThreadExecutor().submit(() -> {
|
||||||
|
try (BufferedReader reader = new BufferedReader(
|
||||||
|
new InputStreamReader(process.getInputStream()))) {
|
||||||
|
String line;
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
log.info("[PROCESS LOG] " + line);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证转换结果
|
||||||
|
*
|
||||||
|
* @param inputFile 输入文件
|
||||||
|
* @param outputPath 输出路径
|
||||||
|
* @param exitCode 退出码
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private static String verifyResult(File inputFile, Path outputPath, int exitCode) {
|
||||||
|
String baseName = FilenameUtils.removeExtension(inputFile.getName());
|
||||||
|
Path expectedMd = outputPath
|
||||||
|
.resolve(baseName)
|
||||||
|
.resolve("auto")
|
||||||
|
.resolve(baseName + ".md");
|
||||||
|
|
||||||
|
if (exitCode == 0 && Files.exists(expectedMd)) {
|
||||||
|
log.info("转换成功:{}", expectedMd.toString());
|
||||||
|
return expectedMd.toString();
|
||||||
|
}
|
||||||
|
return String.format("转换失败(退出码%d)| 预期文件:%s", exitCode, expectedMd);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 正则匹配图片语法,多线程进行处理
|
||||||
|
*
|
||||||
|
* @param content 文本内容
|
||||||
|
* @param basePath 图片路径
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private StringBuffer replaceImageUrl(String content, Path basePath) throws Exception {
|
||||||
|
List<ImageMatch> matches = new ArrayList<>();
|
||||||
|
Matcher matcher = MD_IMAGE_PATTERN.matcher(content);
|
||||||
|
|
||||||
|
// 收集所有匹配的图片项
|
||||||
|
while (matcher.find()) {
|
||||||
|
ImageMatch imgMatch = new ImageMatch();
|
||||||
|
imgMatch.altText = matcher.group(1);
|
||||||
|
imgMatch.imageUrl = matcher.group(2);
|
||||||
|
imgMatch.start = matcher.start();
|
||||||
|
imgMatch.end = matcher.end();
|
||||||
|
matches.add(imgMatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matches.isEmpty()) {
|
||||||
|
return new StringBuffer(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交任务到线程池
|
||||||
|
List<Future<String>> futures = new ArrayList<>();
|
||||||
|
for (ImageMatch imgMatch : matches) {
|
||||||
|
// 为每个图片项创建独立任务
|
||||||
|
Future<String> future = ocrExecutor.submit(() -> processImage(imgMatch, basePath));
|
||||||
|
futures.add(future);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按原始顺序拼接结果
|
||||||
|
StringBuffer sb = new StringBuffer();
|
||||||
|
int previousEnd = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < matches.size(); i++) {
|
||||||
|
ImageMatch imgMatch = matches.get(i);
|
||||||
|
// 阻塞等待结果
|
||||||
|
String replacement = futures.get(i).get();
|
||||||
|
|
||||||
|
// 插入未匹配的原始文本和处理后的结果
|
||||||
|
sb.append(content.substring(previousEnd, imgMatch.start));
|
||||||
|
sb.append(replacement);
|
||||||
|
previousEnd = imgMatch.end;
|
||||||
|
}
|
||||||
|
// 添加剩余文本
|
||||||
|
sb.append(content.substring(previousEnd));
|
||||||
|
return sb;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图片处理任务
|
||||||
|
*
|
||||||
|
* @param imgMatch 图片匹配结果
|
||||||
|
* @param basePath 本地图片路径
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private String processImage(ImageMatch imgMatch, Path basePath) {
|
||||||
|
try {
|
||||||
|
if (!imgMatch.imageUrl.startsWith("http")) {
|
||||||
|
// 处理本地图片
|
||||||
|
Path imagePath = basePath.getParent().resolve(imgMatch.imageUrl).normalize();
|
||||||
|
|
||||||
|
if (!Files.exists(imagePath)) {
|
||||||
|
log.error("图片路径不存在: {}", imagePath);
|
||||||
|
return String.format("", imgMatch.altText, imgMatch.imageUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件后缀安全提取
|
||||||
|
String originalFileName = imagePath.getFileName().toString();
|
||||||
|
String suffix = "";
|
||||||
|
int lastDotIndex = originalFileName.lastIndexOf(".");
|
||||||
|
if (lastDotIndex != -1) {
|
||||||
|
suffix = originalFileName.substring(lastDotIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传OSS
|
||||||
|
try (InputStream inputStream = Files.newInputStream(imagePath)) {
|
||||||
|
OssClient storage = OssFactory.instance();
|
||||||
|
UploadResult uploadResult = storage.uploadSuffix(inputStream, suffix, FileUtils.getMimeType(suffix));
|
||||||
|
|
||||||
|
// 保存数据库记录
|
||||||
|
SysOss sysOss = new SysOss();
|
||||||
|
sysOss.setUrl(uploadResult.getUrl());
|
||||||
|
sysOss.setFileSuffix(suffix);
|
||||||
|
sysOss.setFileName(uploadResult.getFilename());
|
||||||
|
sysOss.setOriginalName(originalFileName);
|
||||||
|
sysOss.setService(storage.getConfigKey());
|
||||||
|
sysOssMapper.insert(sysOss);
|
||||||
|
|
||||||
|
// OCR处理
|
||||||
|
String networkUrl = uploadResult.getUrl();
|
||||||
|
//⚠️ 注意:确保 URL 是公网可访问的,否则模型无法加载图片。
|
||||||
|
//另一种解决方案:使用base64 但是需要申请apikey , 使用demo会出现token超出长度问题。
|
||||||
|
String ocrResult = safeImageUrlOCR(networkUrl);
|
||||||
|
return String.format("", imgMatch.altText, ocrResult, networkUrl);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 处理远程图片
|
||||||
|
String ocrResult = safeImageUrlOCR(imgMatch.imageUrl);
|
||||||
|
return String.format("", imgMatch.altText, ocrResult, imgMatch.imageUrl);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("图片处理失败: {}", imgMatch.imageUrl, e);
|
||||||
|
return String.format("", imgMatch.altText, imgMatch.imageUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OCR调用
|
||||||
|
*
|
||||||
|
* @param imageUrl 图片URL
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private String safeImageUrlOCR(String imageUrl) {
|
||||||
|
try {
|
||||||
|
return imageUrlOCR(imageUrl);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("OCR处理失败: {}", imageUrl, e);
|
||||||
|
// OCR失败时返回空字符串
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 多模态OCR识别图片内容
|
||||||
|
*
|
||||||
|
* @param imageUrl 图片URL
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private static String imageUrlOCR(String imageUrl) {
|
||||||
|
OpenAiChatModel model = OpenAiChatModel.builder()
|
||||||
|
.apiKey("demo")
|
||||||
|
.modelName("gpt-4o-mini")
|
||||||
|
.baseUrl("http://langchain4j.dev/demo/openai/v1")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
UserMessage userMessage = UserMessage.from(
|
||||||
|
TextContent.from(
|
||||||
|
"请按以下逻辑处理图片:\n" +
|
||||||
|
"1. 文字检测:识别图中所有可见文字(包括水印/标签),若无文字则跳至步骤3\n" +
|
||||||
|
"2. 文字处理:\n" +
|
||||||
|
" a. 对识别到的文字进行❗核心信息提炼\n" +
|
||||||
|
" b. ❗禁止直接输出原文内容\n" +
|
||||||
|
" c. 描述文字位置(如'顶部居中')、字体特征(颜色/大小)\n" +
|
||||||
|
"3. 视觉描述:\n" +
|
||||||
|
" a. 若无文字则用❗50字内简洁描述主体对象、场景、色彩搭配与画面氛围\n" +
|
||||||
|
" b. 若有文字则补充说明文字与画面的关系\n" +
|
||||||
|
"4. 输出规则:\n" +
|
||||||
|
" - 最终输出为纯文本,格式:'[文字总结] 视觉描述 关键词:xx,xx'\n" +
|
||||||
|
" - 关键词从内容中提取3个最具代表性的名词\n" +
|
||||||
|
" - 无文字时格式:'[空] 简洁描述 关键词:xx,xx'"
|
||||||
|
),
|
||||||
|
ImageContent.from(imageUrl)
|
||||||
|
);
|
||||||
|
ChatResponse chat = model.chat(userMessage);
|
||||||
|
AiMessage answer = chat.aiMessage();
|
||||||
|
return answer.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 静态内部类保存图片匹配信息
|
||||||
|
*/
|
||||||
|
private static class ImageMatch {
|
||||||
|
String altText; // 替换文本
|
||||||
|
String imageUrl; // 图片地址
|
||||||
|
int start; // 匹配起始位置
|
||||||
|
int end; // 匹配结束位置
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理输出目录
|
||||||
|
*
|
||||||
|
* @param outputPath 输出目录
|
||||||
|
*/
|
||||||
|
private static void cleanOutputDirectory(Path outputPath) {
|
||||||
|
if (Files.exists(outputPath)) {
|
||||||
|
try {
|
||||||
|
Files.walk(outputPath)
|
||||||
|
// 按逆序删除(子目录先删)
|
||||||
|
.sorted((p1, p2) -> -p1.compareTo(p2))
|
||||||
|
.forEach(path -> {
|
||||||
|
try {
|
||||||
|
Files.delete(path);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("清理输出目录失败: {}", path, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("遍历输出目录失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
package org.ruoyi.chain.loader;
|
package org.ruoyi.chain.loader;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
import org.ruoyi.chain.split.*;
|
||||||
|
|
||||||
|
import org.ruoyi.config.properties.PdfProperties;
|
||||||
import org.ruoyi.chain.split.CharacterTextSplitter;
|
import org.ruoyi.chain.split.CharacterTextSplitter;
|
||||||
import org.ruoyi.chain.split.CodeTextSplitter;
|
import org.ruoyi.chain.split.CodeTextSplitter;
|
||||||
import org.ruoyi.chain.split.MarkdownTextSplitter;
|
import org.ruoyi.chain.split.MarkdownTextSplitter;
|
||||||
import org.ruoyi.constant.FileType;
|
import org.ruoyi.constant.FileType;
|
||||||
|
import org.ruoyi.system.mapper.SysOssMapper;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@@ -13,6 +17,10 @@ public class ResourceLoaderFactory {
|
|||||||
private final CharacterTextSplitter characterTextSplitter;
|
private final CharacterTextSplitter characterTextSplitter;
|
||||||
private final CodeTextSplitter codeTextSplitter;
|
private final CodeTextSplitter codeTextSplitter;
|
||||||
private final MarkdownTextSplitter markdownTextSplitter;
|
private final MarkdownTextSplitter markdownTextSplitter;
|
||||||
|
private final TokenTextSplitter tokenTextSplitter;
|
||||||
|
private final ExcelTextSplitter excelTextSplitter;
|
||||||
|
private final PdfProperties pdfProperties;
|
||||||
|
private final SysOssMapper sysOssMapper;
|
||||||
|
|
||||||
|
|
||||||
public ResourceLoader getLoaderByFileType(String fileType){
|
public ResourceLoader getLoaderByFileType(String fileType){
|
||||||
@@ -20,9 +28,11 @@ public class ResourceLoaderFactory {
|
|||||||
return new TextFileLoader(characterTextSplitter);
|
return new TextFileLoader(characterTextSplitter);
|
||||||
} else if (FileType.isWord(fileType)) {
|
} else if (FileType.isWord(fileType)) {
|
||||||
return new WordLoader(characterTextSplitter);
|
return new WordLoader(characterTextSplitter);
|
||||||
|
} else if (FileType.isPdf(fileType) && pdfProperties.getTransition().isEnableMinerU()) {
|
||||||
|
return new PdfMinerUFileLoader(characterTextSplitter,pdfProperties,sysOssMapper);
|
||||||
} else if (FileType.isPdf(fileType)) {
|
} else if (FileType.isPdf(fileType)) {
|
||||||
return new PdfFileLoader(characterTextSplitter);
|
return new PdfFileLoader(characterTextSplitter);
|
||||||
} else if (FileType.isMdFile(fileType)) {
|
}else if (FileType.isMdFile(fileType)) {
|
||||||
return new MarkDownFileLoader(markdownTextSplitter);
|
return new MarkDownFileLoader(markdownTextSplitter);
|
||||||
}else if (FileType.isCodeFile(fileType)) {
|
}else if (FileType.isCodeFile(fileType)) {
|
||||||
return new CodeFileLoader(codeTextSplitter);
|
return new CodeFileLoader(codeTextSplitter);
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package org.ruoyi.config.properties;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PDF 配置属性
|
||||||
|
*
|
||||||
|
* @author zpx
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Component
|
||||||
|
@ConfigurationProperties(prefix = "pdf")
|
||||||
|
public class PdfProperties {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract 配置
|
||||||
|
*/
|
||||||
|
private ExtractConfig extract;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transition 配置
|
||||||
|
*/
|
||||||
|
private TransitionConfig transition;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
public static class ExtractConfig {
|
||||||
|
/**
|
||||||
|
* Service 配置
|
||||||
|
*/
|
||||||
|
private ServiceConfig service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI API 配置
|
||||||
|
*/
|
||||||
|
private AiApiConfig aiApi;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
public static class ServiceConfig {
|
||||||
|
/**
|
||||||
|
* 服务地址 URL
|
||||||
|
*/
|
||||||
|
private String url;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
public static class AiApiConfig {
|
||||||
|
/**
|
||||||
|
* AI API 地址 URL
|
||||||
|
*/
|
||||||
|
private String url;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API 密钥
|
||||||
|
*/
|
||||||
|
private String key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
public static class TransitionConfig {
|
||||||
|
/**
|
||||||
|
* 是否启用 MinerU
|
||||||
|
*/
|
||||||
|
private boolean enableMinerU;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MinerU Conda 环境路径
|
||||||
|
*/
|
||||||
|
private String condaEnvPath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用图片 OCR
|
||||||
|
*/
|
||||||
|
private boolean enableOcr;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user