Compare commits
4 Commits
feature/tt
...
bced456945
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bced456945 | ||
|
|
5d83cceca5 | ||
|
|
6950c6c2c2 | ||
|
|
f9ca8583fb |
@@ -201,17 +201,6 @@
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
<version>8.0.20</version>
|
||||
</dependency>
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>org.nl</groupId>-->
|
||||
<!-- <artifactId>nl-verify-check</artifactId>-->
|
||||
<!-- <version>1.1-Spring264</version>-->
|
||||
<!-- <type>pom</type>-->
|
||||
<!-- </dependency>-->
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>org.nl</groupId>-->
|
||||
<!-- <artifactId>nl-verify-check-sdk</artifactId>-->
|
||||
<!-- <version>1.1-Spring264</version>-->
|
||||
<!-- </dependency>-->
|
||||
<!-- druid数据源驱动 -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
package org.nl.gateway.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaIgnore;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.nl.common.logging.annotation.Log;
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
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<Object> 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<Object> confirm(@RequestBody Map<String, Object> body) throws Exception {
|
||||
String text = String.valueOf(body.getOrDefault("text", ""));
|
||||
voiceService.dispatchText(text);
|
||||
return new ResponseEntity<>(HttpStatus.OK);
|
||||
}
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
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<String, Object> body = new LinkedMultiValueMap<>();
|
||||
body.add("audio", resource);
|
||||
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
|
||||
ResponseEntity<String> 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<String> entity = new HttpEntity<>(payload, headers);
|
||||
ResponseEntity<String> 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<String> entity = new HttpEntity<>(payload, headers);
|
||||
ResponseEntity<String> 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);
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
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<VoiceWsEndpoint> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
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(this.getClass().getSimpleName());
|
||||
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<Structattr>()
|
||||
.set(Structattr::getStoragevehicle_code,null)
|
||||
.eq(Structattr::getStruct_code,taskObj.getPoint_code1()));
|
||||
iStructattrService.update(new LambdaUpdateWrapper<Structattr>()
|
||||
.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<SchBaseTask>()
|
||||
.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);
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ public class VehicleInTask extends AbstractTask {
|
||||
task.setTask_id(IdUtil.getStringId());
|
||||
task.setTask_code(CodeUtil.getNewCode("TASK_CODE"));
|
||||
task.setTask_status(TaskStatus.CREATE.getCode());
|
||||
task.setConfig_code(this.getClass().getSimpleName());
|
||||
task.setConfig_code(json.getString("config_code"));
|
||||
task.setPoint_code1(json.getString("point_code1"));
|
||||
task.setPoint_code2(json.getString("point_code2"));
|
||||
task.setVehicle_code(json.getString("vehicle_code"));
|
||||
|
||||
@@ -4,18 +4,12 @@ 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 com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import org.nl.common.exception.BadRequestException;
|
||||
import org.nl.common.utils.CodeUtil;
|
||||
import org.nl.common.utils.SecurityUtils;
|
||||
import org.nl.common.utils.IdUtil;;
|
||||
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.ISchBasePointService;
|
||||
import org.nl.wms.sch_manage.service.ISchBaseTaskService;
|
||||
import org.nl.wms.sch_manage.service.dao.SchBasePoint;
|
||||
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;
|
||||
@@ -41,17 +35,8 @@ public class VehicleOutTask extends AbstractTask {
|
||||
@Autowired
|
||||
private ISchBaseTaskService taskService;
|
||||
|
||||
/**
|
||||
* 点位服务
|
||||
*/
|
||||
@Autowired
|
||||
private ISchBasePointService iSchBasePointService;
|
||||
|
||||
/**
|
||||
* 仓位服务
|
||||
*/
|
||||
@Autowired
|
||||
private IStructattrService iStructattrService;
|
||||
@Resource
|
||||
private IOutBillService outBillService;
|
||||
|
||||
@Resource
|
||||
private IOStorInvDisMapper ioStorInvDisMapper;
|
||||
@@ -60,9 +45,9 @@ public class VehicleOutTask extends AbstractTask {
|
||||
public String create(JSONObject json) {
|
||||
SchBaseTask task = new SchBaseTask();
|
||||
task.setTask_id(IdUtil.getStringId());
|
||||
task.setTask_code(CodeUtil.getNewCode("TASK_CODE"));
|
||||
task.setTask_code(json.getString("TaskCode"));
|
||||
task.setTask_status(TaskStatus.CREATE.getCode());
|
||||
task.setConfig_code(this.getClass().getSimpleName());
|
||||
task.setConfig_code(json.getString("task_type"));
|
||||
task.setPoint_code1(json.getString("PickingLocation"));
|
||||
task.setPoint_code2(json.getString("PlacedLocation"));
|
||||
task.setVehicle_code(json.getString("vehicle_code"));
|
||||
@@ -146,23 +131,11 @@ public class VehicleOutTask extends AbstractTask {
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void finishTask(SchBaseTask taskObj) {
|
||||
iStructattrService.update(
|
||||
new UpdateWrapper<Structattr>().lambda()
|
||||
.eq(Structattr::getStruct_code, taskObj.getPoint_code1())
|
||||
.set(Structattr::getStoragevehicle_code, null)
|
||||
.set(Structattr::getTask_code, null)
|
||||
.set(Structattr::getLock_type, IOSEnum.LOCK_TYPE.code("未锁定"))
|
||||
);
|
||||
// 更新终点
|
||||
iSchBasePointService.update(
|
||||
new UpdateWrapper<SchBasePoint>().lambda()
|
||||
.eq(SchBasePoint::getPoint_code, taskObj.getPoint_code2())
|
||||
.set(SchBasePoint::getVehicle_code, taskObj.getVehicle_code())
|
||||
);
|
||||
// 更新任务
|
||||
taskObj.setRemark("已完成");
|
||||
// 任务完成
|
||||
taskObj.setTask_status(TaskStatus.FINISHED.getCode());
|
||||
taskObj.setRemark("已完成");
|
||||
taskService.updateById(taskObj);
|
||||
outBillService.taskFinish(taskObj);
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package org.nl.wms.system_manage.controller.speech;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.nl.wms.system_manage.service.speech.SysSpeechService;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author Zhao Ya Fei
|
||||
* @date 2026-04-14
|
||||
* 根据语音输入执行任务
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/speech")
|
||||
public class SysSpeechController {
|
||||
|
||||
@Resource
|
||||
private SysSpeechService sysSpeechService;
|
||||
|
||||
/**
|
||||
* 健康检查
|
||||
* @return
|
||||
*/
|
||||
@GetMapping("/health")
|
||||
public ResponseEntity<Object> health(){
|
||||
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 语音转文字
|
||||
* @return
|
||||
*/
|
||||
@PostMapping("/asr")
|
||||
public ResponseEntity<Object> asr(){
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 文字转语音
|
||||
* @return
|
||||
*/
|
||||
@PostMapping("/tts")
|
||||
public ResponseEntity<Object> tts(){
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户确认文字
|
||||
* @return
|
||||
*/
|
||||
@PostMapping("/confirm")
|
||||
public ResponseEntity<Object> confirm(@RequestBody Map<String, Object> body){
|
||||
Map<String, Object> res = sysSpeechService.confirm(body.getOrDefault("text", "").toString());
|
||||
return ResponseEntity.ok(res);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package org.nl.wms.system_manage.controller.speech;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.nl.common.utils.MapOf;
|
||||
import org.nl.wms.system_manage.service.speech.api.AsrTtsClient;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.websocket.*;
|
||||
import javax.websocket.server.ServerEndpoint;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
@ServerEndpoint("/ws/speech")
|
||||
public class SysSpeechWebSocketEndpoint {
|
||||
|
||||
|
||||
private static AsrTtsClient asrTtsClient;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
// 存储每个会话的上下文
|
||||
private static final Map<String, SessionContext> sessionContexts = new ConcurrentHashMap<>();
|
||||
private Session session;
|
||||
|
||||
//注入静态属性
|
||||
@Autowired
|
||||
public void setAsrTtsClient(AsrTtsClient asrTtsClient) {
|
||||
SysSpeechWebSocketEndpoint.asrTtsClient = asrTtsClient;
|
||||
}
|
||||
|
||||
@OnOpen
|
||||
public void onOpen(Session session) {
|
||||
sessionContexts.put(session.getId(), new SessionContext());
|
||||
this.session = session;
|
||||
log.info("WebSocket 监听: {}", session.getId());
|
||||
}
|
||||
|
||||
@OnMessage
|
||||
public void onTextMessage(String message, Session session) throws IOException {
|
||||
SessionContext ctx = sessionContexts.get(session.getId());
|
||||
if (ctx == null) return;
|
||||
|
||||
JsonNode json = objectMapper.readTree(message);
|
||||
String type = json.get("type").asText();
|
||||
|
||||
if ("start".equals(type)) {
|
||||
//TODO 健康检查
|
||||
asrTtsClient.health().thenAccept(result -> {
|
||||
log.info("健康检查结果: {}", result);
|
||||
Integer resCode = JSON.parseObject(result.toString()).getInteger("code");
|
||||
if (resCode != 0) {
|
||||
sendTextBySys("语音控制服务异常");
|
||||
}
|
||||
}).exceptionally(ex -> {
|
||||
log.error("健康检查异常: {}", session.getId(), ex);
|
||||
sendTextBySys("语音控制服务异常");
|
||||
return null;
|
||||
});
|
||||
} else if ("end".equals(type)) {
|
||||
// 录音结束,将 PCM 数据发送给第三方 ASR
|
||||
byte[] pcmData = ctx.audioBuffer.toByteArray();
|
||||
|
||||
// 异步调用第三方服务,避免阻塞 WebSocket 线程
|
||||
asrTtsClient.recognizeAsync(pcmData, ctx.sampleRate, ctx.channels, ctx.bitDepth)
|
||||
.thenAccept(result -> {
|
||||
JSONObject jsonObject = JSON.parseObject(result.toString(), JSONObject.class);
|
||||
log.info("ASR 输出参数为:-------------------" + jsonObject.toString());
|
||||
Integer code = jsonObject.getInteger("code");
|
||||
if (code == null || code != 0) {
|
||||
String msg = jsonObject.getString("msg");
|
||||
sendTextBySys(msg != null ? msg : "未知错误");
|
||||
return;
|
||||
}
|
||||
JSONObject data = jsonObject.getJSONObject("data");
|
||||
if (data != null) {
|
||||
String text = data.getString("text");
|
||||
sendTextByUser(text);
|
||||
} else {
|
||||
sendTextBySys("语音转文字发生错误");
|
||||
}
|
||||
})
|
||||
.exceptionally(ex -> {
|
||||
log.error("ASR 异常: {}", session.getId(), ex);
|
||||
sendTextByUser("ASR 异常");
|
||||
return null;
|
||||
});
|
||||
ctx.audioBuffer.reset();
|
||||
// 清理上下文
|
||||
// sessionContexts.remove(session.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@OnMessage
|
||||
public void onBinaryMessage(ByteBuffer byteBuffer, Session session) {
|
||||
SessionContext ctx = sessionContexts.get(session.getId());
|
||||
if (ctx != null) {
|
||||
byte[] bytes = new byte[byteBuffer.remaining()];
|
||||
byteBuffer.get(bytes);
|
||||
try {
|
||||
ctx.audioBuffer.write(bytes);
|
||||
System.out.println("已接收音频数据:" + ctx.audioBuffer.size());
|
||||
} catch (IOException e) {
|
||||
log.error("写入音频数据异常: {}", session.getId(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OnClose
|
||||
public void onClose(Session session) {
|
||||
sessionContexts.remove(session.getId());
|
||||
log.info("连接关闭: {}", session.getId());
|
||||
}
|
||||
|
||||
@OnError
|
||||
public void onError(Session session, Throwable error) {
|
||||
log.error("语音控制:ws连接错误", error);
|
||||
sessionContexts.remove(session.getId());
|
||||
}
|
||||
|
||||
// 会话上下文内部类
|
||||
private static class SessionContext {
|
||||
ByteArrayOutputStream audioBuffer = new ByteArrayOutputStream();
|
||||
int sampleRate;
|
||||
int channels;
|
||||
int bitDepth;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "sampleRate=" + sampleRate + ", channels=" + channels + ", bitDepth=" + bitDepth;
|
||||
}
|
||||
}
|
||||
|
||||
private void sendTextByUser(String message) {
|
||||
try {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("type", "user");
|
||||
map.put("message", message);
|
||||
// 结果通过 WebSocket 返回给前端
|
||||
String response = objectMapper.writeValueAsString(map);
|
||||
session.getBasicRemote().sendText(response);
|
||||
} catch (IOException e) {
|
||||
log.error("发送文本消息异常: {}", session.getId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendTextBySys(String message) {
|
||||
try {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
map.put("type", "system");
|
||||
map.put("message", message);
|
||||
session.getBasicRemote().sendText(
|
||||
objectMapper.writeValueAsString(map));
|
||||
} catch (IOException e) {
|
||||
log.error("语音控制:发送错误消息异常: {}", session.getId(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
package org.nl.gateway.voice.controller;
|
||||
package org.nl.wms.system_manage.controller.speech;
|
||||
|
||||
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.nl.wms.system_manage.service.speech.VoiceCallbackService;
|
||||
import org.nl.wms.system_manage.service.speech.dto.InboundTaskRequest;
|
||||
import org.nl.wms.system_manage.service.speech.dto.MoveTaskRequest;
|
||||
import org.nl.wms.system_manage.service.speech.dto.OutboundTaskRequest;
|
||||
import org.nl.wms.system_manage.service.speech.dto.QueryBoundRequest;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -18,19 +18,17 @@ 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 调用流程)
|
||||
*
|
||||
* vc回调接口
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/wms/task")
|
||||
@Slf4j
|
||||
public class VoiceTaskController {
|
||||
public class VoiceCallbackController {
|
||||
|
||||
@Autowired
|
||||
private VoiceTaskService voiceTaskService;
|
||||
private VoiceCallbackService voiceCallbackService;
|
||||
/**
|
||||
* 语音语义解析后异步调用的任务生成接口
|
||||
*/
|
||||
@@ -49,27 +47,28 @@ public class VoiceTaskController {
|
||||
switch (taskType) {
|
||||
case "inbound":
|
||||
InboundTaskRequest inboundRequest = JSON.toJavaObject(commandParam, InboundTaskRequest.class);
|
||||
result = voiceTaskService.handleInboundTask(inboundRequest);
|
||||
result = voiceCallbackService.handleInboundTask(inboundRequest);
|
||||
break;
|
||||
case "move":
|
||||
MoveTaskRequest moveRequest = JSON.toJavaObject(commandParam, MoveTaskRequest.class);
|
||||
result = voiceTaskService.handleMoveTask(moveRequest);
|
||||
result = voiceCallbackService.handleMoveTask(moveRequest);
|
||||
break;
|
||||
case "outbound":
|
||||
OutboundTaskRequest outboundRequest = JSON.toJavaObject(commandParam, OutboundTaskRequest.class);
|
||||
result = voiceTaskService.handleOutboundTask(outboundRequest);
|
||||
result = voiceCallbackService.handleOutboundTask(outboundRequest);
|
||||
break;
|
||||
default:
|
||||
throw new BadRequestException("未知的指令类型: " + commandType);
|
||||
}
|
||||
break;
|
||||
case "InventoryQuery":
|
||||
QueryBoundRequest queryRequest = JSON.toJavaObject(commandParam, QueryBoundRequest.class);
|
||||
result = voiceTaskService.handleQueryTask(queryRequest);
|
||||
QueryBoundRequest queryRequest = JSON.toJavaObject(commandParam, QueryBoundRequest.class);
|
||||
result = voiceCallbackService.handleQueryTask(queryRequest);
|
||||
break;
|
||||
default:
|
||||
throw new BadRequestException("未知的指令类型: " + commandType);
|
||||
}
|
||||
return new ResponseEntity<>(result, HttpStatus.OK);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.nl.wms.system_manage.service.speech;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public interface SysSpeechService {
|
||||
|
||||
/**
|
||||
* 语音识别服务健康检查
|
||||
*
|
||||
* @return Boolean
|
||||
*/
|
||||
Boolean health();
|
||||
|
||||
/**
|
||||
* 语音转文字
|
||||
*
|
||||
* @return Map<String, Object>
|
||||
*/
|
||||
Map<String, Object> asr();
|
||||
|
||||
/**
|
||||
* 文字转语音
|
||||
*
|
||||
* @return Map<String, Object>
|
||||
*/
|
||||
Map<String, Object> tts();
|
||||
|
||||
/**
|
||||
* 语音识别结果确认
|
||||
*
|
||||
* @return Map<String, Object>
|
||||
*/
|
||||
Map<String, Object> confirm(String text);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
package org.nl.gateway.voice.service;
|
||||
package org.nl.wms.system_manage.service.speech;
|
||||
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
@@ -18,10 +19,6 @@ 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;
|
||||
@@ -38,6 +35,10 @@ 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.system_manage.service.speech.dto.InboundTaskRequest;
|
||||
import org.nl.wms.system_manage.service.speech.dto.MoveTaskRequest;
|
||||
import org.nl.wms.system_manage.service.speech.dto.OutboundTaskRequest;
|
||||
import org.nl.wms.system_manage.service.speech.dto.QueryBoundRequest;
|
||||
import org.nl.wms.warehouse_manage.enums.IOSConstant;
|
||||
import org.nl.wms.warehouse_manage.enums.IOSEnum;
|
||||
import org.nl.wms.warehouse_manage.service.IMdPbGroupplateService;
|
||||
@@ -68,7 +69,7 @@ import static org.nl.wms.warehouse_manage.enums.IOSEnum.GROUP_PLATE_STATUS;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class VoiceTaskService {
|
||||
public class VoiceCallbackService {
|
||||
|
||||
@Autowired
|
||||
private IStructattrService iStructattrService;
|
||||
@@ -80,14 +81,14 @@ public class VoiceTaskService {
|
||||
private ISchBasePointService iSchBasePointService;
|
||||
@Autowired
|
||||
private IMdPbGroupplateService iMdPbGroupplateService;
|
||||
@Autowired
|
||||
private VoiceService voiceService;
|
||||
|
||||
|
||||
@Transactional
|
||||
public JSONObject handleInboundTask(InboundTaskRequest inboundTaskRequest){
|
||||
// SchBasePoint one = iSchBasePointService.getOne(new LambdaQueryWrapper<SchBasePoint>()
|
||||
// .eq(SchBasePoint::getPoint_code, inboundTaskRequest.getLocationFrom()));
|
||||
SchBasePoint one = iSchBasePointService.getOne(new LambdaQueryWrapper<SchBasePoint>()
|
||||
.eq(SchBasePoint::getPoint_code, inboundTaskRequest.getLocationFrom()));
|
||||
.eq(SchBasePoint::getPoint_code, "RKD01"));
|
||||
//分配仓位
|
||||
MdMeMaterialbase meMaterialbase = iMdMeMaterialbaseService.getByCode(inboundTaskRequest.getMaterial());
|
||||
|
||||
@@ -115,8 +116,8 @@ public class VoiceTaskService {
|
||||
List<Structattr> structattrs = iStructattrService.inBoundSectDiv(
|
||||
StrategyStructParam.builder()
|
||||
.ioType(StatusEnum.STRATEGY_TYPE.code("入库"))
|
||||
.sect_code("BG01")
|
||||
.stor_code("BGK")
|
||||
.sect_code("YL01")
|
||||
.stor_code("YL")
|
||||
.storagevehicle_code(one.getVehicle_code())
|
||||
.strategyMaters(materss)
|
||||
.build());
|
||||
@@ -125,23 +126,28 @@ public class VoiceTaskService {
|
||||
}
|
||||
Structattr attrDao = structattrs.get(0);
|
||||
//确定起点
|
||||
SchBasePoint schBasePoint = iSchBasePointService.getOne(new LambdaQueryWrapper<SchBasePoint>()
|
||||
.eq(SchBasePoint::getPoint_code, inboundTaskRequest.getLocationFrom()));
|
||||
if (ObjectUtil.isEmpty(schBasePoint)) {
|
||||
throw new BadRequestException("未找到载具所在的点位信息,请检查");
|
||||
}
|
||||
// SchBasePoint schBasePoint = iSchBasePointService.getOne(new LambdaQueryWrapper<SchBasePoint>()
|
||||
// .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_code1", "RKD01");
|
||||
// whereJson.put("point_code1", schBasePoint.getPoint_code());
|
||||
//确定终点
|
||||
whereJson.put("point_code2", attrDao.getStruct_code());
|
||||
// whereJson.put("point_code2", attrDao.getStruct_code());
|
||||
whereJson.put("vehicle_code", one.getVehicle_code());
|
||||
whereJson.put("task_type", "STInTask");
|
||||
whereJson.put("TaskCode", CodeUtil.getNewCode("TASK_CODE"));
|
||||
whereJson.put("PickingLocation", "RKD01");
|
||||
whereJson.put("PlacedLocation", attrDao.getStruct_code());
|
||||
//创建任务
|
||||
String taskId = applyTaskMap.get("VehicleInTask").create(whereJson);
|
||||
String taskId = applyTaskMap.get("STInTask").create(whereJson);
|
||||
// 更新起点绑定id
|
||||
iSchBasePointService.update(
|
||||
new UpdateWrapper<SchBasePoint>().lambda()
|
||||
.set(SchBasePoint::getIos_id, null)
|
||||
.eq(SchBasePoint::getPoint_code, schBasePoint.getPoint_code())
|
||||
.eq(SchBasePoint::getPoint_code, "RKD01")
|
||||
);
|
||||
// 更新终点锁定状态
|
||||
JSONObject lock_map = new JSONObject();
|
||||
@@ -160,8 +166,8 @@ public class VoiceTaskService {
|
||||
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");
|
||||
whereJson.put("stor_code", "YL");
|
||||
whereJson.put("sect_code", "YL01");
|
||||
whereJson.put("siteCode", "CKD01");
|
||||
StrategyMater mater = new StrategyMater();
|
||||
mater.setQty(BigDecimal.valueOf(outboundTaskRequest.getQty()));
|
||||
@@ -215,27 +221,42 @@ public class VoiceTaskService {
|
||||
return null;
|
||||
};
|
||||
public JSONObject handleMoveTask(MoveTaskRequest moveTaskRequest){
|
||||
String taskId = applyTaskMap.get("StructMoveTask").create((JSONObject) JSONObject.toJSON(moveTaskRequest));
|
||||
Structattr attrDao = iStructattrService.findByCode(moveTaskRequest.getLocationFrom());
|
||||
JSONObject taskForm = new JSONObject();
|
||||
taskForm.put("point_code1", moveTaskRequest.getLocationFrom());
|
||||
taskForm.put("point_code", moveTaskRequest.getLocationTo());
|
||||
taskForm.put("config_code", IOSConstant.MOVE_CONFIG_TASK);
|
||||
taskForm.put("vehicle_code", attrDao.getStoragevehicle_code());
|
||||
String taskId = applyTaskMap.get("MoveTask").create((JSONObject) JSONObject.toJSON(taskForm));
|
||||
return null;
|
||||
};
|
||||
|
||||
public JSONObject handleQueryTask(QueryBoundRequest queryBoundRequest){
|
||||
List<StructattrVechielDto> structCode = iStructattrService.collectVechicle(MapOf.of("struct_code", queryBoundRequest.getLocation(), "search", queryBoundRequest.getMaterial()));
|
||||
Map<String, BigDecimal> collect = structCode.stream()
|
||||
.collect(Collectors.groupingBy(StructattrVechielDto::getMaterial_code,
|
||||
Collectors.reducing(BigDecimal.ZERO, StructattrVechielDto::getQty, BigDecimal::add)));
|
||||
StringBuffer stringBuffer = new StringBuffer();
|
||||
stringBuffer.append("当前仓库物料有物料");
|
||||
for (StructattrVechielDto vechielDtos : structCode) {
|
||||
stringBuffer.append(vechielDtos.getMaterial_name()).append("共计")
|
||||
.append(vechielDtos.getQty())
|
||||
collect.entrySet().forEach(r -> {
|
||||
stringBuffer.append(r.getKey()).append("共计")
|
||||
.append(r.getValue())
|
||||
.append("个");
|
||||
}
|
||||
});
|
||||
// for (StructattrVechielDto vechielDtos : structCode) {
|
||||
// stringBuffer.append(vechielDtos.getMaterial_name()).append("共计")
|
||||
// .append(vechielDtos.getQty())
|
||||
// .append("个");
|
||||
// }
|
||||
String text = stringBuffer.toString();
|
||||
try {
|
||||
// voiceService.speechTts(text);
|
||||
}catch (Exception ex){
|
||||
ex.printStackTrace();
|
||||
ex.printStackTrace();
|
||||
}
|
||||
JSONObject result = new JSONObject();
|
||||
result.put("has",text);
|
||||
return result ;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package org.nl.wms.system_manage.service.speech.api;
|
||||
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
|
||||
/**
|
||||
* 语音识别服务接口
|
||||
*/
|
||||
@Component
|
||||
public class AsrTtsClient {
|
||||
|
||||
@Value("${vc.base-url:http://127.0.0.1:5000}")
|
||||
private String vcBaseUrl;
|
||||
|
||||
/**
|
||||
* 健康检查
|
||||
*/
|
||||
public CompletableFuture<Object> health() {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
return HttpRequest.get(vcBaseUrl + "/health")
|
||||
.execute()
|
||||
.body();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 语音转文字
|
||||
*/
|
||||
public static void asr() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 文字转语音
|
||||
*/
|
||||
public CompletableFuture<byte[]> tts(String text) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("text", text);
|
||||
params.put("vcn", "xiaoyan");
|
||||
params.put("speed", 50);
|
||||
params.put("volume", 50);
|
||||
params.put("pitch", 50);
|
||||
return HttpRequest.post(vcBaseUrl + "/api/speech/tts")
|
||||
.body(JSONObject.toJSONString(params))
|
||||
.contentType("application/json")
|
||||
.execute()
|
||||
.bodyBytes();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户确认文字
|
||||
*/
|
||||
public String confirm(String text, boolean confirmed) {
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("text", text);
|
||||
params.put("confirmed", confirmed);
|
||||
return HttpRequest.post(vcBaseUrl + "/api/speech/confirm")
|
||||
.body(JSONObject.toJSONString(params))
|
||||
.contentType("application/json")
|
||||
.execute()
|
||||
.body();
|
||||
|
||||
}
|
||||
|
||||
public CompletionStage<Object> recognizeAsync(byte[] pcmData, int sampleRate, int channels, int bitDepth) {
|
||||
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
return HttpRequest.post(vcBaseUrl + "/api/speech/asr")
|
||||
.form("audio", pcmData, "audio.pcm")
|
||||
// .body(pcmData)
|
||||
.contentType("multipart/form-data")
|
||||
.execute()
|
||||
.body();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,19 @@
|
||||
package org.nl.gateway.voice.service.dto;
|
||||
package org.nl.wms.system_manage.service.speech.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; // 入库物料数量
|
||||
}
|
||||
@@ -1,22 +1,17 @@
|
||||
package org.nl.gateway.voice.service.dto;
|
||||
package org.nl.wms.system_manage.service.speech.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; // 放货点编号:移到哪个库位
|
||||
}
|
||||
@@ -1,22 +1,17 @@
|
||||
package org.nl.gateway.voice.service.dto;
|
||||
package org.nl.wms.system_manage.service.speech.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; // 出库数量
|
||||
}
|
||||
@@ -1,19 +1,15 @@
|
||||
package org.nl.gateway.voice.service.dto;
|
||||
package org.nl.wms.system_manage.service.speech.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; // 物料编码:基于物料查询库存
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.nl.wms.system_manage.service.speech.enums;
|
||||
|
||||
/**
|
||||
* @author Zhao Ya Fei
|
||||
* @date 2026-04-14
|
||||
* @desc 语音识别URL
|
||||
*/
|
||||
public class SpeechUrlConstant {
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package org.nl.wms.system_manage.service.speech.impl;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.nl.common.utils.MapOf;
|
||||
import org.nl.wms.system_manage.service.speech.SysSpeechService;
|
||||
import org.nl.wms.system_manage.service.speech.api.AsrTtsClient;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author Zhao Ya Fei
|
||||
* @date 2026-04-14
|
||||
* 语音识别服务实现类
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class SysSpeechServiceImpl implements SysSpeechService {
|
||||
|
||||
@Resource
|
||||
private AsrTtsClient asrTtsClient;
|
||||
|
||||
@Override
|
||||
public Boolean health() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> asr() {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> tts() {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> confirm(String text) {
|
||||
if ("".equals(text)) {
|
||||
throw new RuntimeException("确认文本为空");
|
||||
}
|
||||
String resJson = asrTtsClient.confirm(text, true);
|
||||
log.info("语音控制:用户确认结果: {}", resJson);
|
||||
//TODO
|
||||
JSONObject jsonObject = JSONObject.parseObject(resJson);
|
||||
JSONObject data = jsonObject.getJSONObject("data");
|
||||
if (data != null) {
|
||||
String resText = data.getString("text");
|
||||
return MapOf.of("text", resText);
|
||||
}
|
||||
byte[] audio = asrTtsClient.tts(text).join();
|
||||
//TODO 音频返回前端播放处理
|
||||
return MapOf.of("text", text);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package org.nl.wms.system_manage.service.speech.session;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.websocket.Session;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* 暂时先不用
|
||||
* 后续如果需要实时展示任务结果可能用到
|
||||
*/
|
||||
@Slf4j
|
||||
//@Component
|
||||
public class SessionManager {
|
||||
|
||||
// userId -> WebSocket Session
|
||||
private final Map<String, Session> sessionMap = new ConcurrentHashMap<>();
|
||||
private final AtomicInteger onlineCount = new AtomicInteger(0);
|
||||
|
||||
/**
|
||||
* 注册会话(支持单端登录策略:新连接踢掉旧连接)
|
||||
*/
|
||||
public void register(String userId, Session session) {
|
||||
Session oldSession = sessionMap.put(userId, session);
|
||||
if (oldSession != null && oldSession.isOpen()) {
|
||||
try { oldSession.close(); } catch (IOException ignored) {}
|
||||
log.info("语音控制:🔄 用户 {} 旧连接已被踢出", userId);
|
||||
}
|
||||
onlineCount.incrementAndGet();
|
||||
log.info("语音控制:✅ 用户 {} 已上线 | 当前在线: {}", userId, onlineCount.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销会话
|
||||
*/
|
||||
public void unregister(String userId) {
|
||||
sessionMap.remove(userId);
|
||||
onlineCount.decrementAndGet();
|
||||
log.info("语音控制:❌ 用户 {} 已下线 | 当前在线: {}", userId, onlineCount.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* 主动推送文本消息
|
||||
*/
|
||||
public boolean pushText(String userId, String message) {
|
||||
Session session = sessionMap.get(userId);
|
||||
if (session == null || !session.isOpen()) {
|
||||
log.warn("⚠️ 推送失败: 用户 {} 不在线", userId);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
session.getBasicRemote().sendText(message);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
log.error("📤 推送文本异常: {}", userId, e);
|
||||
// 连接已失效,自动清理
|
||||
unregister(userId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 主动推送二进制数据(TTS音频、文件流等)
|
||||
*/
|
||||
public boolean pushBinary(String userId, byte[] data) {
|
||||
Session session = sessionMap.get(userId);
|
||||
if (session == null || !session.isOpen()) {
|
||||
log.warn("⚠️ 推送失败: 用户 {} 不在线", userId);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
session.getBasicRemote().sendBinary(ByteBuffer.wrap(data));
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
log.error("📤 推送二进制异常: {}", userId, e);
|
||||
unregister(userId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播文本(如系统公告、全局状态)
|
||||
*/
|
||||
public void broadcastText(String message) {
|
||||
sessionMap.values().forEach(session -> {
|
||||
if (session.isOpen()) {
|
||||
try { session.getBasicRemote().sendText(message); } catch (IOException ignored) {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public int getOnlineCount() { return onlineCount.get(); }
|
||||
}
|
||||
@@ -9,10 +9,11 @@ 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: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
|
||||
# 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:localhost}:${DB_PORT:3306}/${DB_NAME:wms_standardv2}?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false&useOldAliasMetadataBehavior=true&allowPublicKeyRetrieval=true&useSSL=false
|
||||
username: ${DB_USER:root}
|
||||
password: ${DB_PWD:P@ssw0rd.}
|
||||
password: ${DB_PWD:123456}
|
||||
# password: ${DB_PWD:P@ssw0rd.}
|
||||
# 初始连接数
|
||||
initial-size: 15
|
||||
# 最小连接数
|
||||
@@ -137,3 +138,5 @@ sa-token:
|
||||
lucene:
|
||||
index:
|
||||
path: D:\lms\lucene\index
|
||||
vc:
|
||||
base-url: http://192.168.20.156:5000
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
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==
|
||||
@@ -1,17 +1,8 @@
|
||||
import request from '@/utils/request'
|
||||
import request from "@/utils/request";
|
||||
|
||||
export function voiceAsr(formData) {
|
||||
export function confirm(data) {
|
||||
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',
|
||||
url: '/api/speech/confirm',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
|
||||
1184
nladmin-ui/src/components/VoiceInputDialog/index.vue
Normal file
1184
nladmin-ui/src/components/VoiceInputDialog/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -79,6 +79,7 @@ export default {
|
||||
'moreMenu': 'MoreMenu',
|
||||
'browses': 'browse',
|
||||
'fz': 'Full screen zoom',
|
||||
'speech': 'Voice Control',
|
||||
'submit': 'Submit Success',
|
||||
'add': 'Add Success',
|
||||
'edit': 'Edit Success',
|
||||
|
||||
@@ -79,6 +79,7 @@ export default {
|
||||
'moreMenu': '更多菜单',
|
||||
'browses': '浏览',
|
||||
'fz': '全屏缩放',
|
||||
'speech': '语音控制',
|
||||
'submit': '提交成功',
|
||||
'add': '新增成功',
|
||||
'edit': '编辑成功',
|
||||
|
||||
@@ -6,6 +6,13 @@
|
||||
|
||||
<div class="right-menu">
|
||||
<template v-if="device!=='mobile'">
|
||||
<!-- 语音输入按钮 -->
|
||||
<el-tooltip :content="$t('common.speech')" effect="dark" placement="bottom">
|
||||
<div class="right-menu-item hover-effect voice-btn" @click="openVoiceInput">
|
||||
<i class="el-icon-microphone" />
|
||||
</div>
|
||||
</el-tooltip>
|
||||
|
||||
<search id="header-search" class="right-menu-item" />
|
||||
|
||||
<!-- <el-tooltip content="项目文档" effect="dark" placement="bottom">
|
||||
@@ -15,19 +22,17 @@
|
||||
<el-tooltip :content="$t('common.fz')" effect="dark" placement="bottom">
|
||||
<screenfull id="screenfull" class="right-menu-item hover-effect" />
|
||||
</el-tooltip>
|
||||
<notice-icon class="right-menu-item" />
|
||||
<notice-icon-reader ref="noticeIconReader" />
|
||||
<notice-icon class="right-menu-item" />
|
||||
<notice-icon-reader ref="noticeIconReader" />
|
||||
<voice-input-dialog ref="voiceInputDialog" />
|
||||
|
||||
<!-- <el-tooltip content="布局设置" effect="dark" placement="bottom">
|
||||
|
||||
|
||||
<!-- <el-tooltip content="布局设置" effect="dark" placement="bottom">
|
||||
<size-select id="size-select" class="right-menu-item hover-effect" />
|
||||
</el-tooltip>-->
|
||||
|
||||
</template>
|
||||
<el-tooltip content="语音助手" effect="dark" placement="bottom">
|
||||
<el-button class="right-menu-item hover-effect voice-btn" type="text" icon="el-icon-microphone" @click="toggleVoicePanel">
|
||||
语音
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<img :src="Avatar" class="user-avatar">
|
||||
<el-dropdown class="avatar-container right-menu-item hover-effect" trigger="hover">
|
||||
<div class="avatar-wrapper">
|
||||
@@ -62,37 +67,6 @@
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
|
||||
<transition name="el-zoom-in-bottom">
|
||||
<div v-if="voicePanelVisible" class="voice-float">
|
||||
<div class="voice-header">
|
||||
<div class="title">
|
||||
<i :class="['status-dot', wsStatus]" />
|
||||
语音交互
|
||||
<span v-if="connectError" class="error-text">{{ connectError }}</span>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<el-button size="mini" type="text" @click="resetVoice">重置</el-button>
|
||||
<el-button size="mini" type="text" @click="toggleVoicePanel">关闭</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="voiceScroll" class="voice-body">
|
||||
<div v-for="(msg, idx) in messages" :key="idx" :class="['msg', msg.role]">
|
||||
<div class="meta">{{ msg.role === 'user' ? '我' : '系统' }} · {{ msg.time }}</div>
|
||||
<div class="bubble">{{ msg.content }}</div>
|
||||
<div v-if="msg.role==='system' && msg.textRaw" class="bubble-raw">{{ msg.textRaw }}</div>
|
||||
<div v-if="msg.role==='system' && msg.textRaw && !msg.confirmed" class="confirm-box">
|
||||
<el-button size="mini" type="primary" @click="confirmText(msg.textRaw)">语音确认</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="messages.length === 0" class="empty">请点击下方按住说话开始交互</div>
|
||||
</div>
|
||||
<div class="voice-footer">
|
||||
<el-button :type="recording ? 'danger' : 'primary'" circle class="mic" icon="el-icon-microphone" @mousedown.native.prevent="startRecording" @mouseup.native.prevent="stopRecording" @mouseleave.native.prevent="stopRecording" />
|
||||
<div class="hint">按住说话 · 松开发送</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -109,7 +83,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'
|
||||
import VoiceInputDialog from '@/components/VoiceInputDialog'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -121,29 +95,19 @@ export default {
|
||||
SizeSelect,
|
||||
Search,
|
||||
Doc,
|
||||
TopNav
|
||||
TopNav,
|
||||
VoiceInputDialog
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
Avatar: Avatar,
|
||||
dialogVisible: false,
|
||||
language: '简体中文',
|
||||
voicePanelVisible: false,
|
||||
wsStatus: 'http',
|
||||
connectError: '',
|
||||
messages: [],
|
||||
recording: false,
|
||||
mediaRecorder: null,
|
||||
audioChunks: [],
|
||||
pendingSend: false
|
||||
language: '简体中文'
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.initLang()
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.teardownVoice()
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'sidebar',
|
||||
@@ -211,98 +175,8 @@ export default {
|
||||
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
|
||||
}
|
||||
}
|
||||
openVoiceInput() {
|
||||
this.$refs.voiceInputDialog && this.$refs.voiceInputDialog.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -369,10 +243,6 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
.voice-btn {
|
||||
font-weight: 600;
|
||||
color: #0c8bff;
|
||||
}
|
||||
.user-avatar {
|
||||
cursor: pointer;
|
||||
width: 40px;
|
||||
@@ -394,118 +264,4 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user