提交一次代码
This commit is contained in:
@@ -6,7 +6,13 @@ package cn.nianxx.thhotel.platform.debug.common.enums;
|
||||
public enum DebugEmlSuperAgentRunStatus {
|
||||
|
||||
CREATED,
|
||||
PARSING_EML,
|
||||
UPLOADING_ORIGINAL_EML,
|
||||
UPLOADING_MEDIA,
|
||||
BUILDING_SOURCE_MESSAGE,
|
||||
CAPTURING_SOURCE_MESSAGE,
|
||||
SOURCE_CAPTURED,
|
||||
CALLING_SUPERAGENT,
|
||||
SUPERAGENT_SUCCEEDED,
|
||||
SUPERAGENT_FAILED,
|
||||
FAILED
|
||||
|
||||
@@ -45,29 +45,29 @@ public class MybatisDebugEmlSuperAgentRunRepository implements DebugEmlSuperAgen
|
||||
*/
|
||||
@Override
|
||||
public void updateResult(DebugEmlSuperAgentRunUpdate update) {
|
||||
DebugEmlSuperAgentRunEntity entity = new DebugEmlSuperAgentRunEntity();
|
||||
entity.setId(update.id());
|
||||
entity.setSourceMessageId(update.sourceMessageId());
|
||||
entity.setExternalMessageId(update.externalMessageId());
|
||||
entity.setExternalConversationId(update.externalConversationId());
|
||||
entity.setOriginalFileName(update.originalFileName());
|
||||
entity.setOriginalEmlOssUrl(update.originalEmlOssUrl());
|
||||
entity.setOriginalEmlSha256(update.originalEmlSha256());
|
||||
entity.setPayloadJson(update.payloadJson());
|
||||
entity.setSuperagentSessionId(update.superagentSessionId());
|
||||
entity.setSuperagentRunId(update.superagentRunId());
|
||||
entity.setSuperagentProfileId(update.superagentProfileId());
|
||||
entity.setSuperagentProfileVersionId(update.superagentProfileVersionId());
|
||||
entity.setSuperagentModelName(update.superagentModelName());
|
||||
entity.setSuperagentRawAnswer(update.superagentRawAnswer());
|
||||
entity.setSuperagentParsedJson(update.superagentParsedJson());
|
||||
entity.setSuperagentInputTokens(update.superagentInputTokens());
|
||||
entity.setSuperagentOutputTokens(update.superagentOutputTokens());
|
||||
entity.setSuperagentTotalTokens(update.superagentTotalTokens());
|
||||
entity.setRunStatus(update.runStatus());
|
||||
entity.setSafeErrorSummary(update.safeErrorSummary());
|
||||
entity.setUpdatedAt(update.updatedAt());
|
||||
runMapper.updateById(entity);
|
||||
LambdaUpdateWrapper<DebugEmlSuperAgentRunEntity> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(DebugEmlSuperAgentRunEntity::getId, update.id())
|
||||
.set(DebugEmlSuperAgentRunEntity::getSourceMessageId, update.sourceMessageId())
|
||||
.set(DebugEmlSuperAgentRunEntity::getExternalMessageId, update.externalMessageId())
|
||||
.set(DebugEmlSuperAgentRunEntity::getExternalConversationId, update.externalConversationId())
|
||||
.set(DebugEmlSuperAgentRunEntity::getOriginalFileName, update.originalFileName())
|
||||
.set(DebugEmlSuperAgentRunEntity::getOriginalEmlOssUrl, update.originalEmlOssUrl())
|
||||
.set(DebugEmlSuperAgentRunEntity::getOriginalEmlSha256, update.originalEmlSha256())
|
||||
.set(DebugEmlSuperAgentRunEntity::getPayloadJson, update.payloadJson())
|
||||
.set(DebugEmlSuperAgentRunEntity::getSuperagentSessionId, update.superagentSessionId())
|
||||
.set(DebugEmlSuperAgentRunEntity::getSuperagentRunId, update.superagentRunId())
|
||||
.set(DebugEmlSuperAgentRunEntity::getSuperagentProfileId, update.superagentProfileId())
|
||||
.set(DebugEmlSuperAgentRunEntity::getSuperagentProfileVersionId, update.superagentProfileVersionId())
|
||||
.set(DebugEmlSuperAgentRunEntity::getSuperagentModelName, update.superagentModelName())
|
||||
.set(DebugEmlSuperAgentRunEntity::getSuperagentRawAnswer, update.superagentRawAnswer())
|
||||
.set(DebugEmlSuperAgentRunEntity::getSuperagentParsedJson, update.superagentParsedJson())
|
||||
.set(DebugEmlSuperAgentRunEntity::getSuperagentInputTokens, update.superagentInputTokens())
|
||||
.set(DebugEmlSuperAgentRunEntity::getSuperagentOutputTokens, update.superagentOutputTokens())
|
||||
.set(DebugEmlSuperAgentRunEntity::getSuperagentTotalTokens, update.superagentTotalTokens())
|
||||
.set(DebugEmlSuperAgentRunEntity::getRunStatus, update.runStatus())
|
||||
.set(DebugEmlSuperAgentRunEntity::getSafeErrorSummary, update.safeErrorSummary())
|
||||
.set(DebugEmlSuperAgentRunEntity::getUpdatedAt, update.updatedAt());
|
||||
runMapper.update(null, wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -252,6 +252,10 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
|
||||
byte[] emlBytes,
|
||||
LocalDateTime createdAt) throws Exception {
|
||||
String sha256 = sha256(emlBytes);
|
||||
markStage(
|
||||
runId,
|
||||
DebugEmlSuperAgentRunStatus.PARSING_EML,
|
||||
"阶段:解析 EML 邮件,文件名:" + safeFileName + ",大小:" + emlBytes.length + " bytes。");
|
||||
ParsedEmlMessage parsed = parseService.parse(emlBytes, safeFileName);
|
||||
String originalMessageId = parsed.messageId();
|
||||
String originalConversationId = parsed.conversationId();
|
||||
@@ -260,17 +264,33 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
|
||||
List<String> warnings = new ArrayList<>();
|
||||
List<UploadedMedia> uploadedMedia = new ArrayList<>();
|
||||
|
||||
markStage(
|
||||
runId,
|
||||
DebugEmlSuperAgentRunStatus.UPLOADING_ORIGINAL_EML,
|
||||
"阶段:上传原始 EML 到 OSS,文件名:" + safeFileName + ",大小:" + emlBytes.length + " bytes。");
|
||||
uploadedMedia.add(uploadOriginalEml(runId, safeFileName, emlBytes, createdAt));
|
||||
int inlineIndex = 1;
|
||||
int attachmentIndex = 1;
|
||||
for (ParsedEmlMediaItem mediaItem : parsed.mediaItems()) {
|
||||
if (SourceMessageMediaType.INLINE_IMAGE.code().equals(mediaItem.mediaType())) {
|
||||
markStage(
|
||||
runId,
|
||||
DebugEmlSuperAgentRunStatus.UPLOADING_MEDIA,
|
||||
mediaUploadStageSummary(mediaItem, "inline", inlineIndex));
|
||||
uploadedMedia.add(uploadParsedMedia(runId, createdAt, mediaItem, "inline", inlineIndex++));
|
||||
} else {
|
||||
markStage(
|
||||
runId,
|
||||
DebugEmlSuperAgentRunStatus.UPLOADING_MEDIA,
|
||||
mediaUploadStageSummary(mediaItem, "attachments", attachmentIndex));
|
||||
uploadedMedia.add(uploadParsedMedia(runId, createdAt, mediaItem, "attachments", attachmentIndex++));
|
||||
}
|
||||
}
|
||||
|
||||
markStage(
|
||||
runId,
|
||||
DebugEmlSuperAgentRunStatus.BUILDING_SOURCE_MESSAGE,
|
||||
"阶段:构造 SourceMessage payload。");
|
||||
String htmlWithOssUrls = replaceCidReferences(parsed.htmlBody(), uploadedMedia, warnings);
|
||||
String htmlBodySanitized = htmlSanitizerService.sanitizeHtml(htmlWithOssUrls);
|
||||
String htmlRenderMode = htmlSanitizerService.htmlRenderMode(htmlWithOssUrls);
|
||||
@@ -285,6 +305,10 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
|
||||
htmlWithOssUrls,
|
||||
uploadedMedia);
|
||||
String payloadJson = objectMapper.writeValueAsString(payload);
|
||||
markStage(
|
||||
runId,
|
||||
DebugEmlSuperAgentRunStatus.CAPTURING_SOURCE_MESSAGE,
|
||||
"阶段:写入 SourceMessage Inbox。");
|
||||
SourceMessageCaptureResult captureResult = sourceMessageCaptureService.capture(new CaptureSourceMessageCommand(
|
||||
hotelId,
|
||||
SOURCE_PROVIDER,
|
||||
@@ -311,6 +335,10 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
|
||||
sha256,
|
||||
payloadJson);
|
||||
|
||||
markStage(
|
||||
runId,
|
||||
DebugEmlSuperAgentRunStatus.CALLING_SUPERAGENT,
|
||||
"阶段:调用 SuperAgent Open API,source_message_id=" + captureResult.inboxId() + "。");
|
||||
SuperAgentOpenApiResult superAgentResult = superAgentOpenApiClient.invokeMailDebug(new SuperAgentMailDebugRequest(
|
||||
buildSuperAgentMessage(payloadJson),
|
||||
"debug-eml-" + runId,
|
||||
@@ -658,6 +686,23 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记 Debug 运行中的当前阶段。这里复用安全摘要字段保存短文本面包屑,便于测试环境定位卡点。
|
||||
*/
|
||||
private void markStage(
|
||||
Long runId,
|
||||
DebugEmlSuperAgentRunStatus status,
|
||||
String safeSummary) {
|
||||
if (runId == null) {
|
||||
return;
|
||||
}
|
||||
runRepository.updateStatus(new DebugEmlSuperAgentRunStatusUpdate(
|
||||
runId,
|
||||
status.name(),
|
||||
truncate(safeSummary, 512),
|
||||
nowUtc()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记 Debug 运行失败。
|
||||
*/
|
||||
@@ -676,6 +721,18 @@ public class DebugEmlSuperAgentRunServiceImpl implements DebugEmlSuperAgentRunSe
|
||||
updatedAt));
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造媒体上传阶段摘要,只包含安全元数据,不包含正文、公开 URL 或二进制内容。
|
||||
*/
|
||||
private String mediaUploadStageSummary(ParsedEmlMediaItem mediaItem, String folder, int index) {
|
||||
String fileName = safeFileName(mediaItem.fileName(), folder + "-" + index);
|
||||
return "阶段:上传邮件媒体到 OSS,目录:" + folder
|
||||
+ ",序号:" + index
|
||||
+ ",类型:" + mediaItem.mediaType()
|
||||
+ ",文件名:" + fileName
|
||||
+ ",大小:" + mediaItem.sizeBytes() + " bytes。";
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据异常选择 Debug run 状态。
|
||||
*/
|
||||
|
||||
@@ -71,6 +71,9 @@ public class HotelAccessServiceImpl implements HotelAccessService {
|
||||
.orElseGet(() -> hotels.isEmpty() ? null : hotels.get(0).hotelId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析当前用户可访问酒店;超级管理员取全部启用酒店,普通用户取授权关系。
|
||||
*/
|
||||
private Set<String> resolveAccessibleHotelIds(PlatformUserEntity user, List<PlatformHotelEntity> activeHotels) {
|
||||
if (Boolean.TRUE.equals(user.getSuperAdmin())) {
|
||||
return new LinkedHashSet<>(activeHotels.stream()
|
||||
@@ -80,6 +83,9 @@ public class HotelAccessServiceImpl implements HotelAccessService {
|
||||
return new LinkedHashSet<>(hotelRepository.listUserHotelIds(user.getId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 按前端偏好、用户默认酒店、系统默认酒店的优先级解析当前默认酒店。
|
||||
*/
|
||||
private String resolveDefaultHotelId(
|
||||
PlatformUserEntity user,
|
||||
Set<String> accessibleHotelIds,
|
||||
|
||||
@@ -161,6 +161,9 @@ public class AuthServiceImpl implements AuthService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装当前用户上下文响应,登录成功和 /me 接口共用同一套权限、酒店和菜单逻辑。
|
||||
*/
|
||||
private AuthMeResult buildMeResult(PlatformUserEntity user, String preferredHotelId) {
|
||||
List<AuthHotelResult> hotels = hotelAccessService.listAccessibleHotels(user, preferredHotelId);
|
||||
String defaultHotelId = hotelAccessService.resolveDefaultHotelId(hotels);
|
||||
@@ -180,6 +183,9 @@ public class AuthServiceImpl implements AuthService {
|
||||
menus);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建请求线程内的用户上下文,供后续业务接口审计和权限收口复用。
|
||||
*/
|
||||
private AuthenticatedUserContext buildSecurityContext(PlatformUserEntity user) {
|
||||
List<AuthHotelResult> hotels = hotelAccessService.listAccessibleHotels(user, null);
|
||||
List<String> permissions = accessControlService.listPermissionCodes(
|
||||
@@ -195,11 +201,17 @@ public class AuthServiceImpl implements AuthService {
|
||||
permissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制解析当前 token 对应 session;登录态恢复接口必须拿到有效 session。
|
||||
*/
|
||||
private AuthSessionSnapshot resolveRequiredSession(String authorizationHeader) {
|
||||
String token = extractRequiredBearerToken(authorizationHeader);
|
||||
return resolveSessionByToken(token, true).orElseThrow(this::invalidSession);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据明文 token 查找有效 session,并按调用场景决定是否抛出无效登录态异常。
|
||||
*/
|
||||
private Optional<AuthSessionSnapshot> resolveSessionByToken(String token, boolean strict) {
|
||||
String tokenHash = tokenService.hashToken(token);
|
||||
Optional<PlatformUserSessionEntity> session = identityRepository.findSessionByTokenHash(tokenHash);
|
||||
@@ -222,6 +234,9 @@ public class AuthServiceImpl implements AuthService {
|
||||
return user.map(value -> new AuthSessionSnapshot(existingSession.getId(), existingSession.getExpiresAt(), value));
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新 session 最近访问时间;只更新访问时间字段,避免覆盖 token 安全字段。
|
||||
*/
|
||||
private void touchSession(Long sessionId) {
|
||||
if (sessionId == null) {
|
||||
return;
|
||||
@@ -233,16 +248,25 @@ public class AuthServiceImpl implements AuthService {
|
||||
identityRepository.updateSession(session);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将过期 session 标记为 EXPIRED,避免后续重复按 ACTIVE 处理。
|
||||
*/
|
||||
private void markExpired(PlatformUserSessionEntity session) {
|
||||
session.setSessionStatus(PlatformUserSessionStatus.EXPIRED.name());
|
||||
session.setUpdatedAt(nowUtc());
|
||||
identityRepository.updateSession(session);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断用户是否为可登录状态,禁用用户不能登录或继续使用旧 session。
|
||||
*/
|
||||
private boolean isActiveUser(PlatformUserEntity user) {
|
||||
return PlatformUserStatus.ACTIVE.name().equals(user.getUserStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析必须存在的 Bearer token;缺失时返回受控 401。
|
||||
*/
|
||||
private String extractRequiredBearerToken(String authorizationHeader) {
|
||||
return extractBearerToken(authorizationHeader)
|
||||
.orElseThrow(() -> new AuthException(
|
||||
@@ -251,6 +275,9 @@ public class AuthServiceImpl implements AuthService {
|
||||
"请先登录。"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Authorization 请求头中解析 Bearer token;无效格式按空 token 处理。
|
||||
*/
|
||||
private Optional<String> extractBearerToken(String authorizationHeader) {
|
||||
if (authorizationHeader == null || authorizationHeader.isBlank()) {
|
||||
return Optional.empty();
|
||||
@@ -263,6 +290,9 @@ public class AuthServiceImpl implements AuthService {
|
||||
return token.isBlank() ? Optional.empty() : Optional.of(token);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建统一的 session 失效异常,避免向前端泄露 token 或账号内部状态。
|
||||
*/
|
||||
private AuthException invalidSession() {
|
||||
return new AuthException(
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
@@ -270,10 +300,16 @@ public class AuthServiceImpl implements AuthService {
|
||||
"登录已失效,请重新登录。");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 UTC 当前时间,数据库 LocalDateTime 统一按 UTC 语义保存。
|
||||
*/
|
||||
private LocalDateTime nowUtc() {
|
||||
return LocalDateTime.now(ZoneOffset.UTC);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取客户端 IP 摘要,第一版只保留请求直接来源。
|
||||
*/
|
||||
private String clientIp(HttpServletRequest request) {
|
||||
if (request == null) {
|
||||
return null;
|
||||
@@ -285,6 +321,9 @@ public class AuthServiceImpl implements AuthService {
|
||||
return truncate(request.getRemoteAddr(), 64);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取 User-Agent 摘要,避免过长请求头直接进入 session 表。
|
||||
*/
|
||||
private String userAgent(HttpServletRequest request) {
|
||||
if (request == null) {
|
||||
return null;
|
||||
@@ -292,6 +331,9 @@ public class AuthServiceImpl implements AuthService {
|
||||
return truncate(request.getHeader("User-Agent"), 256);
|
||||
}
|
||||
|
||||
/**
|
||||
* 截断外部输入摘要字段,保护数据库字段长度和日志可读性。
|
||||
*/
|
||||
private String truncate(String value, int maxLength) {
|
||||
if (value == null || value.length() <= maxLength) {
|
||||
return value;
|
||||
|
||||
Reference in New Issue
Block a user