From 9c8358c8ffc8927a219de58b440029e4c7d2789b Mon Sep 17 00:00:00 2001 From: zhangzq Date: Wed, 15 Apr 2026 13:04:00 +0800 Subject: [PATCH] =?UTF-8?q?add:=E5=A2=9E=E5=8A=A0tts=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nladmin-system/nlsso-server/pom.xml | 11 + .../gateway/controller/GateWayController.java | 1 + .../voice/config/VoiceBeansConfig.java | 14 + .../voice/controller/VoiceController.java | 48 ++++ .../voice/controller/VoiceTaskController.java | 67 +++++ .../gateway/voice/service/VoiceService.java | 128 +++++++++ .../voice/service/VoiceTaskService.java | 237 ++++++++++++++++ .../voice/service/dto/InboundTaskRequest.java | 26 ++ .../voice/service/dto/MoveTaskRequest.java | 22 ++ .../service/dto/OutboundTaskRequest.java | 22 ++ .../voice/service/dto/QueryBoundRequest.java | 19 ++ .../voice/websocket/VoiceWsEndpoint.java | 82 ++++++ .../service/dao/mapper/StructattrMapper.xml | 3 + .../service/util/tasks/StructMoveTask.java | 163 +++++++++++ .../main/resources/config/application-dev.yml | 2 +- .../src/main/resources/private_key.txt | 1 + nladmin-ui/src/api/voice.js | 18 ++ nladmin-ui/src/layout/components/Navbar.vue | 261 +++++++++++++++++- 18 files changed, 1123 insertions(+), 2 deletions(-) create mode 100644 nladmin-system/nlsso-server/src/main/java/org/nl/gateway/voice/config/VoiceBeansConfig.java create mode 100644 nladmin-system/nlsso-server/src/main/java/org/nl/gateway/voice/controller/VoiceController.java create mode 100644 nladmin-system/nlsso-server/src/main/java/org/nl/gateway/voice/controller/VoiceTaskController.java create mode 100644 nladmin-system/nlsso-server/src/main/java/org/nl/gateway/voice/service/VoiceService.java create mode 100644 nladmin-system/nlsso-server/src/main/java/org/nl/gateway/voice/service/VoiceTaskService.java create mode 100644 nladmin-system/nlsso-server/src/main/java/org/nl/gateway/voice/service/dto/InboundTaskRequest.java create mode 100644 nladmin-system/nlsso-server/src/main/java/org/nl/gateway/voice/service/dto/MoveTaskRequest.java create mode 100644 nladmin-system/nlsso-server/src/main/java/org/nl/gateway/voice/service/dto/OutboundTaskRequest.java create mode 100644 nladmin-system/nlsso-server/src/main/java/org/nl/gateway/voice/service/dto/QueryBoundRequest.java create mode 100644 nladmin-system/nlsso-server/src/main/java/org/nl/gateway/voice/websocket/VoiceWsEndpoint.java create mode 100644 nladmin-system/nlsso-server/src/main/java/org/nl/wms/sch_manage/service/util/tasks/StructMoveTask.java create mode 100644 nladmin-system/nlsso-server/src/main/resources/private_key.txt create mode 100644 nladmin-ui/src/api/voice.js diff --git a/nladmin-system/nlsso-server/pom.xml b/nladmin-system/nlsso-server/pom.xml index 6006c7b..35420c2 100644 --- a/nladmin-system/nlsso-server/pom.xml +++ b/nladmin-system/nlsso-server/pom.xml @@ -201,6 +201,17 @@ mysql-connector-java 8.0.20 + + + + + + + + + + + com.alibaba diff --git a/nladmin-system/nlsso-server/src/main/java/org/nl/gateway/controller/GateWayController.java b/nladmin-system/nlsso-server/src/main/java/org/nl/gateway/controller/GateWayController.java index 5b99619..e75cfd3 100644 --- a/nladmin-system/nlsso-server/src/main/java/org/nl/gateway/controller/GateWayController.java +++ b/nladmin-system/nlsso-server/src/main/java/org/nl/gateway/controller/GateWayController.java @@ -1,4 +1,5 @@ package org.nl.gateway.controller; + import cn.dev33.satoken.annotation.SaIgnore; import lombok.extern.slf4j.Slf4j; import org.nl.common.logging.annotation.Log; diff --git a/nladmin-system/nlsso-server/src/main/java/org/nl/gateway/voice/config/VoiceBeansConfig.java b/nladmin-system/nlsso-server/src/main/java/org/nl/gateway/voice/config/VoiceBeansConfig.java new file mode 100644 index 0000000..c9823fc --- /dev/null +++ b/nladmin-system/nlsso-server/src/main/java/org/nl/gateway/voice/config/VoiceBeansConfig.java @@ -0,0 +1,14 @@ +package org.nl.gateway.voice.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class VoiceBeansConfig { + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} diff --git a/nladmin-system/nlsso-server/src/main/java/org/nl/gateway/voice/controller/VoiceController.java b/nladmin-system/nlsso-server/src/main/java/org/nl/gateway/voice/controller/VoiceController.java new file mode 100644 index 0000000..a71c24f --- /dev/null +++ b/nladmin-system/nlsso-server/src/main/java/org/nl/gateway/voice/controller/VoiceController.java @@ -0,0 +1,48 @@ +package org.nl.gateway.voice.controller; + +import cn.dev33.satoken.annotation.SaIgnore; +import lombok.extern.slf4j.Slf4j; +import org.nl.common.base.TableDataInfo; +import org.nl.common.utils.MapOf; +import org.nl.gateway.voice.service.VoiceService; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import java.util.Map; + +/** + * 语音交互 HTTP 入口 + */ +@RestController +@RequestMapping("/api/wms/voice") +@Slf4j +public class VoiceController { + + @Resource + private VoiceService voiceService; + + /** + * 语音转文字:接收 multipart/form-data audio + */ + @PostMapping(value = "/asr", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @SaIgnore + public ResponseEntity asr(@RequestPart("audio") MultipartFile audio) throws Exception { + String text = voiceService.asrFromMultipart(audio); + return new ResponseEntity<>(TableDataInfo.buildJson(MapOf.of("text",text)), HttpStatus.OK); + } + + /** + * 文本确认后调度任务(或转发至 VC / WMS) + */ + @PostMapping("/confirm") + @SaIgnore + public ResponseEntity confirm(@RequestBody Map body) throws Exception { + String text = String.valueOf(body.getOrDefault("text", "")); + voiceService.dispatchText(text); + return new ResponseEntity<>(HttpStatus.OK); + } +} diff --git a/nladmin-system/nlsso-server/src/main/java/org/nl/gateway/voice/controller/VoiceTaskController.java b/nladmin-system/nlsso-server/src/main/java/org/nl/gateway/voice/controller/VoiceTaskController.java new file mode 100644 index 0000000..3ae3f78 --- /dev/null +++ b/nladmin-system/nlsso-server/src/main/java/org/nl/gateway/voice/controller/VoiceTaskController.java @@ -0,0 +1,67 @@ +package org.nl.gateway.voice.controller; + +import cn.dev33.satoken.annotation.SaIgnore; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import lombok.extern.slf4j.Slf4j; +import org.nl.common.exception.BadRequestException; +import org.nl.gateway.voice.service.VoiceTaskService; +import org.nl.gateway.voice.service.dto.InboundTaskRequest; +import org.nl.gateway.voice.service.dto.MoveTaskRequest; +import org.nl.gateway.voice.service.dto.OutboundTaskRequest; +import org.nl.gateway.voice.service.dto.QueryBoundRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.util.Map; + +/** + * 语音交互任务入口(占位,接入 VC 调用流程) + */ +@RestController +@RequestMapping("/api/wms/task") +@Slf4j +public class VoiceTaskController { + + @Autowired + private VoiceTaskService voiceTaskService; + /** + * 语音语义解析后异步调用的任务生成接口 + */ + @PostMapping("/generate") + @SaIgnore + public ResponseEntity generate(@RequestBody JSONObject voiceDto) { + System.out.println("接收任务" + JSON.toJSONString(voiceDto)); + + String commandType = voiceDto.getString("commandType"); + JSONObject result; + switch (commandType) { + case "taskIssue": + String taskType = voiceDto.getString("taskType"); + switch (taskType) { + case "inbound": + InboundTaskRequest inboundRequest = JSON.toJavaObject(voiceDto, InboundTaskRequest.class); + result = voiceTaskService.handleInboundTask(inboundRequest); + case "move": + MoveTaskRequest moveRequest = JSON.toJavaObject(voiceDto, MoveTaskRequest.class); + result = voiceTaskService.handleMoveTask(moveRequest); + case "outbound": + OutboundTaskRequest outboundRequest = JSON.toJavaObject(voiceDto, OutboundTaskRequest.class); + result = voiceTaskService.handleOutboundTask(outboundRequest); + default: + throw new BadRequestException("未知的指令类型: " + commandType); + + } + case "InventoryQuery": + QueryBoundRequest queryRequest = JSON.toJavaObject(voiceDto, QueryBoundRequest.class); + result = voiceTaskService.handleQueryTask(queryRequest); + default: + throw new BadRequestException("未知的指令类型: " + commandType); + } + } +} diff --git a/nladmin-system/nlsso-server/src/main/java/org/nl/gateway/voice/service/VoiceService.java b/nladmin-system/nlsso-server/src/main/java/org/nl/gateway/voice/service/VoiceService.java new file mode 100644 index 0000000..d9c6934 --- /dev/null +++ b/nladmin-system/nlsso-server/src/main/java/org/nl/gateway/voice/service/VoiceService.java @@ -0,0 +1,128 @@ +package org.nl.gateway.voice.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.util.Base64Utils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.web.multipart.MultipartFile; + +@Slf4j +@Service +@RequiredArgsConstructor +public class VoiceService { + + @Value("${vc.base-url:http://127.0.0.1:5000}") + private String vcBaseUrl; + + /** + * VC 调度接口(前端确认文字后调用)。默认使用 VC 文档的 /api/voice/dispatch 作为示例,可按需调整。 + */ + @Value("${vc.schedule-url:}") + private String vcScheduleUrl; + + private final RestTemplate restTemplate; + private final ObjectMapper mapper = new ObjectMapper(); + + public String asrFromBase64(String dataUrl) throws Exception { + byte[] audioBytes = decodeDataUrl(dataUrl); + return asrBytes(audioBytes); + } + + public String asrFromMultipart(MultipartFile file) throws Exception { + return asrBytes(file.getBytes()); + } + + private String asrBytes(byte[] audioBytes) throws Exception { + String url = vcBaseUrl + "/api/speech/asr"; + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.MULTIPART_FORM_DATA); + ByteArrayResource resource = new ByteArrayResource(audioBytes) { + @Override + public String getFilename() { + return "voice.webm"; + } + + @Override + public long contentLength() { + return audioBytes.length; + } + }; + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("audio", resource); + HttpEntity> requestEntity = new HttpEntity<>(body, headers); + ResponseEntity resp; + try { + resp = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class); + }catch (Exception ex){ + return "123456"; + } + if (!resp.getStatusCode().is2xxSuccessful()) { + return "123456"; +// throw new IllegalStateException("ASR请求失败:" + resp.getStatusCode()); + } + JsonNode root = mapper.readTree(resp.getBody()); + int code = root.path("code").asInt(-1); + if (code != 0) { + String msg = root.path("msg").asText("asr error"); + throw new IllegalStateException("ASR错误:" + msg); + } + String text = root.path("data").path("text").asText(""); + return text == null ? "" : text; + } + + public void dispatchText(String text) throws Exception { + String scheduleUrl = vcScheduleUrl == null || vcScheduleUrl.isEmpty() + ? vcBaseUrl + "/api/voice/dispatch" + : vcScheduleUrl; + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + String payload = mapper.writeValueAsString(mapper.createObjectNode().put("text", text)); + HttpEntity entity = new HttpEntity<>(payload, headers); + ResponseEntity resp = restTemplate.postForEntity(scheduleUrl, entity, String.class); + if (!resp.getStatusCode().is2xxSuccessful()) { + throw new IllegalStateException("调度请求失败:" + resp.getStatusCode()); + } + } + + public void speechTts(String text) throws Exception { + String scheduleUrl = vcScheduleUrl == null || vcScheduleUrl.isEmpty() + ? vcBaseUrl + "/api/speech/tts" + : vcScheduleUrl; + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + String payload = mapper.writeValueAsString(mapper.createObjectNode() + .put("text", text) + .put("vcn","wms") + .put("speed",50) + .put("volume",50) + .put("pitch",50) + ); + HttpEntity entity = new HttpEntity<>(payload, headers); + ResponseEntity resp = restTemplate.postForEntity(scheduleUrl, entity, String.class); + if (!resp.getStatusCode().is2xxSuccessful()) { + throw new IllegalStateException("调度请求失败:" + resp.getStatusCode()); + } + } + + private byte[] decodeDataUrl(String dataUrl) { + if (dataUrl == null) { + throw new IllegalArgumentException("音频为空"); + } + String base64; + int comma = dataUrl.indexOf(","); + base64 = comma > 0 ? dataUrl.substring(comma + 1) : dataUrl; + return Base64Utils.decodeFromString(base64); + } +} diff --git a/nladmin-system/nlsso-server/src/main/java/org/nl/gateway/voice/service/VoiceTaskService.java b/nladmin-system/nlsso-server/src/main/java/org/nl/gateway/voice/service/VoiceTaskService.java new file mode 100644 index 0000000..29d23af --- /dev/null +++ b/nladmin-system/nlsso-server/src/main/java/org/nl/gateway/voice/service/VoiceTaskService.java @@ -0,0 +1,237 @@ +package org.nl.gateway.voice.service; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.nl.common.exception.BadRequestException; +import org.nl.common.utils.CodeUtil; +import org.nl.common.utils.IdUtil; +import org.nl.common.utils.MapOf; +import org.nl.common.utils.SecurityUtils; +import org.nl.config.SpringContextHolder; +import org.nl.gateway.voice.service.dto.InboundTaskRequest; +import org.nl.gateway.voice.service.dto.MoveTaskRequest; +import org.nl.gateway.voice.service.dto.OutboundTaskRequest; +import org.nl.gateway.voice.service.dto.QueryBoundRequest; +import org.nl.wms.basedata_manage.enums.BaseDataEnum; +import org.nl.wms.basedata_manage.service.IMdMeMaterialbaseService; +import org.nl.wms.basedata_manage.service.IStructattrService; +import org.nl.wms.basedata_manage.service.dao.*; +import org.nl.wms.basedata_manage.service.dto.StrategyMater; +import org.nl.wms.basedata_manage.service.dto.StrategyStructMaterialVO; +import org.nl.wms.basedata_manage.service.dto.StrategyStructParam; +import org.nl.wms.pda_manage.ios_manage.service.PdaIosInService; +import org.nl.wms.pda_manage.ios_manage.service.PdaIosOutService; +import org.nl.wms.sch_manage.enums.StatusEnum; +import org.nl.wms.sch_manage.enums.TaskEnum; +import org.nl.wms.sch_manage.service.ISchBasePointService; +import org.nl.wms.sch_manage.service.dao.SchBasePoint; +import org.nl.wms.sch_manage.service.util.AbstractTask; +import org.nl.wms.sch_manage.service.util.tasks.StInTask; +import org.nl.wms.sch_manage.service.util.tasks.VehicleOutTask; +import org.nl.wms.warehouse_manage.enums.IOSConstant; +import org.nl.wms.warehouse_manage.enums.IOSEnum; +import org.nl.wms.warehouse_manage.service.IMdPbGroupplateService; +import org.nl.wms.warehouse_manage.service.IStIvtMoveinvService; +import org.nl.wms.warehouse_manage.service.dao.GroupPlate; +import org.nl.wms.warehouse_manage.service.dto.MoveInsertDto; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Base64Utils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.nl.wms.warehouse_manage.enums.IOSEnum.GROUP_PLATE_STATUS; + +@Slf4j +@Service +public class VoiceTaskService { + + @Autowired + private IStructattrService iStructattrService; + @Resource + private Map applyTaskMap; + @Autowired + private IMdMeMaterialbaseService iMdMeMaterialbaseService; + @Autowired + private ISchBasePointService iSchBasePointService; + @Autowired + private IMdPbGroupplateService iMdPbGroupplateService; + @Autowired + private VoiceService voiceService; + + + @Transactional + public JSONObject handleInboundTask(InboundTaskRequest inboundTaskRequest){ + SchBasePoint one = iSchBasePointService.getOne(new LambdaQueryWrapper() + .eq(SchBasePoint::getPoint_code, inboundTaskRequest.getLocationFrom())); + //分配仓位 + MdMeMaterialbase meMaterialbase = iMdMeMaterialbaseService.getByCode(inboundTaskRequest.getMaterial()); + + GroupPlate groupDao = GroupPlate.builder() + .group_id(IdUtil.getStringId()) + .material_id(meMaterialbase.getMaterial_code()) + .storagevehicle_code(one.getVehicle_code()) + .pcsn("111") + .qty_unit_id(meMaterialbase.getQty_unit_id()) + .qty_unit_name(meMaterialbase.getQty_unit_name()) + .qty(new BigDecimal(inboundTaskRequest.getQty())) + .status(GROUP_PLATE_STATUS.code("组盘")) + .create_id(SecurityUtils.getCurrentUserId()) + .create_name(SecurityUtils.getCurrentNickName()) + .create_time(DateUtil.now()) + .build(); + iMdPbGroupplateService.save(groupDao); + StrategyMater mater = new StrategyMater(); + mater.setMaterial_code(inboundTaskRequest.getMaterial()); + mater.setMaterial_id(meMaterialbase.getMaterial_id()); + mater.setQty(new BigDecimal(inboundTaskRequest.getQty())); + mater.setPcsn("111"); + List materss = new ArrayList<>(); + materss.add(mater); + List structattrs = iStructattrService.inBoundSectDiv( + StrategyStructParam.builder() + .ioType(StatusEnum.STRATEGY_TYPE.code("入库")) + .sect_code("BG01") + .stor_code("BGK") + .storagevehicle_code(one.getVehicle_code()) + .strategyMaters(materss) + .build()); + if (CollectionUtils.isEmpty(structattrs)) { + throw new BadRequestException("无可用货位"); + } + Structattr attrDao = structattrs.get(0); + //确定起点 + SchBasePoint schBasePoint = iSchBasePointService.getOne(new LambdaQueryWrapper() + .eq(SchBasePoint::getPoint_code, inboundTaskRequest.getLocationFrom())); + if (ObjectUtil.isEmpty(schBasePoint)) { + throw new BadRequestException("未找到载具所在的点位信息,请检查"); + } + JSONObject whereJson = new JSONObject(); + whereJson.put("point_code1", schBasePoint.getPoint_code()); + //确定终点 + whereJson.put("point_code2", attrDao.getStruct_code()); + //创建任务 + String taskId = applyTaskMap.get("VehicleInTask").create(whereJson); + // 更新起点绑定id + iSchBasePointService.update( + new UpdateWrapper().lambda() + .set(SchBasePoint::getIos_id, null) + .eq(SchBasePoint::getPoint_code, schBasePoint.getPoint_code()) + ); + // 更新终点锁定状态 + JSONObject lock_map = new JSONObject(); + lock_map.put("struct_code", attrDao.getStruct_code()); + lock_map.put("inv_id", null); + lock_map.put("inv_code", null); + lock_map.put("inv_type", null); + lock_map.put("taskdtl_id", taskId); + lock_map.put("lock_type", IOSEnum.LOCK_TYPE.code("入库锁")); + iStructattrService.updateStatusByCode("0", lock_map); + return null; + }; + @Transactional + public JSONObject handleOutboundTask(OutboundTaskRequest outboundTaskRequest){ + MdMeMaterialbase meMaterialbase = iMdMeMaterialbaseService.getByCode(outboundTaskRequest.getMaterial()); + JSONObject whereJson = new JSONObject(); + whereJson.put("material_id", meMaterialbase.getMaterial_id()); + whereJson.put("material_code", meMaterialbase.getMaterial_code()); + whereJson.put("stor_code", "BGK"); + whereJson.put("sect_code", "BG01"); + StrategyMater mater = new StrategyMater(); + mater.setQty(BigDecimal.valueOf(outboundTaskRequest.getQty())); + mater.setMaterial_id(meMaterialbase.getMaterial_id()); + mater.setMaterial_code(meMaterialbase.getMaterial_code()); + List maters = new ArrayList<>(); + maters.add(mater); + StrategyStructParam strategyStructParam = StrategyStructParam.builder() + .ioType(whereJson.getString(StatusEnum.STRATEGY_TYPE.code("出库"))) + .sect_code(whereJson.getString("sect_code")) + .stor_code(whereJson.getString("stor_code")) + .strategyMaters(maters) + .build(); + List structList = iStructattrService.outBoundSectDiv(strategyStructParam); + if (CollectionUtils.isEmpty(structList)) { + throw new BadRequestException("无可用库存!"); + } + structList.forEach(r -> { + //创建任务 + JSONObject taskForm = new JSONObject(); + taskForm.put("task_type", IOSConstant.VEHICLE_OUT_TASK); + taskForm.put("config_code", IOSConstant.VEHICLE_IN_TASK); + taskForm.put("TaskCode", CodeUtil.getNewCode("TASK_CODE")); + taskForm.put("PickingLocation", r.getStruct_code()); + taskForm.put("PlacedLocation", whereJson.getString("siteCode")); + taskForm.put("vehicle_code", r.getStoragevehicle_code()); + applyTaskMap.get(IOSConstant.VEHICLE_OUT_TASK).create(whereJson); + }); + //更新组盘记录表 + Set vehicleCodeSet = structList.stream() + .map(StrategyStructMaterialVO::getStoragevehicle_code) + .collect(Collectors.toSet()); + iMdPbGroupplateService.update( + new GroupPlate(), + new LambdaUpdateWrapper() + .set(GroupPlate::getStatus, IOSEnum.GROUP_PLATE_STATUS.code("出库")) + .in(GroupPlate::getStoragevehicle_code, vehicleCodeSet) + ); + //锁定仓位 + Set structCodeSet = structList.stream() + .map(StrategyStructMaterialVO::getStruct_code) + .collect(Collectors.toSet()); + iStructattrService.update( + new LambdaUpdateWrapper() + .set(Structattr::getInv_id, null) + .set(Structattr::getInv_code, null) + .set(Structattr::getInv_type, null) + .set(Structattr::getLock_type, IOSEnum.LOCK_TYPE.code("出库锁")) + .in(Structattr::getStruct_code, structCodeSet) + ); + return null; + }; + public JSONObject handleMoveTask(MoveTaskRequest moveTaskRequest){ + String taskId = applyTaskMap.get("VehicleInTask").create((JSONObject) JSONObject.toJSON(moveTaskRequest)); + return null; + }; + + public JSONObject handleQueryTask(QueryBoundRequest queryBoundRequest){ + List structCode = iStructattrService.collectVechicle(MapOf.of("struct_code", queryBoundRequest.getLocation(), "search", queryBoundRequest.getMaterial())); + StringBuffer stringBuffer = new StringBuffer(); + stringBuffer.append("当前仓库库位") + .append(queryBoundRequest.getLocation()).append("有物料"); + for (StructattrVechielDto vechielDtos : structCode) { + stringBuffer.append(vechielDtos.getMaterial_name()).append(vechielDtos.getQty()) + .append(vechielDtos.getQty_unit_name()); + } + String text = stringBuffer.toString(); + try { + voiceService.speechTts(text); + }catch (Exception ex){ + ex.printStackTrace(); + } + return null; + }; +} diff --git a/nladmin-system/nlsso-server/src/main/java/org/nl/gateway/voice/service/dto/InboundTaskRequest.java b/nladmin-system/nlsso-server/src/main/java/org/nl/gateway/voice/service/dto/InboundTaskRequest.java new file mode 100644 index 0000000..653d41f --- /dev/null +++ b/nladmin-system/nlsso-server/src/main/java/org/nl/gateway/voice/service/dto/InboundTaskRequest.java @@ -0,0 +1,26 @@ +package org.nl.gateway.voice.service.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class InboundTaskRequest { + + @JsonProperty("commandType") + private String commandType; // 指令类型:taskIssue + + @JsonProperty("deviceId") + private String deviceId; // 语音设备ID + + @JsonProperty("taskType") + private String taskType; // 任务类型:inbound + + @JsonProperty("locationFrom") + private String locationFrom; // 取货点编号:从哪个位置入库 + + @JsonProperty("material") + private String material; // 物料编码 + + @JsonProperty("qty") + private Integer qty; // 入库物料数量 +} diff --git a/nladmin-system/nlsso-server/src/main/java/org/nl/gateway/voice/service/dto/MoveTaskRequest.java b/nladmin-system/nlsso-server/src/main/java/org/nl/gateway/voice/service/dto/MoveTaskRequest.java new file mode 100644 index 0000000..e12e498 --- /dev/null +++ b/nladmin-system/nlsso-server/src/main/java/org/nl/gateway/voice/service/dto/MoveTaskRequest.java @@ -0,0 +1,22 @@ +package org.nl.gateway.voice.service.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class MoveTaskRequest { + @JsonProperty("commandType") + private String commandType; // 指令类型:taskIssue + + @JsonProperty("deviceId") + private String deviceId; // 语音设备ID + + @JsonProperty("taskType") + private String taskType; // 任务类型:move + + @JsonProperty("locationFrom") + private String locationFrom; // 取货点编号:从哪个库位取货 + + @JsonProperty("locationTo") + private String locationTo; // 放货点编号:移到哪个库位 +} diff --git a/nladmin-system/nlsso-server/src/main/java/org/nl/gateway/voice/service/dto/OutboundTaskRequest.java b/nladmin-system/nlsso-server/src/main/java/org/nl/gateway/voice/service/dto/OutboundTaskRequest.java new file mode 100644 index 0000000..1df0031 --- /dev/null +++ b/nladmin-system/nlsso-server/src/main/java/org/nl/gateway/voice/service/dto/OutboundTaskRequest.java @@ -0,0 +1,22 @@ +package org.nl.gateway.voice.service.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class OutboundTaskRequest { + @JsonProperty("commandType") + private String commandType; // 指令类型:taskIssue + + @JsonProperty("deviceId") + private String deviceId; // 语音设备ID + + @JsonProperty("taskType") + private String taskType; // 任务类型:outbound + + @JsonProperty("material") + private String material; // 物料编码:需要出什么料 + + @JsonProperty("qty") + private Integer qty; // 出库数量 +} diff --git a/nladmin-system/nlsso-server/src/main/java/org/nl/gateway/voice/service/dto/QueryBoundRequest.java b/nladmin-system/nlsso-server/src/main/java/org/nl/gateway/voice/service/dto/QueryBoundRequest.java new file mode 100644 index 0000000..d6fbbe7 --- /dev/null +++ b/nladmin-system/nlsso-server/src/main/java/org/nl/gateway/voice/service/dto/QueryBoundRequest.java @@ -0,0 +1,19 @@ +package org.nl.gateway.voice.service.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class QueryBoundRequest { + @JsonProperty("commandType") + private String commandType; // 指令类型:InventoryQuery + + @JsonProperty("deviceId") + private String deviceId; // 语音设备ID + + @JsonProperty("location") + private String location; // 货位编号:基于货位查看库存 + + @JsonProperty("material") + private String material; // 物料编码:基于物料查询库存 +} diff --git a/nladmin-system/nlsso-server/src/main/java/org/nl/gateway/voice/websocket/VoiceWsEndpoint.java b/nladmin-system/nlsso-server/src/main/java/org/nl/gateway/voice/websocket/VoiceWsEndpoint.java new file mode 100644 index 0000000..58dd5e0 --- /dev/null +++ b/nladmin-system/nlsso-server/src/main/java/org/nl/gateway/voice/websocket/VoiceWsEndpoint.java @@ -0,0 +1,82 @@ +package org.nl.gateway.voice.websocket; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.nl.gateway.voice.service.VoiceService; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import javax.websocket.*; +import javax.websocket.server.ServerEndpoint; +import java.io.IOException; +import java.util.concurrent.CopyOnWriteArraySet; + +@Slf4j +@Component +@ServerEndpoint("/voice/ws") +public class VoiceWsEndpoint { + + private static final CopyOnWriteArraySet clients = new CopyOnWriteArraySet<>(); + private Session session; + private static VoiceService staticVoiceService; + private static final ObjectMapper mapper = new ObjectMapper(); + + @Resource + public void setVoiceService(VoiceService voiceService) { + VoiceWsEndpoint.staticVoiceService = voiceService; + } + + @OnOpen + public void onOpen(Session session) { + System.out.println("---onOpen----"); + + this.session = session; + clients.add(this); + sendText("{\"message\":\"语音服务已连接\"}"); + } + + @OnClose + public void onClose() { + System.out.println("---onClose----"); + clients.remove(this); + } + + @OnError + public void onError(Session session, Throwable thr) { + log.error("ws error", thr); + } + + @OnMessage + public void onMessage(String message, Session session) { + System.out.println("---onMessage----"); + try { + JsonNode root = mapper.readTree(message); + String type = root.path("type").asText(""); + if ("audio".equalsIgnoreCase(type)) { + String payload = root.path("payload").asText(""); + String text = staticVoiceService.asrFromBase64(payload); + sendText(mapper.createObjectNode().put("text", text).toString()); + } else if ("confirm".equalsIgnoreCase(type)) { + String text = root.path("text").asText(""); + staticVoiceService.dispatchText(text); + sendText(mapper.createObjectNode().put("message", "已提交任务").toString()); + } else { + sendText(mapper.createObjectNode().put("message", "未知类型").toString()); + } + } catch (Exception e) { + log.error("处理消息失败", e); + sendText(mapper.createObjectNode().put("message", "处理失败:" + e.getMessage()).toString()); + } + } + + private void sendText(String text) { + if (this.session != null && this.session.isOpen()) { + try { + this.session.getBasicRemote().sendText(text); + } catch (IOException e) { + log.error("ws send error", e); + } + } + } +} diff --git a/nladmin-system/nlsso-server/src/main/java/org/nl/wms/basedata_manage/service/dao/mapper/StructattrMapper.xml b/nladmin-system/nlsso-server/src/main/java/org/nl/wms/basedata_manage/service/dao/mapper/StructattrMapper.xml index 6bd6d15..f201d82 100644 --- a/nladmin-system/nlsso-server/src/main/java/org/nl/wms/basedata_manage/service/dao/mapper/StructattrMapper.xml +++ b/nladmin-system/nlsso-server/src/main/java/org/nl/wms/basedata_manage/service/dao/mapper/StructattrMapper.xml @@ -35,6 +35,9 @@ AND gro.pcsn = #{pcsn} + + AND ivt.struct_code = #{struct_code} + AND ivt.stor_code = #{stor_code} diff --git a/nladmin-system/nlsso-server/src/main/java/org/nl/wms/sch_manage/service/util/tasks/StructMoveTask.java b/nladmin-system/nlsso-server/src/main/java/org/nl/wms/sch_manage/service/util/tasks/StructMoveTask.java new file mode 100644 index 0000000..2454bb5 --- /dev/null +++ b/nladmin-system/nlsso-server/src/main/java/org/nl/wms/sch_manage/service/util/tasks/StructMoveTask.java @@ -0,0 +1,163 @@ +package org.nl.wms.sch_manage.service.util.tasks; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.ObjectUtil; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import org.nl.common.exception.BadRequestException; +import org.nl.common.utils.CodeUtil; +import org.nl.common.utils.IdUtil; +import org.nl.common.utils.SecurityUtils; +import org.nl.gateway.voice.service.dto.MoveTaskRequest; +import org.nl.wms.basedata_manage.enums.BaseDataEnum; +import org.nl.wms.basedata_manage.service.IStructattrService; +import org.nl.wms.basedata_manage.service.dao.Structattr; +import org.nl.wms.sch_manage.enums.TaskStatus; +import org.nl.wms.sch_manage.service.ISchBaseTaskService; +import org.nl.wms.sch_manage.service.dao.SchBaseTask; +import org.nl.wms.sch_manage.service.util.AbstractTask; +import org.nl.wms.sch_manage.service.util.AcsTaskDto; +import org.nl.wms.sch_manage.service.util.TaskType; +import org.nl.wms.warehouse_manage.enums.IOSEnum; +import org.nl.wms.warehouse_manage.inAndOut.service.IOutBillService; +import org.nl.wms.warehouse_manage.inAndOut.service.dao.IOStorInvDis; +import org.nl.wms.warehouse_manage.inAndOut.service.dao.mapper.IOStorInvDisMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; + +; + +/** + * @Author: gbx + * @Description: 空载具出库任务 + * @Date: 2025/7/3 + */ +@Component(value = "StructMoveTask") +@TaskType("StructMoveTask") +public class StructMoveTask extends AbstractTask { + @Autowired + private ISchBaseTaskService taskService; + @Autowired + private IStructattrService iStructattrService; + + @Override + public String create(JSONObject json) { + MoveTaskRequest moveTaskRequest = JSONObject.toJavaObject(json, MoveTaskRequest.class); + final Structattr structattr = iStructattrService.getByCode(moveTaskRequest.getLocationFrom()); + SchBaseTask task = new SchBaseTask(); + task.setTask_id(IdUtil.getStringId()); + task.setTask_code(CodeUtil.getNewCode("TASK_CODE")); + task.setTask_status(TaskStatus.CREATE.getCode()); + task.setConfig_code(json.getString("task_type")); + task.setPoint_code1(moveTaskRequest.getLocationFrom()); + task.setPoint_code2(moveTaskRequest.getLocationTo()); + task.setVehicle_code(structattr.getStoragevehicle_code()); + task.setRequest_param(json.toString()); + task.setPriority(json.getString("Priority")); + task.setCreate_id(SecurityUtils.getCurrentUserId()); + task.setCreate_name(SecurityUtils.getCurrentNickName()); + task.setCreate_time(DateUtil.now()); + taskService.save(task); + return task.getTask_id(); + } + + @Override + public AcsTaskDto sendAcsParam(String taskId) { + SchBaseTask taskDao = taskService.getById(taskId); + // 组织下发给acs的数据 + AcsTaskDto acsTaskDto = new AcsTaskDto(); + acsTaskDto.setExt_task_uuid(taskDao.getTask_id()); + acsTaskDto.setTask_code(taskDao.getTask_code()); + acsTaskDto.setStart_device_code(taskDao.getPoint_code1()); + acsTaskDto.setNext_device_code(taskDao.getPoint_code2()); + if (taskDao.getPoint_code2().contains("-")) { + acsTaskDto.setNext_device_code(taskDao.getPoint_code2().replace('-', '_')); + } + acsTaskDto.setPriority(taskDao.getPriority()); + acsTaskDto.setTask_type("1"); + + return acsTaskDto; + } + + @Override + protected void updateStatus(String task_code, TaskStatus status) { + // 校验任务 + SchBaseTask taskObj = taskService.getByCode(task_code); + if (taskObj.getTask_status().equals(TaskStatus.FINISHED.getCode())) { + throw new BadRequestException("该任务已完成!"); + } + if (taskObj.getTask_status().equals(TaskStatus.CANCELED.getCode())) { + throw new BadRequestException("该任务已取消!"); + } + // 根据传来的类型去对任务进行操作 + if (status.equals(TaskStatus.EXECUTING)) { + taskObj.setTask_status(TaskStatus.EXECUTING.getCode()); + taskObj.setRemark("执行中"); + taskService.updateById(taskObj); + } + if (status.equals(TaskStatus.FINISHED)) { + this.finishTask(taskObj); + } + if (status.equals(TaskStatus.CANCELED)) { + this.cancelTask(taskObj); + } + } + + @Override + public void forceFinish(String task_code) { + SchBaseTask taskObj = taskService.getByCode(task_code); + if (ObjectUtil.isEmpty(taskObj)) { + throw new BadRequestException("该任务不存在"); + } + this.finishTask(taskObj); + } + + @Override + public void cancel(String task_code) { + SchBaseTask taskObj = taskService.getByCode(task_code); + if (ObjectUtil.isEmpty(taskObj)) { + throw new BadRequestException("该任务不存在"); + } + if (!TaskStatus.CREATE.getCode().equals(taskObj.getTask_status())) { + throw new BadRequestException("任务状态必须为生成才能取消任务"); + } + this.cancelTask(taskObj); + } + + @Override + public void backMes(String task_code) { + } + + @Transactional(rollbackFor = Exception.class) + public void finishTask(SchBaseTask taskObj) { + // 任务完成 + iStructattrService.update(new LambdaUpdateWrapper() + .set(Structattr::getStoragevehicle_code,null) + .eq(Structattr::getStruct_code,taskObj.getPoint_code1())); + iStructattrService.update(new LambdaUpdateWrapper() + .set(Structattr::getStoragevehicle_code,taskObj.getVehicle_code()) + .eq(Structattr::getStruct_code,taskObj.getPoint_code2())); + taskObj.setTask_status(TaskStatus.FINISHED.getCode()); + taskObj.setRemark("已完成"); + taskService.updateById(taskObj); + + } + + @Transactional(rollbackFor = Exception.class) + public void cancelTask(SchBaseTask taskObj) { + // 取消任务 + taskService.update(new LambdaUpdateWrapper() + .set(SchBaseTask::getIs_delete, BaseDataEnum.IS_YES_NOT.code("是")) + .set(SchBaseTask::getTask_status, TaskStatus.CANCELED.getCode()) + .set(SchBaseTask::getRemark,"已取消") + .eq(SchBaseTask::getTask_id,taskObj.getTask_id()) + ); + // 更新任务状态 + taskObj.setTask_status(TaskStatus.CANCELED.getCode()); + taskObj.setRemark("已取消"); + taskService.updateById(taskObj); + } +} diff --git a/nladmin-system/nlsso-server/src/main/resources/config/application-dev.yml b/nladmin-system/nlsso-server/src/main/resources/config/application-dev.yml index c1387f7..3f2a7a3 100644 --- a/nladmin-system/nlsso-server/src/main/resources/config/application-dev.yml +++ b/nladmin-system/nlsso-server/src/main/resources/config/application-dev.yml @@ -9,7 +9,7 @@ spring: druid: db-type: com.alibaba.druid.pool.DruidDataSource driverClassName: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://${DB_HOST:192.168.81.251}:${DB_PORT:3306}/${DB_NAME:wms_standardv2}?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false&useOldAliasMetadataBehavior=true&allowPublicKeyRetrieval=true&useSSL=false + url: jdbc:mysql://${DB_HOST:192.168.81.251}:${DB_PORT:3306}/${DB_NAME:hwall_wms}?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false&useOldAliasMetadataBehavior=true&allowPublicKeyRetrieval=true&useSSL=false # url: jdbc:mysql://${DB_HOST:localhost}:${DB_PORT:3306}/${DB_NAME:wms_oulun}?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false&useOldAliasMetadataBehavior=true&allowPublicKeyRetrieval=true&useSSL=false username: ${DB_USER:root} password: ${DB_PWD:P@ssw0rd.} diff --git a/nladmin-system/nlsso-server/src/main/resources/private_key.txt b/nladmin-system/nlsso-server/src/main/resources/private_key.txt new file mode 100644 index 0000000..7ae8272 --- /dev/null +++ b/nladmin-system/nlsso-server/src/main/resources/private_key.txt @@ -0,0 +1 @@ +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCBbWQ38mZdmOX379myX/NFn/qFIeP3kbogDiWlGtc1JNt6eDSsOEShUNj3o8Jo5Qaepyo6j4stP4WpmCAUFsdyOodzU0R60P7gFOR1OIdKyyQ2OS9J1MdNXRRuksfD1WVG+azoB+huQo2D52bcXSjnu1UDRDrXN3XXZgh1L2V/aDg+Gi9QAIsMDHtN62zKsHs4tlClHt0KORSdAxN9RjPzUFNYXfxW3dNTM9zfltoM2bgeUfG61F5EMipkAEVjDb4+Pu2BsNUamjy85eKDWA8NxDU6uuDkxLNiLx5KipLxOR+EM4/cOqRwHdEj8matpGlqBSOfOxXd6Sh5XmVStBjtAgMBAAECggEAQCbcme6IVrRGqJI2MXfluQkGv56AxGFzBBh/CEs5iJnwP8/9K6/oNJ1CLdz5q8x5b4IkKEqmDZOCyQEiRVLVIQVpxfvr4YReEOvKIWAXjzcJh+boTYwuDWapjfUrFyJaxMdUsN3ak2xhgJPeJDP45oOwK6JSGALhYhas8oi/olptl3leZs/5Z3h9UE69u80XRdhjtGyfS3AOOtT6dVcfKw6H8tmoKmx43ZfPvoV+a7hcwHO587mI1epAhYGOn81e5QoNBegiCEv9KutuZtauJuGHKcsvNh/FK8QujRJ1TFxOsMtxsJWZfxQxUuvJ0PulCpGpmkuHFNGDmV3ukJO1AQKBgQC8eiTaWgq8eCrIOi5fYtXQUmzv2e5BOhMrRyUWoB30N7GmKcdNGT5HJVXztidcBj53cNd8T6t5yTwYFrdZ5Lll7ItPAub25CSnGQU2nmceHK+46PNlQfLZRrlyeUuGYJTHVZanV+6Pneqn+6XifTa969HzpejpiJuG8iYVmcztfQKBgQCvy5ha6tBS+sIrjXL8/lrxXMDm4xT3CnCLmBqInppLwfFOgcQFzYWL6SQSJ7k3uC+xFT++VgsRLz/pQrVLsQzkY6mUF8sI7F0kevy/jAFzl9cgFn9BXu1ATyWloQIAX/UdSbzSWxIH3BW3BNOWZ0x91HUqBDAFzyLBkIns8LZ0MQKBgQCyg9oN+kS69/JFjV3IuLsdQkSt9LNGknP/hLYrNOLKIkofwOhlLOigyEsdt0SWU8+sn3Np6afXhPNnOXTWLt4vHJlh77TE2ZehsQAQGH5Athj1waZvHMSgaO1S8HHJSAcCuh0kSRPKcV8FVkNrPv+vaQGFjXoKX3o3mXja8r53nQKBgQCElQVj1GKnoo1csYJ+wgqurCikObFvG8WD0oR4cz2lUzD956qCQd2thnj45FKxbk0xvffkQhp4rG0ELJZ07qPtgCi+Ey/CnBknUUZb5GiX2HWbsrvo/oHqlYasIwFSbQx9OUaaU6sGmHscHBzD+0ZaRCjVNnFNgEoTOEJ9m5HPkQKBgQC0Kd29rQMIm5wXhIyW+bVdwmEyB/Xuq6Ch7lVVfZ6WMSoDbQZdYH3Mxw+yzjYpcS8jf/7x7mYH9Z0ggXwX7CAcRqhpjtKU800KzwQ2Cnd7Jmgq56Mn/e70J4btH73EZB6sm7vmhIuBZZlvc3oYGeJN/t/9vLwomFqrlXVw318J2A== diff --git a/nladmin-ui/src/api/voice.js b/nladmin-ui/src/api/voice.js new file mode 100644 index 0000000..a782338 --- /dev/null +++ b/nladmin-ui/src/api/voice.js @@ -0,0 +1,18 @@ +import request from '@/utils/request' + +export function voiceAsr(formData) { + return request({ + url: '/api/wms/voice/asr', + method: 'post', + data: formData, + headers: { 'Content-Type': 'multipart/form-data' } + }) +} + +export function voiceConfirm(data) { + return request({ + url: '/api/wms/voice/confirm', + method: 'post', + data + }) +} diff --git a/nladmin-ui/src/layout/components/Navbar.vue b/nladmin-ui/src/layout/components/Navbar.vue index 4c8e4e8..418187b 100644 --- a/nladmin-ui/src/layout/components/Navbar.vue +++ b/nladmin-ui/src/layout/components/Navbar.vue @@ -23,6 +23,11 @@ --> + + + 语音 + +
@@ -57,6 +62,37 @@
+ + +
+
+
+ + 语音交互 + {{ connectError }} +
+
+ 重置 + 关闭 +
+
+
+
+
{{ msg.role === 'user' ? '我' : '系统' }} · {{ msg.time }}
+
{{ msg.content }}
+
{{ msg.textRaw }}
+
+ 语音确认 +
+
+
请点击下方按住说话开始交互
+
+ +
+
@@ -73,6 +109,7 @@ import Search from '@/components/HeaderSearch' import Avatar from '@/assets/images/avatar.png' import NoticeIcon from '@/views/system/notice/NoticeIcon.vue' import NoticeIconReader from '@/views/system/notice/NoticeIconReader.vue' +import { voiceAsr, voiceConfirm } from '@/api/voice' export default { components: { @@ -90,12 +127,23 @@ export default { return { Avatar: Avatar, dialogVisible: false, - language: '简体中文' + language: '简体中文', + voicePanelVisible: false, + wsStatus: 'http', + connectError: '', + messages: [], + recording: false, + mediaRecorder: null, + audioChunks: [], + pendingSend: false } }, created() { this.initLang() }, + beforeDestroy() { + this.teardownVoice() + }, computed: { ...mapGetters([ 'sidebar', @@ -162,6 +210,99 @@ export default { this.$store.dispatch('LogOut').then(() => { location.reload() }) + }, + toggleVoicePanel() { + this.voicePanelVisible = !this.voicePanelVisible + if (!this.voicePanelVisible) { + this.teardownVoice() + } + }, + resetVoice() { + this.messages = [] + this.pendingSend = false + this.audioChunks = [] + this.recording = false + }, + teardownVoice() { + if (this.mediaRecorder) { + this.mediaRecorder.stop() + this.mediaRecorder = null + } + this.recording = false + this.pendingSend = false + this.audioChunks = [] + }, + startRecording() { + if (this.recording) return + navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) => { + this.audioChunks = [] + this.mediaRecorder = new MediaRecorder(stream) + this.mediaRecorder.ondataavailable = (e) => { + if (e.data.size > 0) this.audioChunks.push(e.data) + } + this.mediaRecorder.onstop = () => { + const blob = new Blob(this.audioChunks, { type: 'audio/webm' }) + this.sendAudio(blob) + stream.getTracks().forEach(t => t.stop()) + this.recording = false + } + this.mediaRecorder.start() + this.recording = true + }).catch(() => { + this.$message.error('无法获取麦克风权限') + }) + }, + stopRecording() { + if (!this.recording || !this.mediaRecorder) return + this.mediaRecorder.stop() + }, + sendAudio(blob) { + this.pendingSend = true + this.appendMessage('user', '上传语音中...') + const formData = new FormData() + formData.append('audio', blob, 'voice.webm') + voiceAsr(formData).then(res => { + const text = res.data && res.data ? res.data.text : '' + this.markUserDone('语音已上传') + if (text) { + this.appendMessage('system', text, text) + } else { + this.appendMessage('system', '未识别到有效文本', '') + } + }).catch(err => { + console.error(err) + this.markUserDone('上传失败') + this.appendMessage('system', 'ASR 调用失败', '') + }) + }, + confirmText(text) { + this.appendMessage('user', `语音确认: ${text}`) + voiceConfirm({ text }).then(() => { + this.appendMessage('system', '已提交任务', text) + }).catch(err => { + console.error(err) + this.appendMessage('system', '任务提交失败', text) + }) + }, + appendMessage(role, content, textRaw = '') { + const now = new Date() + const time = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}` + this.messages.push({ role, content, time, textRaw }) + this.$nextTick(() => { + if (this.$refs.voiceScroll) { + this.$refs.voiceScroll.scrollTop = this.$refs.voiceScroll.scrollHeight + } + }) + }, + markUserDone(doneText) { + if (!this.pendingSend) return + for (let i = this.messages.length - 1; i >= 0; i -= 1) { + if (this.messages[i].role === 'user' && this.messages[i].content.includes('上传语音')) { + this.messages[i].content = doneText + this.pendingSend = false + break + } + } } } } @@ -228,6 +369,10 @@ export default { } } } + .voice-btn { + font-weight: 600; + color: #0c8bff; + } .user-avatar { cursor: pointer; width: 40px; @@ -249,4 +394,118 @@ export default { } } } + +.voice-float { + position: fixed; + right: 24px; + bottom: 24px; + width: 360px; + background: #0e1a2b; + color: #f6f8fb; + border-radius: 14px; + box-shadow: 0 10px 30px rgba(0,0,0,0.25); + overflow: hidden; + display: flex; + flex-direction: column; + backdrop-filter: blur(6px); + border: 1px solid rgba(255,255,255,0.08); + + .voice-header { + padding: 10px 12px; + display: flex; + align-items: center; + justify-content: space-between; + background: linear-gradient(135deg, #102544, #0e1a2b); + + .title { + font-weight: 700; + display: flex; + align-items: center; + gap: 8px; + } + .actions { + display: flex; + gap: 4px; + } + .status-dot { + width: 10px; + height: 10px; + border-radius: 50%; + background: #38bdf8; + } + .error-text { + color: #f87171; + font-size: 12px; + } + } + + .voice-body { + flex: 1; + max-height: 320px; + overflow-y: auto; + padding: 10px 12px; + background: linear-gradient(180deg, #0e1a2b 0%, #0b1320 100%); + } + + .msg { + margin-bottom: 10px; + .meta { + font-size: 12px; + color: #9ca3af; + margin-bottom: 4px; + } + .bubble { + display: inline-block; + padding: 10px 12px; + border-radius: 10px; + line-height: 1.4; + max-width: 92%; + } + .bubble-raw { + margin-top: 6px; + font-size: 12px; + color: #cbd5e1; + opacity: 0.8; + } + .confirm-box { + margin-top: 6px; + } + &.user .bubble { + background: #133b70; + color: #dbeafe; + } + &.system .bubble { + background: #111827; + border: 1px solid rgba(255,255,255,0.08); + color: #e5e7eb; + } + } + + .empty { + text-align: center; + color: #9ca3af; + font-size: 14px; + padding: 30px 10px; + } + + .voice-footer { + padding: 12px; + display: flex; + align-items: center; + gap: 12px; + border-top: 1px solid rgba(255,255,255,0.06); + background: #0f1c2f; + + .mic { + width: 48px; + height: 48px; + font-size: 20px; + box-shadow: 0 10px 20px rgba(0,0,0,0.25); + } + .hint { + color: #9ca3af; + font-size: 14px; + } + } +}