feat: 完善 Agent 标准交互与安全运行时

- 接入 AG-UI 运行投影、Turn 时间线和审批隔离

- 增加 Agent Skill 冻结绑定与运行时消费闭环

- 增加受控工作区、内置工具和私有 Artifact 生命周期
This commit is contained in:
2026-08-19 22:13:41 +08:00
parent 91d66e636d
commit 4e8640dcaf
241 changed files with 24382 additions and 2777 deletions

View File

@@ -129,7 +129,7 @@ public class ChatPersistDispatcher {
payload.setUserId(userId);
payload.setOperatorId(operatorId);
payload.setOperateAt(operateAt);
eventProducer.send(buildEvent(
ChatPersistEvent event = buildEvent(
UUID.randomUUID().toString(),
ChatPersistEventType.SESSION_DELETED,
sessionId,
@@ -137,7 +137,9 @@ public class ChatPersistDispatcher {
BigInteger.ZERO,
operateAt,
chatJsonSupport.toJson(payload)
));
);
persistImmediately(event);
eventProducer.send(event);
}
private void appendMessage(ChatAppendMessageCommand command, ChatPersistEventType eventType) {

View File

@@ -29,6 +29,26 @@ public interface ChatRoundOperateService {
*/
List<ChatMessageRecord> listVariants(BigInteger sessionId, BigInteger roundId);
/**
* 查询轮次下未执行业务安全投影的答案版本,供完整会话批量投影使用。
*
* @param sessionId 会话 ID
* @param roundId 轮次 ID
* @return 原始答案版本列表
*/
default List<ChatMessageRecord> listVariantsUnprojected(BigInteger sessionId, BigInteger roundId) {
return listVariants(sessionId, roundId);
}
/**
* 对同一会话的答案版本执行一次批量业务安全投影。
*
* @param sessionId 会话 ID
* @param records 待投影答案版本
*/
default void projectVariants(BigInteger sessionId, List<ChatMessageRecord> records) {
}
/**
* 切换指定轮次当前选中的答案版本。
*

View File

@@ -0,0 +1,50 @@
package tech.easyflow.chatlog.service;
import tech.easyflow.chatlog.domain.dto.ChatMessageRecord;
import tech.easyflow.chatlog.domain.dto.ChatSessionSummary;
import java.math.BigInteger;
import java.util.List;
/**
* 聊天会话删除与历史返回的业务扩展点。
*/
public interface ChatSessionExtension {
/**
* 判断扩展是否处理指定会话类型。
*
* @param summary 会话摘要
* @return 需要处理时为 true
*/
boolean supports(ChatSessionSummary summary);
/**
* 在会话删除落库前同步处理关联资源。
*
* @param summary 会话摘要
* @param userId 会话用户 ID
* @param operatorId 操作人 ID
*/
default void beforeDelete(ChatSessionSummary summary, BigInteger userId, BigInteger operatorId) {
}
/**
* 在会话删除分发成功后同步处理关联资源。
*
* @param summary 会话摘要
* @param userId 会话用户 ID
* @param operatorId 操作人 ID
*/
default void afterDelete(ChatSessionSummary summary, BigInteger userId, BigInteger operatorId) {
}
/**
* 在历史消息返回前覆盖业务安全投影。
*
* @param summary 会话摘要
* @param records 本次返回的消息集合
*/
default void projectMessages(ChatSessionSummary summary, List<ChatMessageRecord> records) {
}
}

View File

@@ -0,0 +1,70 @@
package tech.easyflow.chatlog.service;
import org.springframework.stereotype.Component;
import tech.easyflow.chatlog.domain.dto.ChatMessageRecord;
import tech.easyflow.chatlog.domain.dto.ChatSessionSummary;
import java.math.BigInteger;
import java.util.List;
/**
* 按会话类型同步分发会话生命周期与历史投影扩展。
*/
@Component
public class ChatSessionExtensionDispatcher {
private final List<ChatSessionExtension> extensions;
/**
* 创建扩展分发器。
*
* @param extensions 当前应用注册的会话扩展
*/
public ChatSessionExtensionDispatcher(List<ChatSessionExtension> extensions) {
this.extensions = extensions == null ? List.of() : List.copyOf(extensions);
}
/**
* 在会话删除前同步执行匹配扩展。
*
* @param summary 会话摘要
* @param userId 会话用户 ID
* @param operatorId 操作人 ID
*/
public void beforeDelete(ChatSessionSummary summary, BigInteger userId, BigInteger operatorId) {
for (ChatSessionExtension extension : extensions) {
if (extension.supports(summary)) {
extension.beforeDelete(summary, userId, operatorId);
}
}
}
/**
* 在会话删除分发成功后同步执行匹配扩展。
*
* @param summary 会话摘要
* @param userId 会话用户 ID
* @param operatorId 操作人 ID
*/
public void afterDelete(ChatSessionSummary summary, BigInteger userId, BigInteger operatorId) {
for (ChatSessionExtension extension : extensions) {
if (extension.supports(summary)) {
extension.afterDelete(summary, userId, operatorId);
}
}
}
/**
* 在消息返回前同步执行匹配扩展。
*
* @param summary 会话摘要
* @param records 本次返回消息
*/
public void projectMessages(ChatSessionSummary summary, List<ChatMessageRecord> records) {
for (ChatSessionExtension extension : extensions) {
if (extension.supports(summary)) {
extension.projectMessages(summary, records);
}
}
}
}

View File

@@ -5,6 +5,8 @@ import tech.easyflow.chatlog.domain.dto.ChatHistoryPage;
import tech.easyflow.chatlog.domain.query.ChatPageQuery;
import tech.easyflow.chatlog.repository.analyticaldb.ChatAnalyticalDBRepository;
import tech.easyflow.chatlog.service.ChatHistoryQueryService;
import tech.easyflow.chatlog.service.ChatSessionExtensionDispatcher;
import tech.easyflow.chatlog.service.ChatSessionQueryService;
import java.math.BigInteger;
@@ -12,13 +14,29 @@ import java.math.BigInteger;
public class ChatHistoryQueryServiceImpl implements ChatHistoryQueryService {
private final ChatAnalyticalDBRepository chatAnalyticalDBRepository;
private final ChatSessionQueryService chatSessionQueryService;
private final ChatSessionExtensionDispatcher extensionDispatcher;
public ChatHistoryQueryServiceImpl(ChatAnalyticalDBRepository chatAnalyticalDBRepository) {
/**
* 创建归档历史查询服务。
*
* @param chatAnalyticalDBRepository 分析库仓储
* @param chatSessionQueryService 会话摘要查询服务
* @param extensionDispatcher 会话业务扩展分发器
*/
public ChatHistoryQueryServiceImpl(ChatAnalyticalDBRepository chatAnalyticalDBRepository,
ChatSessionQueryService chatSessionQueryService,
ChatSessionExtensionDispatcher extensionDispatcher) {
this.chatAnalyticalDBRepository = chatAnalyticalDBRepository;
this.chatSessionQueryService = chatSessionQueryService;
this.extensionDispatcher = extensionDispatcher;
}
@Override
public ChatHistoryPage queryHistoryMessages(BigInteger sessionId, ChatPageQuery query) {
return chatAnalyticalDBRepository.queryHistory(sessionId, query);
ChatHistoryPage page = chatAnalyticalDBRepository.queryHistory(sessionId, query);
extensionDispatcher.projectMessages(
chatSessionQueryService.getSessionSummary(sessionId), page.getRecords());
return page;
}
}

View File

@@ -1,12 +1,16 @@
package tech.easyflow.chatlog.service.impl;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import tech.easyflow.chatlog.domain.command.ChatRoundSelectCommand;
import tech.easyflow.chatlog.domain.dto.ChatMessageRecord;
import tech.easyflow.chatlog.domain.dto.ChatRoundRecord;
import tech.easyflow.chatlog.service.ChatRoundCommandService;
import tech.easyflow.chatlog.service.ChatRoundOperateService;
import tech.easyflow.chatlog.service.ChatRoundQueryService;
import tech.easyflow.chatlog.service.ChatSessionExtensionDispatcher;
import tech.easyflow.chatlog.service.ChatSessionQueryService;
import tech.easyflow.chatlog.support.ChatConstants;
import tech.easyflow.common.web.exceptions.BusinessException;
@@ -23,6 +27,8 @@ public class ChatRoundOperateServiceImpl implements ChatRoundOperateService {
private final ChatRoundQueryService chatRoundQueryService;
private final ChatRoundCommandService chatRoundCommandService;
private ChatSessionQueryService chatSessionQueryService;
private ChatSessionExtensionDispatcher extensionDispatcher;
public ChatRoundOperateServiceImpl(ChatRoundQueryService chatRoundQueryService,
ChatRoundCommandService chatRoundCommandService) {
@@ -30,6 +36,20 @@ public class ChatRoundOperateServiceImpl implements ChatRoundOperateService {
this.chatRoundCommandService = chatRoundCommandService;
}
/**
* 延迟注入会话投影依赖,避免会话查询服务与轮次服务形成初始化环。
*
* @param chatSessionQueryService 会话查询服务
* @param extensionDispatcher 会话扩展分发器
*/
@Autowired
@Lazy
public void setProjectionDependencies(ChatSessionQueryService chatSessionQueryService,
ChatSessionExtensionDispatcher extensionDispatcher) {
this.chatSessionQueryService = chatSessionQueryService;
this.extensionDispatcher = extensionDispatcher;
}
@Override
public ChatRoundRecord requireRegeneratableRound(BigInteger sessionId, BigInteger roundId) {
ChatRoundRecord round = requireLatestRound(sessionId, roundId);
@@ -42,6 +62,13 @@ public class ChatRoundOperateServiceImpl implements ChatRoundOperateService {
@Override
public List<ChatMessageRecord> listVariants(BigInteger sessionId, BigInteger roundId) {
List<ChatMessageRecord> variants = listVariantsUnprojected(sessionId, roundId);
projectVariants(sessionId, variants);
return variants;
}
@Override
public List<ChatMessageRecord> listVariantsUnprojected(BigInteger sessionId, BigInteger roundId) {
ChatRoundRecord round = chatRoundQueryService.getRound(sessionId, roundId);
if (round == null) {
throw new BusinessException("轮次不存在");
@@ -59,6 +86,17 @@ public class ChatRoundOperateServiceImpl implements ChatRoundOperateService {
return variants;
}
@Override
public void projectVariants(BigInteger sessionId, List<ChatMessageRecord> records) {
if (records == null || records.isEmpty()) {
return;
}
if (chatSessionQueryService == null || extensionDispatcher == null) {
throw new IllegalStateException("聊天答案版本安全投影服务未初始化");
}
extensionDispatcher.projectMessages(chatSessionQueryService.getSessionSummary(sessionId), records);
}
@Override
public ChatMessageRecord selectVariant(BigInteger sessionId, BigInteger roundId, Integer variantIndex, BigInteger operatorId) {
ChatRoundRecord round = requireLatestRound(sessionId, roundId);
@@ -81,6 +119,7 @@ public class ChatRoundOperateServiceImpl implements ChatRoundOperateService {
selected.setSelectedVariantIndex(variantIndex);
selected.setVariantCount(round.getVariantCount());
selected.setSwitchable(true);
projectVariants(sessionId, List.of(selected));
return selected;
}

View File

@@ -5,6 +5,8 @@ import tech.easyflow.chatlog.domain.command.ChatSessionUpsertCommand;
import tech.easyflow.chatlog.domain.dto.ChatSessionSummary;
import tech.easyflow.chatlog.service.ChatPersistDispatcher;
import tech.easyflow.chatlog.service.ChatSessionCommandService;
import tech.easyflow.chatlog.service.ChatSessionExtensionDispatcher;
import tech.easyflow.chatlog.service.ChatSessionQueryService;
import java.math.BigInteger;
@@ -12,9 +14,22 @@ import java.math.BigInteger;
public class ChatSessionCommandServiceImpl implements ChatSessionCommandService {
private final ChatPersistDispatcher chatPersistDispatcher;
private final ChatSessionQueryService chatSessionQueryService;
private final ChatSessionExtensionDispatcher extensionDispatcher;
public ChatSessionCommandServiceImpl(ChatPersistDispatcher chatPersistDispatcher) {
/**
* 创建会话命令服务。
*
* @param chatPersistDispatcher 聊天持久化分发器
* @param chatSessionQueryService 会话查询服务
* @param extensionDispatcher 会话业务扩展分发器
*/
public ChatSessionCommandServiceImpl(ChatPersistDispatcher chatPersistDispatcher,
ChatSessionQueryService chatSessionQueryService,
ChatSessionExtensionDispatcher extensionDispatcher) {
this.chatPersistDispatcher = chatPersistDispatcher;
this.chatSessionQueryService = chatSessionQueryService;
this.extensionDispatcher = extensionDispatcher;
}
@Override
@@ -29,6 +44,9 @@ public class ChatSessionCommandServiceImpl implements ChatSessionCommandService
@Override
public void deleteSession(BigInteger sessionId, BigInteger userId, BigInteger operatorId) {
ChatSessionSummary summary = chatSessionQueryService.getSessionSummary(sessionId);
extensionDispatcher.beforeDelete(summary, userId, operatorId);
chatPersistDispatcher.deleteSession(sessionId, userId, operatorId);
extensionDispatcher.afterDelete(summary, userId, operatorId);
}
}

View File

@@ -1,6 +1,7 @@
package tech.easyflow.chatlog.service.impl;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import tech.easyflow.chatlog.cache.ChatHotStateService;
import tech.easyflow.chatlog.domain.dto.ChatHistoryPage;
import tech.easyflow.chatlog.domain.dto.ChatMessageRecord;
@@ -11,6 +12,7 @@ import tech.easyflow.chatlog.repository.mysql.MySqlChatLogRepository;
import tech.easyflow.chatlog.repository.mysql.MySqlChatLogTableManager;
import tech.easyflow.chatlog.repository.mysql.MySqlChatSessionRepository;
import tech.easyflow.chatlog.service.ChatSessionQueryService;
import tech.easyflow.chatlog.service.ChatSessionExtensionDispatcher;
import java.math.BigInteger;
import java.util.*;
@@ -22,6 +24,8 @@ public class ChatSessionQueryServiceImpl implements ChatSessionQueryService {
private final MySqlChatLogRepository logRepository;
private final MySqlChatLogTableManager tableManager;
private final ChatHotStateService chatHotStateService;
private ChatSessionExtensionDispatcher extensionDispatcher =
new ChatSessionExtensionDispatcher(List.of());
public ChatSessionQueryServiceImpl(MySqlChatSessionRepository sessionRepository,
MySqlChatLogRepository logRepository,
@@ -33,6 +37,16 @@ public class ChatSessionQueryServiceImpl implements ChatSessionQueryService {
this.chatHotStateService = chatHotStateService;
}
/**
* 设置会话历史业务扩展分发器。
*
* @param extensionDispatcher 扩展分发器
*/
@Autowired
public void setExtensionDispatcher(ChatSessionExtensionDispatcher extensionDispatcher) {
this.extensionDispatcher = extensionDispatcher;
}
@Override
public List<ChatSessionSummary> listSessions(BigInteger userId, BigInteger assistantId, ChatPageQuery query) {
return listSessions(userId, assistantId, null, query);
@@ -97,22 +111,31 @@ public class ChatSessionQueryServiceImpl implements ChatSessionQueryService {
);
page.setRecords(records);
page.setTotal(Math.max(total, query.getOffset() + records.size()));
extensionDispatcher.projectMessages(summary, records);
return page;
}
@Override
public List<ChatMessageRecord> listMainlineMessages(BigInteger sessionId) {
return logRepository.listMainlineMessages(sessionId, tableManager.listRecentExistingMonths(3));
ChatSessionSummary summary = getSessionSummary(sessionId);
List<ChatMessageRecord> records =
logRepository.listMainlineMessages(sessionId, tableManager.listRecentExistingMonths(3));
extensionDispatcher.projectMessages(summary, records);
return records;
}
@Override
public List<ChatMessageRecord> getRecentTail(BigInteger sessionId, int limit) {
ChatSessionSummary summary = getSessionSummary(sessionId);
List<ChatMessageRecord> cached = chatHotStateService.getSessionTail(sessionId);
if (cached != null && isTailReliable(cached)) {
return cached.subList(0, Math.min(limit, cached.size()));
List<ChatMessageRecord> records = cached.subList(0, Math.min(limit, cached.size()));
extensionDispatcher.projectMessages(summary, records);
return records;
}
List<ChatMessageRecord> records = logRepository.listRecentTail(sessionId, tableManager.listRecentExistingMonths(3), limit);
chatHotStateService.setSessionTail(sessionId, records);
extensionDispatcher.projectMessages(summary, records);
return records;
}