From 3645d8d06288a14de3dc279b41bdfadc0d9602b2 Mon Sep 17 00:00:00 2001 From: dengqichen Date: Fri, 14 Nov 2025 11:18:00 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=9E=84=E5=BB=BA=E9=80=9A?= =?UTF-8?q?=E7=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../IJenkinsServiceIntegration.java | 9 +- .../impl/JenkinsServiceIntegrationImpl.java | 279 +++++++++++------- .../delegate/JenkinsBuildDelegate.java | 4 +- .../delegate/NotificationNodeDelegate.java | 36 ++- 4 files changed, 201 insertions(+), 127 deletions(-) diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/integration/IJenkinsServiceIntegration.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/integration/IJenkinsServiceIntegration.java index c59d58ee..03db9e59 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/integration/IJenkinsServiceIntegration.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/integration/IJenkinsServiceIntegration.java @@ -21,13 +21,6 @@ public interface IJenkinsServiceIntegration extends IExternalSystemIntegration { */ boolean testConnection(ExternalSystem system); - /** - * 获取Jenkins Crumb(用于CSRF保护) - * - * @param externalSystem Jenkins系统配置 - * @return Crumb响应信息 - */ - JenkinsCrumbIssuerResponse getJenkinsCrumbIssue(ExternalSystem externalSystem); /** * 使用参数触发构建 @@ -88,7 +81,7 @@ public interface IJenkinsServiceIntegration extends IExternalSystemIntegration { * 获取任务详情 * * @param externalSystem Jenkins系统配置 - * @param jobName 任务名称 + * @param jobName 任务名称 * @return 任务详情 */ JenkinsJobResponse job(ExternalSystem externalSystem, String jobName); diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/integration/impl/JenkinsServiceIntegrationImpl.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/integration/impl/JenkinsServiceIntegrationImpl.java index e50c1648..65728385 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/integration/impl/JenkinsServiceIntegrationImpl.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/integration/impl/JenkinsServiceIntegrationImpl.java @@ -7,6 +7,7 @@ import com.qqchen.deploy.backend.deploy.entity.ExternalSystem; import com.qqchen.deploy.backend.deploy.enums.JenkinsBuildStatus; import com.qqchen.deploy.backend.deploy.integration.IJenkinsServiceIntegration; import com.qqchen.deploy.backend.deploy.integration.response.JenkinsBuildResponse; +import com.qqchen.deploy.backend.deploy.integration.response.JenkinsConsoleOutputResponse; import com.qqchen.deploy.backend.deploy.integration.response.JenkinsJobResponse; import com.qqchen.deploy.backend.deploy.integration.response.JenkinsQueueBuildInfoResponse; import com.qqchen.deploy.backend.deploy.integration.response.JenkinsCrumbIssuerResponse; @@ -29,18 +30,22 @@ import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; import org.apache.commons.lang3.StringUtils; + import java.io.StringReader; import java.io.StringWriter; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; + import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; + import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; + import org.xml.sax.InputSource; import java.nio.charset.Charset; @@ -48,6 +53,7 @@ import java.util.Base64; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; @Slf4j @Service @@ -58,20 +64,84 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration private final RestTemplate restTemplate = new RestTemplate(); + // Jenkins Crumb缓存 - 线程安全 + private static final Map CRUMB_CACHE = new ConcurrentHashMap<>(); + + private static final long CRUMB_EXPIRE_TIME = 25 * 60 * 1000; // 25分钟过期 + + /** + * Jenkins Crumb缓存内部类 + */ + private static class JenkinsCrumbCache { + final JenkinsCrumbIssuerResponse crumb; + + final ExternalSystem decryptedSystem; + + final long expireTime; + + JenkinsCrumbCache(JenkinsCrumbIssuerResponse crumb, ExternalSystem system) { + this.crumb = crumb; + this.decryptedSystem = system; + this.expireTime = System.currentTimeMillis() + CRUMB_EXPIRE_TIME; + } + + boolean isExpired() { + return System.currentTimeMillis() > expireTime; + } + } + + /** + * 线程安全地获取Jenkins Crumb缓存 + * 如果缓存不存在或已过期,会重新获取和解密 + */ + private synchronized JenkinsCrumbCache getCrumbCache(ExternalSystem system) { + Long systemId = system.getId(); + JenkinsCrumbCache cache = CRUMB_CACHE.get(systemId); + + if (cache == null || cache.isExpired()) { + log.debug("Jenkins Crumb缓存失效,重新获取: systemId={}", systemId); + + // 解密系统信息(只在这里解密一次) + ExternalSystem decryptedSystem = decryptSystem(system); + + // 获取新的crumb + JenkinsCrumbIssuerResponse crumb = fetchCrumbFromJenkins(decryptedSystem); + + // 创建新缓存 + cache = new JenkinsCrumbCache(crumb, decryptedSystem); + CRUMB_CACHE.put(systemId, cache); + + log.debug("Jenkins Crumb缓存已更新: systemId={}, expireTime={}", systemId, cache.expireTime); + } + + return cache; + } + + /** + * 直接从Jenkins服务器获取Crumb(不经过缓存) + */ + private JenkinsCrumbIssuerResponse fetchCrumbFromJenkins(ExternalSystem decryptedSystem) { + String url = decryptedSystem.getUrl() + "/crumbIssuer/api/json"; + HttpHeaders headers = createBasicHeaders(decryptedSystem); + HttpEntity entity = new HttpEntity<>(headers); + + ResponseEntity response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class); + return convertResponse(response); + } + @Override public boolean testConnection(ExternalSystem system) { - // 解密系统信息 - system = decryptSystem(system); - try { + // 直接使用原始系统信息构建URL(URL不需要解密) String url = system.getUrl() + "/api/json"; - + + // 创建请求头(内部自动处理解密和crumb) HttpHeaders headers = createHeaders(system); - + // 打印实际发送的请求头 log.info("Authorization头: {}", headers.getFirst("Authorization")); log.info("==================================="); - + HttpEntity entity = new HttpEntity<>(headers); ResponseEntity response = restTemplate.exchange( @@ -88,71 +158,67 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration } } + /** + * 创建包含认证信息和Jenkins Crumb的完整请求头 + * 内部自动处理缓存获取和解密 + */ private HttpHeaders createHeaders(ExternalSystem system) { - HttpHeaders headers = new HttpHeaders(); - switch (system.getAuthType()) { - case BASIC -> { - String auth = system.getUsername() + ":" + system.getPassword(); - byte[] encodedAuth = Base64.getEncoder().encode(auth.getBytes()); - headers.set("Authorization", "Basic " + new String(encodedAuth)); + // 获取缓存的解密系统和crumb + JenkinsCrumbCache cache = getCrumbCache(system); + ExternalSystem decryptedSystem = cache.decryptedSystem; + JenkinsCrumbIssuerResponse crumb = cache.crumb; + + // 创建基础认证头 + HttpHeaders headers = createBasicHeaders(decryptedSystem); + + // 自动添加Jenkins Crumb和Cookie(如果存在) + if (crumb != null) { + if (crumb.getCrumb() != null) { + headers.set("Jenkins-Crumb", crumb.getCrumb()); + } + if (crumb.getCookie() != null) { + headers.set("Cookie", crumb.getCookie()); } - case TOKEN -> headers.set("Authorization", "Bearer " + system.getToken()); - case OAUTH -> headers.set("Authorization", "Bearer " + system.getToken()); } + return headers; } - private HttpEntity createHttpEntity(ExternalSystem jenkins) { + /** + * 创建基础认证头(不包含crumb),用于获取crumb时避免循环依赖 + */ + private HttpHeaders createBasicHeaders(ExternalSystem decryptedSystem) { HttpHeaders headers = new HttpHeaders(); - - // 根据认证类型设置认证信息 - switch (jenkins.getAuthType()) { + switch (decryptedSystem.getAuthType()) { case BASIC -> { - // Basic认证 - String auth = jenkins.getUsername() + ":" + jenkins.getPassword(); + String auth = decryptedSystem.getUsername() + ":" + decryptedSystem.getPassword(); byte[] encodedAuth = Base64.getEncoder().encode(auth.getBytes()); - String authHeader = "Basic " + new String(encodedAuth); - headers.set("Authorization", authHeader); + headers.set("Authorization", "Basic " + new String(encodedAuth)); } case TOKEN -> { // Jenkins API Token认证也使用Basic Auth格式:username:apiToken - String auth = jenkins.getUsername() + ":" + jenkins.getToken(); + String auth = decryptedSystem.getUsername() + ":" + decryptedSystem.getToken(); byte[] encodedAuth = Base64.getEncoder().encode(auth.getBytes()); - String authHeader = "Basic " + new String(encodedAuth); - headers.set("Authorization", authHeader); + headers.set("Authorization", "Basic " + new String(encodedAuth)); } - default -> throw new RuntimeException("Unsupported authentication type: " + jenkins.getAuthType()); + default -> throw new RuntimeException("Unsupported authentication type: " + decryptedSystem.getAuthType()); } // 设置接受JSON响应 headers.set("Accept", "application/json"); - - return new HttpEntity<>(headers); + return headers; } - @Override - public JenkinsCrumbIssuerResponse getJenkinsCrumbIssue(ExternalSystem externalSystem) { - externalSystem = decryptSystem(externalSystem); - String url = externalSystem.getUrl() + "/crumbIssuer/api/json"; - HttpHeaders headers = createHeaders(externalSystem); - HttpEntity entity = new HttpEntity<>(headers); - return convertResponse(restTemplate.exchange(url, HttpMethod.GET, entity, String.class)); - } - @Override public String buildWithParameters(ExternalSystem externalSystem, String jobName, Map parameters) { - externalSystem = decryptSystem(externalSystem); try { // 如果参数中包含PIPELINE_SCRIPT,确保Job配置中有该参数 if (parameters.containsKey("PIPELINE_SCRIPT")) { ensurePipelineScriptParameter(externalSystem, jobName); } - // 1. 获取Crumb - JenkinsCrumbIssuerResponse jenkinsCrumbIssue = getJenkinsCrumbIssue(externalSystem); - - // 2. 构建URL + // 直接使用原始系统信息构建URL(URL不需要解密) String url = UriComponentsBuilder.fromHttpUrl(externalSystem.getUrl()) .path("/job/") .path(jobName) @@ -160,11 +226,9 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration .build() .toUriString(); - // 3. 构建请求头 + // 创建请求头(自动包含认证和crumb) HttpHeaders headers = createHeaders(externalSystem); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); - headers.set("Jenkins-Crumb", jenkinsCrumbIssue.getCrumb()); - headers.set("Cookie", jenkinsCrumbIssue.getCookie()); // 4. 构建请求体 MultiValueMap formData = new LinkedMultiValueMap<>(); @@ -196,25 +260,20 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration */ private void ensurePipelineScriptParameter(ExternalSystem externalSystem, String jobName) { try { - // 获取 Crumb - JenkinsCrumbIssuerResponse jenkinsCrumbIssue = getJenkinsCrumbIssue(externalSystem); - - // 获取Job配置 + // 直接使用原始系统信息构建Job配置URL(URL不需要解密) String configUrl = String.format("%s/job/%s/config.xml", externalSystem.getUrl(), jobName); + + // 创建请求头(自动包含认证和crumb) HttpHeaders headers = createHeaders(externalSystem); - // 设置请求和响应的字符编码 headers.set(HttpHeaders.ACCEPT_CHARSET, "UTF-8"); headers.set(HttpHeaders.CONTENT_TYPE, "application/xml;charset=UTF-8"); headers.setAcceptCharset(Collections.singletonList(Charset.forName("UTF-8"))); - - headers.set("Jenkins-Crumb", jenkinsCrumbIssue.getCrumb()); - headers.set("Cookie", jenkinsCrumbIssue.getCookie()); - + HttpEntity entity = new HttpEntity<>(headers); - + // 配置RestTemplate,确保使用UTF-8编码 restTemplate.getMessageConverters().add(0, new StringHttpMessageConverter(Charset.forName("UTF-8"))); - + ResponseEntity response = restTemplate.exchange( configUrl, HttpMethod.GET, @@ -232,7 +291,7 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration factory.setFeature("http://xml.org/sax/features/external-general-entities", false); factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); DocumentBuilder builder = factory.newDocumentBuilder(); - + // 使用UTF-8编码解析XML InputSource is = new InputSource(new StringReader(response.getBody())); is.setEncoding("UTF-8"); @@ -259,7 +318,7 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration // 需要添加PIPELINE_SCRIPT参数 Element root = doc.getDocumentElement(); - + // 获取或创建properties元素 NodeList propertiesList = doc.getElementsByTagName("properties"); Element properties; @@ -292,19 +351,19 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration // 创建PIPELINE_SCRIPT参数定义 Element stringParam = doc.createElement("hudson.model.StringParameterDefinition"); - + Element name = doc.createElement("name"); name.setTextContent("PIPELINE_SCRIPT"); stringParam.appendChild(name); - + Element description = doc.createElement("description"); description.setTextContent("Pipeline脚本内容"); stringParam.appendChild(description); - + Element defaultValue = doc.createElement("defaultValue"); defaultValue.setTextContent(""); stringParam.appendChild(defaultValue); - + Element trim = doc.createElement("trim"); trim.setTextContent("false"); stringParam.appendChild(trim); @@ -327,8 +386,6 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration updateHeaders.setContentType(MediaType.APPLICATION_XML); updateHeaders.set(HttpHeaders.ACCEPT_CHARSET, "UTF-8"); updateHeaders.set("Content-Type", "application/xml; charset=utf-8"); - updateHeaders.set("Jenkins-Crumb", jenkinsCrumbIssue.getCrumb()); - updateHeaders.set("Cookie", jenkinsCrumbIssue.getCookie()); HttpEntity updateEntity = new HttpEntity<>(updatedConfig, updateHeaders); restTemplate.exchange( configUrl, @@ -351,7 +408,7 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration if (xml == null) { return ""; } - + // 移除XML中的无效字符 StringBuilder cleanXml = new StringBuilder(); for (int i = 0; i < xml.length(); i++) { @@ -373,9 +430,14 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration @Override public JenkinsQueueBuildInfoResponse getQueuedBuildInfo(ExternalSystem externalSystem, String queueId) { - externalSystem = decryptSystem(externalSystem); + // 直接使用原始系统信息构建URL(URL不需要解密) String queueUrl = String.format("%s/queue/item/%s/api/json", externalSystem.getUrl().trim(), queueId); - ResponseEntity> response = restTemplate.exchange(queueUrl, HttpMethod.GET, createHttpEntity(externalSystem), new ParameterizedTypeReference<>() { + + // 创建请求头(内部自动处理解密和crumb) + HttpHeaders headers = createHeaders(externalSystem); + HttpEntity entity = new HttpEntity<>(headers); + + ResponseEntity> response = restTemplate.exchange(queueUrl, HttpMethod.GET, entity, new ParameterizedTypeReference<>() { }); Map queueInfo = response.getBody(); @@ -404,13 +466,18 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration @Override public JenkinsBuildStatus getBuildStatus(ExternalSystem externalSystem, String jobName, Integer buildNumber) { - externalSystem = decryptSystem(externalSystem); try { + // 直接使用原始系统信息构建URL(URL不需要解密) String url = String.format("%s/job/%s/%d/api/json", externalSystem.getUrl().trim(), jobName, buildNumber); + + // 创建请求头(内部自动处理解密和crumb) + HttpHeaders headers = createHeaders(externalSystem); + HttpEntity entity = new HttpEntity<>(headers); + ResponseEntity> response = restTemplate.exchange( url, HttpMethod.GET, - createHttpEntity(externalSystem), + entity, new ParameterizedTypeReference<>() { } ); @@ -435,13 +502,13 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration @Override public JenkinsBuildResponse getBuildDetails(ExternalSystem externalSystem, String jobName, Integer buildNumber) { - externalSystem = decryptSystem(externalSystem); try { // 构建 tree 参数,只获取我们需要的字段 String treeQuery = "number,url,result,duration,timestamp,building," + "changeSets[items[commitId,author,message]]," + "artifacts[fileName,relativePath,displayPath]"; - + + // 直接使用原始系统信息构建URL(URL不需要解密) String url = UriComponentsBuilder.fromHttpUrl(externalSystem.getUrl()) .path("/job/") .path(jobName) @@ -452,7 +519,9 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration .build() .toUriString(); - HttpEntity entity = new HttpEntity<>(createHeaders(externalSystem)); + // 创建请求头(自动包含认证和crumb) + HttpHeaders headers = createHeaders(externalSystem); + HttpEntity entity = new HttpEntity<>(headers); ResponseEntity response = restTemplate.exchange( url, HttpMethod.GET, @@ -463,22 +532,22 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) { // 打印原始响应用于调试 log.info("Jenkins build details raw response: {}", response.getBody()); - + ObjectMapper mapper = new ObjectMapper(); JenkinsBuildResponse buildResponse = mapper.readValue(response.getBody(), JenkinsBuildResponse.class); - + // 清理URL,移除基础URL部分 if (buildResponse.getUrl() != null) { String baseUrl = StringUtils.removeEnd(externalSystem.getUrl(), "/"); buildResponse.setUrl(buildResponse.getUrl().replace(baseUrl, "")); } - + return buildResponse; } - + throw new RuntimeException("Failed to get build details: empty response"); } catch (Exception e) { - log.error("Failed to get build details: job={}, buildNumber={}, error={}", + log.error("Failed to get build details: job={}, buildNumber={}, error={}", jobName, buildNumber, e.getMessage(), e); throw new RuntimeException("获取Jenkins构建详情失败: " + jobName + "#" + buildNumber, e); } @@ -563,10 +632,9 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration * @return 视图列表 */ public List listViews(ExternalSystem externalSystem) { - final ExternalSystem decryptedSystem = decryptSystem(externalSystem); String treeQuery = "views[name,url,description,_class]"; List views = callJenkinsApi( - decryptedSystem, + externalSystem, "/api/json", treeQuery, "views", @@ -574,7 +642,7 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration ); // 过滤和清洗数据 - final String baseUrl = StringUtils.removeEnd(decryptedSystem.getUrl(), "/"); + final String baseUrl = StringUtils.removeEnd(externalSystem.getUrl(), "/"); return views.stream() .filter(view -> !"all".equalsIgnoreCase(view.getName())) .peek(view -> { @@ -598,7 +666,6 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration * @return 任务列表 */ public List listJobs(ExternalSystem externalSystem, String viewName) { - externalSystem = decryptSystem(externalSystem); // 只查询必要的字段,确保包含lastBuild的timestamp字段 String treeQuery = "jobs[name,url,description,buildable,nextBuildNumber," + "lastBuild[number,timestamp,result],color]"; @@ -632,9 +699,8 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration @Override public JenkinsJobResponse job(ExternalSystem externalSystem, String jobName) { - externalSystem = decryptSystem(externalSystem); try { - // 1. 构建请求URL + // 直接使用原始系统信息构建URL(URL不需要解密) String url = UriComponentsBuilder.fromHttpUrl(externalSystem.getUrl()) .path("/job/") .path(jobName) @@ -681,7 +747,6 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration * @return 构建信息列表 */ public List listBuilds(ExternalSystem externalSystem, String jobName) { - externalSystem = decryptSystem(externalSystem); // Jenkins API 默认只返回最近的若干个构建,通过 {from,to} 可以指定范围 String treeQuery = "builds[number,url,result,timestamp,duration,building,actions[_class,parameters[_class,name,value]]]"; List builds = callJenkinsApi( @@ -703,18 +768,13 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration } @Override - public com.qqchen.deploy.backend.deploy.integration.response.JenkinsConsoleOutputResponse getConsoleOutput( - ExternalSystem externalSystem, - String jobName, - Integer buildNumber, - Long startOffset) { + public JenkinsConsoleOutputResponse getConsoleOutput(ExternalSystem externalSystem, String jobName, Integer buildNumber, Long startOffset) { try { if (startOffset == null) { startOffset = 0L; } - - // 构建 Jenkins Progressive Text API URL - // Jenkins API: /job/{jobName}/{buildNumber}/logText/progressiveText?start={offset} + + // 直接使用原始系统信息构建 Jenkins Progressive Text API URL(URL不需要解密) String url = UriComponentsBuilder.fromHttpUrl(externalSystem.getUrl()) .path("/job/") .path(jobName) @@ -724,23 +784,22 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration .queryParam("start", startOffset) .build() .toUriString(); - - // 发送请求 + + // 创建请求头(自动包含认证和crumb) HttpHeaders headers = createHeaders(externalSystem); headers.setAccept(Collections.singletonList(MediaType.TEXT_PLAIN)); HttpEntity entity = new HttpEntity<>(headers); - + ResponseEntity response = restTemplate.exchange( url, HttpMethod.GET, entity, String.class ); - + // 解析响应 - com.qqchen.deploy.backend.deploy.integration.response.JenkinsConsoleOutputResponse result = - new com.qqchen.deploy.backend.deploy.integration.response.JenkinsConsoleOutputResponse(); - + JenkinsConsoleOutputResponse result = new JenkinsConsoleOutputResponse(); + // 从响应体获取日志内容(按行分割) String logContent = response.getBody(); if (logContent != null && !logContent.isEmpty()) { @@ -749,30 +808,28 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration } else { result.setLines(Collections.emptyList()); } - + // 从响应头获取下一次读取的偏移量 // Jenkins 返回 X-Text-Size 表示当前日志总大小 // X-More-Data 表示是否还有更多数据 String textSize = response.getHeaders().getFirst("X-Text-Size"); String moreData = response.getHeaders().getFirst("X-More-Data"); - + result.setNextOffset(textSize != null ? Long.parseLong(textSize) : startOffset); result.setHasMoreData("true".equalsIgnoreCase(moreData)); result.setIsComplete(!"true".equalsIgnoreCase(moreData)); - - log.debug("Retrieved Jenkins console output: job={}, build={}, offset={}->{}, lines={}, hasMore={}", - jobName, buildNumber, startOffset, result.getNextOffset(), + + log.debug("Retrieved Jenkins console output: job={}, build={}, offset={}->{}, lines={}, hasMore={}", + jobName, buildNumber, startOffset, result.getNextOffset(), result.getLines().size(), result.getHasMoreData()); - + return result; - + } catch (Exception e) { - log.error("Failed to get Jenkins console output: job={}, build={}, offset={}, error={}", - jobName, buildNumber, startOffset, e.getMessage(), e); - + log.error("Failed to get Jenkins console output: job={}, build={}, offset={}, error={}", jobName, buildNumber, startOffset, e.getMessage(), e); + // 返回空结果而不是抛异常,避免中断轮询 - com.qqchen.deploy.backend.deploy.integration.response.JenkinsConsoleOutputResponse emptyResult = - new com.qqchen.deploy.backend.deploy.integration.response.JenkinsConsoleOutputResponse(); + JenkinsConsoleOutputResponse emptyResult = new JenkinsConsoleOutputResponse(); emptyResult.setLines(Collections.emptyList()); emptyResult.setNextOffset(startOffset); emptyResult.setHasMoreData(false); diff --git a/backend/src/main/java/com/qqchen/deploy/backend/workflow/delegate/JenkinsBuildDelegate.java b/backend/src/main/java/com/qqchen/deploy/backend/workflow/delegate/JenkinsBuildDelegate.java index 22dce399..28564063 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/workflow/delegate/JenkinsBuildDelegate.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/workflow/delegate/JenkinsBuildDelegate.java @@ -43,8 +43,6 @@ public class JenkinsBuildDelegate extends BaseNodeDelegate configs, NotificationInputMapping input) { @@ -42,21 +49,22 @@ public class NotificationNodeDelegate extends BaseNodeDelegate new RuntimeException("通知渠道不存在: " + input.getChannelId())); + // 2. 查询渠道和模板信息 + NotificationChannel channel = notificationChannelRepository.findById(input.getChannelId()).orElseThrow(() -> new RuntimeException("通知渠道不存在: " + input.getChannelId())); + NotificationTemplate template = notificationTemplateRepository.findById(input.getNotificationTemplateId()).orElseThrow(() -> new RuntimeException("通知模板不存在: " + input.getNotificationTemplateId())); // 3. 构建SendNotificationRequest SendNotificationRequest request = new SendNotificationRequest(); request.setNotificationTemplateId(input.getNotificationTemplateId()); request.setTemplateParams(execution.getVariables()); - // 4. 根据渠道类型创建基础的sendRequest(具体配置由NotificationService处理) + // 4. 根据渠道类型创建sendRequest,并从模板配置中获取参数 switch (channel.getChannelType()) { case WEWORK -> { WeworkSendNotificationRequest weworkRequest = new WeworkSendNotificationRequest(); weworkRequest.setChannelId(input.getChannelId()); - // 其他配置(消息类型等)由NotificationService根据模板配置自动设置 + // 从模板配置中获取消息类型 + weworkRequest.setMessageType(getWeworkMessageType(template)); request.setSendRequest(weworkRequest); } case EMAIL -> { @@ -81,6 +89,24 @@ public class NotificationNodeDelegate extends BaseNodeDelegate