增加构建通知

This commit is contained in:
dengqichen 2025-11-14 11:18:00 +08:00
parent 123b9054da
commit 3645d8d062
4 changed files with 201 additions and 127 deletions

View File

@ -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);

View File

@ -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<Long, JenkinsCrumbCache> 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<String> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
return convertResponse(response);
}
@Override
public boolean testConnection(ExternalSystem system) {
// 解密系统信息
system = decryptSystem(system);
try {
// 直接使用原始系统信息构建URLURL不需要解密
String url = system.getUrl() + "/api/json";
// 创建请求头内部自动处理解密和crumb
HttpHeaders headers = createHeaders(system);
// 打印实际发送的请求头
log.info("Authorization头: {}", headers.getFirst("Authorization"));
log.info("===================================");
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<String> 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<String> 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<String> entity = new HttpEntity<>(headers);
return convertResponse(restTemplate.exchange(url, HttpMethod.GET, entity, String.class));
}
@Override
public String buildWithParameters(ExternalSystem externalSystem, String jobName, Map<String, String> parameters) {
externalSystem = decryptSystem(externalSystem);
try {
// 如果参数中包含PIPELINE_SCRIPT确保Job配置中有该参数
if (parameters.containsKey("PIPELINE_SCRIPT")) {
ensurePipelineScriptParameter(externalSystem, jobName);
}
// 1. 获取Crumb
JenkinsCrumbIssuerResponse jenkinsCrumbIssue = getJenkinsCrumbIssue(externalSystem);
// 2. 构建URL
// 直接使用原始系统信息构建URLURL不需要解密
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<String, String> 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配置URLURL不需要解密
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<String> entity = new HttpEntity<>(headers);
// 配置RestTemplate确保使用UTF-8编码
restTemplate.getMessageConverters().add(0, new StringHttpMessageConverter(Charset.forName("UTF-8")));
ResponseEntity<String> 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<String> 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);
// 直接使用原始系统信息构建URLURL不需要解密
String queueUrl = String.format("%s/queue/item/%s/api/json", externalSystem.getUrl().trim(), queueId);
ResponseEntity<Map<String, Object>> response = restTemplate.exchange(queueUrl, HttpMethod.GET, createHttpEntity(externalSystem), new ParameterizedTypeReference<>() {
// 创建请求头内部自动处理解密和crumb
HttpHeaders headers = createHeaders(externalSystem);
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<Map<String, Object>> response = restTemplate.exchange(queueUrl, HttpMethod.GET, entity, new ParameterizedTypeReference<>() {
});
Map<String, Object> 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 {
// 直接使用原始系统信息构建URLURL不需要解密
String url = String.format("%s/job/%s/%d/api/json", externalSystem.getUrl().trim(), jobName, buildNumber);
// 创建请求头内部自动处理解密和crumb
HttpHeaders headers = createHeaders(externalSystem);
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<Map<String, Object>> 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]";
// 直接使用原始系统信息构建URLURL不需要解密
String url = UriComponentsBuilder.fromHttpUrl(externalSystem.getUrl())
.path("/job/")
.path(jobName)
@ -452,7 +519,9 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration
.build()
.toUriString();
HttpEntity<String> entity = new HttpEntity<>(createHeaders(externalSystem));
// 创建请求头自动包含认证和crumb
HttpHeaders headers = createHeaders(externalSystem);
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<String> 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<JenkinsViewResponse> listViews(ExternalSystem externalSystem) {
final ExternalSystem decryptedSystem = decryptSystem(externalSystem);
String treeQuery = "views[name,url,description,_class]";
List<JenkinsViewResponse> 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<JenkinsJobResponse> 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
// 直接使用原始系统信息构建URLURL不需要解密
String url = UriComponentsBuilder.fromHttpUrl(externalSystem.getUrl())
.path("/job/")
.path(jobName)
@ -681,7 +747,6 @@ public class JenkinsServiceIntegrationImpl extends BaseExternalSystemIntegration
* @return 构建信息列表
*/
public List<JenkinsBuildResponse> 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<JenkinsBuildResponse> 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 URLURL不需要解密
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<String> entity = new HttpEntity<>(headers);
ResponseEntity<String> 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);

View File

@ -43,8 +43,6 @@ public class JenkinsBuildDelegate extends BaseNodeDelegate<JenkinsBuildInputMapp
@Resource
private IExternalSystemRepository externalSystemRepository;
@Resource
private IJenkinsJobRepository jenkinsJobRepository;
@Resource
private IWorkflowNodeLogService workflowNodeLogService;
@ -221,7 +219,7 @@ public class JenkinsBuildDelegate extends BaseNodeDelegate<JenkinsBuildInputMapp
throw new RuntimeException("Build status polling was interrupted", e);
}
}
throw new RuntimeException(String.format("Jenkins build timed out after %d minutes: job=%s, buildNumber=%d",
throw new RuntimeException(String.format("Jenkins build timed out after %d minutes: job=%s, buildNumber=%d",
MAX_BUILD_POLLS * BUILD_POLL_INTERVAL / 60, jobName, buildNumber));
}

View File

@ -1,11 +1,15 @@
package com.qqchen.deploy.backend.workflow.delegate;
import com.qqchen.deploy.backend.framework.utils.JsonUtils;
import com.qqchen.deploy.backend.notification.dto.EmailSendNotificationRequest;
import com.qqchen.deploy.backend.notification.dto.SendNotificationRequest;
import com.qqchen.deploy.backend.notification.dto.WeworkSendNotificationRequest;
import com.qqchen.deploy.backend.notification.entity.NotificationChannel;
import com.qqchen.deploy.backend.notification.entity.NotificationTemplate;
import com.qqchen.deploy.backend.notification.entity.config.WeworkTemplateConfig;
import com.qqchen.deploy.backend.notification.enums.WeworkMessageTypeEnum;
import com.qqchen.deploy.backend.notification.repository.INotificationChannelRepository;
import com.qqchen.deploy.backend.notification.repository.INotificationTemplateRepository;
import com.qqchen.deploy.backend.notification.service.INotificationService;
import com.qqchen.deploy.backend.workflow.dto.inputmapping.NotificationInputMapping;
import com.qqchen.deploy.backend.workflow.dto.outputs.NotificationOutputs;
@ -33,6 +37,9 @@ public class NotificationNodeDelegate extends BaseNodeDelegate<NotificationInput
@Resource
private INotificationChannelRepository notificationChannelRepository;
@Resource
private INotificationTemplateRepository notificationTemplateRepository;
@Override
protected void executeInternal(DelegateExecution execution, Map<String, Object> configs, NotificationInputMapping input) {
@ -42,21 +49,22 @@ public class NotificationNodeDelegate extends BaseNodeDelegate<NotificationInput
return;
}
try {
// 2. 查询渠道信息仅用于确定渠道类型
NotificationChannel channel = notificationChannelRepository.findById(input.getChannelId())
.orElseThrow(() -> 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<NotificationInput
}
}
/**
* 从模板配置中获取企业微信消息类型
*/
private WeworkMessageTypeEnum getWeworkMessageType(NotificationTemplate template) {
try {
if (template.getTemplateConfig() != null) {
WeworkTemplateConfig weworkConfig = JsonUtils.fromMap(template.getTemplateConfig(), WeworkTemplateConfig.class);
if (weworkConfig != null && weworkConfig.getMessageType() != null) {
return weworkConfig.getMessageType();
}
}
} catch (Exception e) {
log.warn("解析企业微信模板配置失败,使用默认消息类型: {}", e.getMessage());
}
// 默认使用TEXT类型
return WeworkMessageTypeEnum.TEXT;
}
/**
* 获取邮件收件人列表