package xiaozhi.modules.task.service; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Pattern; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import lombok.RequiredArgsConstructor; import xiaozhi.common.exception.ErrorCode; import xiaozhi.common.exception.RenException; import xiaozhi.modules.task.dao.TaskEventDao; import xiaozhi.modules.task.dao.TaskInstanceDao; import xiaozhi.modules.task.dao.TaskStepDao; import xiaozhi.modules.task.dto.TaskEventDTO; import xiaozhi.modules.task.dto.TaskInstanceListDTO; import xiaozhi.modules.task.dto.TaskInstanceListItemDTO; import xiaozhi.modules.task.dto.TaskInstanceUpsertDTO; import xiaozhi.modules.task.dto.TaskTurnDTO; import xiaozhi.modules.task.entity.TaskEventEntity; import xiaozhi.modules.task.entity.TaskInstanceEntity; import xiaozhi.modules.task.entity.TaskStepEntity; @Service @RequiredArgsConstructor public class TaskInstanceService { private static final Pattern LAYOUT_ID = Pattern.compile("^[A-Za-z0-9_-]{1,31}$"); private static final int LIST_MAX = 100; private final TaskInstanceDao instanceDao; private final TaskStepDao stepDao; private final TaskEventDao eventDao; @Transactional(rollbackFor = Exception.class) public void upsert(String id, TaskInstanceUpsertDTO body) { if (StringUtils.isBlank(id) || body == null) { throw new RenException(ErrorCode.PARAM_VALUE_NULL); } if (StringUtils.isBlank(body.getTaskSessionId())) { body.setTaskSessionId(id); } if (!id.equals(body.getTaskSessionId())) { throw new RenException("path id must match task_session_id"); } validateLayoutId(body.getAppId(), "app_id"); validateLayoutId(body.getTaskId(), "task_id"); String status = StringUtils.defaultIfBlank(body.getStatus(), "running"); if (!status.equals("running") && !status.equals("completed") && !status.equals("cancelled")) { throw new RenException("invalid status"); } try { persistInstance(id, body, status); } catch (RenException ex) { throw ex; } catch (Exception ex) { throw new RenException("TaskInstance 写入失败: " + ex.getMessage(), ex); } } private void persistInstance(String id, TaskInstanceUpsertDTO body, String status) { Map existingSteps = loadExistingSteps(id); TaskInstanceEntity existing = instanceDao.selectById(id); Date now = new Date(); TaskInstanceEntity entity = new TaskInstanceEntity(); entity.setId(id); entity.setMacAddress(StringUtils.left(StringUtils.trimToNull(body.getDeviceId()), 50)); entity.setAgentId(StringUtils.left(StringUtils.trimToNull(body.getAgentId()), 32)); entity.setWsSessionId(StringUtils.left(StringUtils.trimToEmpty(body.getWsSessionId()), 64)); entity.setAppId(body.getAppId()); entity.setTaskId(body.getTaskId()); entity.setSource(StringUtils.left(StringUtils.defaultIfBlank(body.getSource(), "voice"), 24)); entity.setInteractionMode(StringUtils.left(StringUtils.trimToEmpty(body.getInteractionMode()), 32)); entity.setStatus(status); entity.setEndReason(StringUtils.left(StringUtils.trimToEmpty(body.getEndReason()), 32)); entity.setStartedAt(body.getStartedAt() == null ? 0L : body.getStartedAt()); entity.setEndedAt(body.getEndedAt()); entity.setUpdatedAt(now); if (existing == null) { entity.setCreatedAt(now); instanceDao.insert(entity); } else { entity.setCreatedAt(existing.getCreatedAt()); instanceDao.updateById(entity); } stepDao.delete(new LambdaQueryWrapper().eq(TaskStepEntity::getInstanceId, id)); eventDao.delete(new LambdaQueryWrapper().eq(TaskEventEntity::getInstanceId, id)); List turns = body.getTurns() == null ? List.of() : body.getTurns(); for (TaskTurnDTO turn : turns) { if (turn == null || turn.getIndex() == null) { continue; } TaskStepEntity step = new TaskStepEntity(); step.setInstanceId(id); step.setStepIndex(turn.getIndex()); step.setPromptText(turn.getPromptText()); step.setAsrText(turn.getAsrText()); step.setEchoRejected(Boolean.TRUE.equals(turn.getEchoRejected()) ? 1 : 0); step.setRecordedAt(turn.getRecordedAt()); TaskStepEntity previous = existingSteps.get(turn.getIndex()); String audioId = StringUtils.trimToNull(turn.getAudioId()); if (audioId == null && previous != null) { audioId = previous.getAudioId(); } step.setAudioId(audioId); String expected = StringUtils.trimToNull(turn.getExpectedText()); if (expected == null && previous != null) { expected = previous.getExpectedText(); } step.setExpectedText(expected); if (turn.getMetrics() != null && !turn.getMetrics().isEmpty()) { step.setMetrics(turn.getMetrics()); step.setAssessedAt(turn.getAssessedAt()); } else if (previous != null) { step.setMetrics(previous.getMetrics()); step.setAssessedAt( turn.getAssessedAt() != null ? turn.getAssessedAt() : previous.getAssessedAt()); } else { step.setMetrics(null); step.setAssessedAt(turn.getAssessedAt()); } stepDao.insert(step); } List events = body.getEvents() == null ? List.of() : body.getEvents(); for (TaskEventDTO event : events) { if (event == null || StringUtils.isBlank(event.getName())) { continue; } TaskEventEntity row = new TaskEventEntity(); row.setInstanceId(id); row.setName(event.getName()); row.setAppId(StringUtils.defaultIfBlank(event.getAppId(), body.getAppId())); row.setTaskId(StringUtils.defaultIfBlank(event.getTaskId(), body.getTaskId())); row.setAtMs(event.getAt()); eventDao.insert(row); } } public TaskInstanceUpsertDTO getDetail(String id) { if (StringUtils.isBlank(id)) { return null; } TaskInstanceEntity entity = instanceDao.selectById(id); if (entity == null) { return null; } TaskInstanceUpsertDTO dto = new TaskInstanceUpsertDTO(); dto.setTaskSessionId(entity.getId()); dto.setWsSessionId(entity.getWsSessionId()); dto.setDeviceId(entity.getMacAddress()); dto.setAgentId(entity.getAgentId()); dto.setAppId(entity.getAppId()); dto.setTaskId(entity.getTaskId()); dto.setSource(entity.getSource()); dto.setInteractionMode(entity.getInteractionMode()); dto.setStartedAt(entity.getStartedAt()); dto.setEndedAt(entity.getEndedAt()); dto.setStatus(entity.getStatus()); dto.setEndReason(entity.getEndReason()); List steps = stepDao.selectList( new LambdaQueryWrapper().eq(TaskStepEntity::getInstanceId, id) .orderByAsc(TaskStepEntity::getStepIndex)); for (TaskStepEntity step : steps) { TaskTurnDTO turn = new TaskTurnDTO(); turn.setIndex(step.getStepIndex()); turn.setPromptText(step.getPromptText()); turn.setExpectedText(step.getExpectedText()); turn.setAsrText(step.getAsrText()); turn.setEchoRejected(step.getEchoRejected() != null && step.getEchoRejected() == 1); turn.setRecordedAt(step.getRecordedAt()); turn.setAudioId(step.getAudioId()); turn.setMetrics(step.getMetrics()); turn.setAssessedAt(step.getAssessedAt()); dto.getTurns().add(turn); } List events = eventDao.selectList( new LambdaQueryWrapper().eq(TaskEventEntity::getInstanceId, id) .orderByAsc(TaskEventEntity::getId)); for (TaskEventEntity event : events) { TaskEventDTO item = new TaskEventDTO(); item.setName(event.getName()); item.setAppId(event.getAppId()); item.setTaskId(event.getTaskId()); item.setAt(event.getAtMs()); dto.getEvents().add(item); } return dto; } public TaskInstanceListDTO listByMac(String macAddress, String appId, Integer limit) { if (StringUtils.isBlank(macAddress)) { throw new RenException(ErrorCode.PARAM_VALUE_NULL); } int size = limit == null ? 20 : Math.min(Math.max(limit, 1), LIST_MAX); LambdaQueryWrapper wrapper = new LambdaQueryWrapper() .eq(TaskInstanceEntity::getMacAddress, macAddress) .eq(StringUtils.isNotBlank(appId), TaskInstanceEntity::getAppId, appId) .orderByDesc(TaskInstanceEntity::getStartedAt) .last("LIMIT " + size); List rows = instanceDao.selectList(wrapper); Long total = instanceDao.selectCount(new LambdaQueryWrapper() .eq(TaskInstanceEntity::getMacAddress, macAddress) .eq(StringUtils.isNotBlank(appId), TaskInstanceEntity::getAppId, appId)); TaskInstanceListDTO result = new TaskInstanceListDTO(); result.setTotal(total == null ? 0 : total); for (TaskInstanceEntity row : rows) { TaskInstanceListItemDTO item = new TaskInstanceListItemDTO(); item.setId(row.getId()); item.setAppId(row.getAppId()); item.setTaskId(row.getTaskId()); item.setStatus(row.getStatus()); item.setEndReason(row.getEndReason()); item.setStartedAt(row.getStartedAt()); result.getList().add(item); } return result; } @Transactional(rollbackFor = Exception.class) public void attachAudio(String instanceId, Integer stepIndex, String audioId) { if (StringUtils.isBlank(instanceId) || stepIndex == null || StringUtils.isBlank(audioId)) { return; } stepDao.updateAudioId(instanceId, stepIndex, audioId); } private Map loadExistingSteps(String instanceId) { Map map = new HashMap<>(); List steps = stepDao.selectList( new LambdaQueryWrapper().eq(TaskStepEntity::getInstanceId, instanceId)); for (TaskStepEntity step : steps) { if (step.getStepIndex() != null) { map.put(step.getStepIndex(), step); } } return map; } private void validateLayoutId(String id, String field) { if (StringUtils.isBlank(id) || !LAYOUT_ID.matcher(id).matches()) { throw new RenException("invalid " + field); } } }