feat: 支持智能体文档附件与轻量读取
- 建立文档上传、异步读取、对象存储、补偿与聊天绑定闭环 - 按智能体 20K 上下文预算选择文档片段并保留稳定引用 - 统一聊天文件卡片、类型图标、草稿恢复与可靠下载
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
package tech.easyflow.agent.config;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.AssertTrue;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.bind.DefaultValue;
|
||||
import org.springframework.boot.convert.DataSizeUnit;
|
||||
import org.springframework.util.unit.DataSize;
|
||||
import org.springframework.util.unit.DataUnit;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* Agent 文档附件的 I/O、并发与格式安全边界配置。
|
||||
*/
|
||||
@Validated
|
||||
@ConfigurationProperties(prefix = "easyflow.agent.document")
|
||||
public class AgentDocumentProperties {
|
||||
|
||||
/** 是否启用文档附件。 */
|
||||
private boolean enabled = true;
|
||||
/** 单轮最大文档数。 */
|
||||
@Min(1)
|
||||
private int maxDocumentsPerTurn = 3;
|
||||
/** 单轮文档总字节上限。 */
|
||||
@NotNull
|
||||
@DataSizeUnit(DataUnit.MEGABYTES)
|
||||
private DataSize maxTotalBytesPerTurn = DataSize.ofMegabytes(30);
|
||||
/** 单文档读取超时。 */
|
||||
@NotNull
|
||||
private Duration readTimeout = Duration.ofSeconds(30);
|
||||
/** 未绑定文档保留时间。 */
|
||||
@NotNull
|
||||
private Duration tempRetention = Duration.ofHours(24);
|
||||
/** 读取线程池配置。 */
|
||||
@Valid
|
||||
@NotNull
|
||||
private Reader reader = new Reader();
|
||||
/** 格式安全边界。 */
|
||||
@Valid
|
||||
@NotNull
|
||||
private Limits limits = new Limits();
|
||||
|
||||
/** @return 是否启用文档附件 */
|
||||
public boolean isEnabled() { return enabled; }
|
||||
/** @param enabled 是否启用文档附件 */
|
||||
public void setEnabled(boolean enabled) { this.enabled = enabled; }
|
||||
/** @return 单轮最大文档数 */
|
||||
public int getMaxDocumentsPerTurn() { return maxDocumentsPerTurn; }
|
||||
/** @param maxDocumentsPerTurn 单轮最大文档数 */
|
||||
public void setMaxDocumentsPerTurn(int maxDocumentsPerTurn) { this.maxDocumentsPerTurn = maxDocumentsPerTurn; }
|
||||
/** @return 单轮文档总大小 */
|
||||
public DataSize getMaxTotalBytesPerTurn() { return maxTotalBytesPerTurn; }
|
||||
/** @param maxTotalBytesPerTurn 单轮文档总大小 */
|
||||
public void setMaxTotalBytesPerTurn(DataSize maxTotalBytesPerTurn) {
|
||||
this.maxTotalBytesPerTurn = maxTotalBytesPerTurn;
|
||||
}
|
||||
/** @return 读取超时 */
|
||||
public Duration getReadTimeout() { return readTimeout; }
|
||||
/** @param readTimeout 读取超时 */
|
||||
public void setReadTimeout(Duration readTimeout) { this.readTimeout = readTimeout; }
|
||||
/** @return 临时附件保留时间 */
|
||||
public Duration getTempRetention() { return tempRetention; }
|
||||
/** @param tempRetention 临时附件保留时间 */
|
||||
public void setTempRetention(Duration tempRetention) { this.tempRetention = tempRetention; }
|
||||
/** @return 读取线程池配置 */
|
||||
public Reader getReader() { return reader; }
|
||||
/** @param reader 读取线程池配置 */
|
||||
public void setReader(Reader reader) { this.reader = reader; }
|
||||
/** @return 格式安全边界 */
|
||||
public Limits getLimits() { return limits; }
|
||||
/** @param limits 格式安全边界 */
|
||||
public void setLimits(Limits limits) { this.limits = limits; }
|
||||
|
||||
/**
|
||||
* 校验持续时间和数据大小均为正值。
|
||||
*
|
||||
* @return 配置是否合法
|
||||
*/
|
||||
@AssertTrue(message = "文档大小与时间配置必须大于 0")
|
||||
public boolean isPositiveBoundaries() {
|
||||
return maxTotalBytesPerTurn != null && maxTotalBytesPerTurn.toBytes() > 0
|
||||
&& readTimeout != null && !readTimeout.isZero() && !readTimeout.isNegative()
|
||||
&& tempRetention != null && !tempRetention.isZero() && !tempRetention.isNegative();
|
||||
}
|
||||
|
||||
/**
|
||||
* 文档读取线程池配置。
|
||||
*/
|
||||
public static class Reader {
|
||||
|
||||
/** 核心线程数。 */
|
||||
@Min(1)
|
||||
private int coreSize = 2;
|
||||
/** 最大线程数。 */
|
||||
@Min(1)
|
||||
private int maxSize = 4;
|
||||
/** 有界队列容量。 */
|
||||
@Min(1)
|
||||
private int queueCapacity = 32;
|
||||
|
||||
/** @return 核心线程数 */
|
||||
public int getCoreSize() { return coreSize; }
|
||||
/** @param coreSize 核心线程数 */
|
||||
public void setCoreSize(int coreSize) { this.coreSize = coreSize; }
|
||||
/** @return 最大线程数 */
|
||||
public int getMaxSize() { return maxSize; }
|
||||
/** @param maxSize 最大线程数 */
|
||||
public void setMaxSize(int maxSize) { this.maxSize = maxSize; }
|
||||
/** @return 队列容量 */
|
||||
public int getQueueCapacity() { return queueCapacity; }
|
||||
/** @param queueCapacity 队列容量 */
|
||||
public void setQueueCapacity(int queueCapacity) { this.queueCapacity = queueCapacity; }
|
||||
|
||||
/**
|
||||
* 校验最大线程数不小于核心线程数。
|
||||
*
|
||||
* @return 配置是否合法
|
||||
*/
|
||||
@AssertTrue(message = "文档读取最大线程数不能小于核心线程数")
|
||||
public boolean isThreadRangeValid() {
|
||||
return maxSize >= coreSize;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 各文档格式的安全边界。
|
||||
*/
|
||||
public static class Limits {
|
||||
|
||||
/** Office 单文件上限。 */
|
||||
@NotNull
|
||||
@DataSizeUnit(DataUnit.MEGABYTES)
|
||||
private DataSize officeMaxBytes = DataSize.ofMegabytes(20);
|
||||
/** Excel 单文件上限。 */
|
||||
@NotNull
|
||||
@DataSizeUnit(DataUnit.MEGABYTES)
|
||||
private DataSize excelMaxBytes = DataSize.ofMegabytes(10);
|
||||
/** 文本单文件上限。 */
|
||||
@NotNull
|
||||
@DataSizeUnit(DataUnit.MEGABYTES)
|
||||
private DataSize textMaxBytes = DataSize.ofMegabytes(5);
|
||||
/** PDF 最大页数。 */
|
||||
@Min(1)
|
||||
private int maxPdfPages = 200;
|
||||
/** 演示文稿最大幻灯片数。 */
|
||||
@Min(1)
|
||||
private int maxSlides = 200;
|
||||
/** 表格最大工作表数。 */
|
||||
@Min(1)
|
||||
private int maxSheets = 20;
|
||||
/** 表格最大非空单元格数。 */
|
||||
@Min(1)
|
||||
private int maxNonEmptyCells = 50_000;
|
||||
/** 最大展开内容量。 */
|
||||
@NotNull
|
||||
@DataSizeUnit(DataUnit.MEGABYTES)
|
||||
private DataSize maxExpandedBytes = DataSize.ofMegabytes(150);
|
||||
|
||||
/** @return Office 单文件上限 */
|
||||
public DataSize getOfficeMaxBytes() { return officeMaxBytes; }
|
||||
/** @param officeMaxBytes Office 单文件上限 */
|
||||
public void setOfficeMaxBytes(DataSize officeMaxBytes) { this.officeMaxBytes = officeMaxBytes; }
|
||||
/** @return Excel 单文件上限 */
|
||||
public DataSize getExcelMaxBytes() { return excelMaxBytes; }
|
||||
/** @param excelMaxBytes Excel 单文件上限 */
|
||||
public void setExcelMaxBytes(DataSize excelMaxBytes) { this.excelMaxBytes = excelMaxBytes; }
|
||||
/** @return 文本单文件上限 */
|
||||
public DataSize getTextMaxBytes() { return textMaxBytes; }
|
||||
/** @param textMaxBytes 文本单文件上限 */
|
||||
public void setTextMaxBytes(DataSize textMaxBytes) { this.textMaxBytes = textMaxBytes; }
|
||||
/** @return 最大 PDF 页数 */
|
||||
public int getMaxPdfPages() { return maxPdfPages; }
|
||||
/** @param maxPdfPages 最大 PDF 页数 */
|
||||
public void setMaxPdfPages(int maxPdfPages) { this.maxPdfPages = maxPdfPages; }
|
||||
/** @return 最大幻灯片数 */
|
||||
public int getMaxSlides() { return maxSlides; }
|
||||
/** @param maxSlides 最大幻灯片数 */
|
||||
public void setMaxSlides(int maxSlides) { this.maxSlides = maxSlides; }
|
||||
/** @return 最大工作表数 */
|
||||
public int getMaxSheets() { return maxSheets; }
|
||||
/** @param maxSheets 最大工作表数 */
|
||||
public void setMaxSheets(int maxSheets) { this.maxSheets = maxSheets; }
|
||||
/** @return 最大非空单元格数 */
|
||||
public int getMaxNonEmptyCells() { return maxNonEmptyCells; }
|
||||
/** @param maxNonEmptyCells 最大非空单元格数 */
|
||||
public void setMaxNonEmptyCells(int maxNonEmptyCells) { this.maxNonEmptyCells = maxNonEmptyCells; }
|
||||
/** @return 最大展开内容量 */
|
||||
public DataSize getMaxExpandedBytes() { return maxExpandedBytes; }
|
||||
/** @param maxExpandedBytes 最大展开内容量 */
|
||||
public void setMaxExpandedBytes(DataSize maxExpandedBytes) { this.maxExpandedBytes = maxExpandedBytes; }
|
||||
|
||||
/**
|
||||
* 校验所有数据大小为正值。
|
||||
*
|
||||
* @return 配置是否合法
|
||||
*/
|
||||
@AssertTrue(message = "文档格式大小上限必须大于 0")
|
||||
public boolean isPositiveSizes() {
|
||||
return positive(officeMaxBytes) && positive(excelMaxBytes)
|
||||
&& positive(textMaxBytes) && positive(maxExpandedBytes);
|
||||
}
|
||||
|
||||
private boolean positive(DataSize value) {
|
||||
return value != null && value.toBytes() > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package tech.easyflow.agent.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* Agent 文档读取独立有界线程池配置。
|
||||
*/
|
||||
@Configuration
|
||||
public class AgentDocumentReaderConfig {
|
||||
|
||||
/**
|
||||
* 创建文档读取线程池;队列满时直接拒绝,避免任务回落到调用线程。
|
||||
*
|
||||
* @param properties 文档配置
|
||||
* @return 文档读取执行器
|
||||
*/
|
||||
@Bean(name = "agentDocumentReaderExecutor", destroyMethod = "shutdown")
|
||||
public ExecutorService agentDocumentReaderExecutor(AgentDocumentProperties properties) {
|
||||
AgentDocumentProperties.Reader reader = properties.getReader();
|
||||
return new ThreadPoolExecutor(
|
||||
reader.getCoreSize(),
|
||||
reader.getMaxSize(),
|
||||
60L,
|
||||
TimeUnit.SECONDS,
|
||||
new ArrayBlockingQueue<>(reader.getQueueCapacity()),
|
||||
new ReaderThreadFactory(),
|
||||
new ThreadPoolExecutor.AbortPolicy());
|
||||
}
|
||||
|
||||
/**
|
||||
* 文档读取线程命名工厂。
|
||||
*/
|
||||
private static final class ReaderThreadFactory implements ThreadFactory {
|
||||
|
||||
private final AtomicInteger index = new AtomicInteger(1);
|
||||
|
||||
/**
|
||||
* 创建守护读取线程。
|
||||
*
|
||||
* @param runnable 读取任务
|
||||
* @return 命名线程
|
||||
*/
|
||||
@Override
|
||||
public Thread newThread(Runnable runnable) {
|
||||
Thread thread = new Thread(runnable);
|
||||
thread.setName("agent-document-reader-" + index.getAndIncrement());
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,10 @@ import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
@MapperScan("tech.easyflow.agent.mapper")
|
||||
@ComponentScan("tech.easyflow.agent")
|
||||
@EnableScheduling
|
||||
@EnableConfigurationProperties({AgentRuntimeProperties.class, AgentMediaProperties.class})
|
||||
@EnableConfigurationProperties({
|
||||
AgentRuntimeProperties.class,
|
||||
AgentMediaProperties.class,
|
||||
AgentDocumentProperties.class
|
||||
})
|
||||
public class AgentModuleConfig {
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user