mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-23 23:53:55 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14a63c4b97 | ||
|
|
098d13a34b | ||
|
|
488f247744 | ||
|
|
c74dbf03cd | ||
|
|
1f836c3235 | ||
|
|
bd2e2e77d5 | ||
|
|
6282ef14e8 | ||
|
|
558f23688f | ||
|
|
541f2de599 | ||
|
|
68116254cd | ||
|
|
77ff4599ea | ||
|
|
652f5a3247 | ||
|
|
9b6e57b143 | ||
|
|
bdc19256bf | ||
|
|
de5eaf4467 | ||
|
|
ca884833d1 | ||
|
|
9fc164a69d | ||
|
|
0da2da83a5 | ||
|
|
bfdfa44edd | ||
|
|
ed1cfe5eb6 | ||
|
|
b696f3f5db | ||
|
|
7a0cf5ef9a | ||
|
|
d1b7cb6a36 | ||
|
|
b21d699472 | ||
|
|
029fadd778 | ||
|
|
215d50235b | ||
|
|
000e676397 | ||
|
|
208c045d3d | ||
|
|
ee20f59a8e | ||
|
|
925cf3dabb | ||
|
|
0049d0e926 | ||
|
|
56959808a9 | ||
|
|
8667a9680e | ||
|
|
8c7d129089 | ||
|
|
a3a9b98a1d | ||
|
|
f2f66579cb | ||
|
|
1354cf2a87 | ||
|
|
15f5592429 | ||
|
|
dc69094f00 | ||
|
|
8ad3783a36 | ||
|
|
15d5dd8e92 | ||
|
|
b07edb977b | ||
|
|
1509fc2039 | ||
|
|
2877bb8faa | ||
|
|
a83410cb49 | ||
|
|
4ff9f2235b | ||
|
|
1bc611f097 | ||
|
|
4e0a3941ff | ||
|
|
4e885dac3e | ||
|
|
eafd131a22 | ||
|
|
289ce3716b | ||
|
|
2242070bdc | ||
|
|
ee83cea11c | ||
|
|
6e571a43d8 | ||
|
|
466a0245a9 | ||
|
|
d9ce5e4ce7 | ||
|
|
6b0dd291c2 | ||
|
|
4ff77d0d7e | ||
|
|
e00da5d8d2 | ||
|
|
1a525a59ab | ||
|
|
d5581c4236 | ||
|
|
4bf950574c | ||
|
|
0b9e434c4b | ||
|
|
c4b1466643 | ||
|
|
488842029b | ||
|
|
1b0ce59371 | ||
|
|
97bea89204 | ||
|
|
0892dfc601 | ||
|
|
20d66ff401 | ||
|
|
e2fda532d1 | ||
|
|
68035af7b3 | ||
|
|
ee25555f0e | ||
|
|
e528510f5c | ||
|
|
afa0527b4c | ||
|
|
dd7d23498f | ||
|
|
b15c59aa48 | ||
|
|
98a4ce701c | ||
|
|
2fc0de8f4e | ||
|
|
66fd551d68 | ||
|
|
b80086a349 | ||
|
|
57ed552f43 | ||
|
|
5a4d65986d | ||
|
|
427b3bb774 |
@@ -155,6 +155,10 @@ main/manager-web/node_modules
|
|||||||
main/xiaozhi-server/models/SenseVoiceSmall/model.pt
|
main/xiaozhi-server/models/SenseVoiceSmall/model.pt
|
||||||
main/xiaozhi-server/models/sherpa-onnx*
|
main/xiaozhi-server/models/sherpa-onnx*
|
||||||
my_wakeup_words.mp3
|
my_wakeup_words.mp3
|
||||||
|
!main/xiaozhi-server/config/assets/bind_code.wav
|
||||||
|
!main/xiaozhi-server/config/assets/bind_not_found.wav
|
||||||
|
!main/xiaozhi-server/config/assets/bind_code/*.wav
|
||||||
|
!main/xiaozhi-server/config/assets/max_output_size.wav
|
||||||
main/manager-api/.vscode
|
main/manager-api/.vscode
|
||||||
|
|
||||||
# Ignore webpack cache directory
|
# Ignore webpack cache directory
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ server:
|
|||||||
|
|
||||||
```
|
```
|
||||||
智控台地址: https://2662r3426b.vicp.fun
|
智控台地址: https://2662r3426b.vicp.fun
|
||||||
OTA接口地址: htts://2662r3426b.vicp.fun/xiaozhi/ota/
|
OTA接口地址: https://2662r3426b.vicp.fun/xiaozhi/ota/
|
||||||
Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
||||||
```
|
```
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -138,12 +138,12 @@ manager-api:
|
|||||||
secret: 你的server.secret值
|
secret: 你的server.secret值
|
||||||
```
|
```
|
||||||
1、把你刚才从`智控台`复制过来的`server.secret`的`参数值`复制到`.config.yaml`文件里的`secret`里。
|
1、把你刚才从`智控台`复制过来的`server.secret`的`参数值`复制到`.config.yaml`文件里的`secret`里。
|
||||||
2、注意,把`url`改成下面的`http://xiaozhi-esp32-server-web/xiaozhi`
|
2、注意,把`url`改成下面的`http://xiaozhi-esp32-server-web:8002/xiaozhi`
|
||||||
|
|
||||||
类似这样的效果
|
类似这样的效果
|
||||||
```
|
```
|
||||||
manager-api:
|
manager-api:
|
||||||
url: http://xiaozhi-esp32-server-web/xiaozhi
|
url: http://xiaozhi-esp32-server-web:8002/xiaozhi
|
||||||
secret: 12345678-xxxx-xxxx-xxxx-123456789000
|
secret: 12345678-xxxx-xxxx-xxxx-123456789000
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ config WEBSOCKET_URL
|
|||||||
如果你是全模块部署本项目,就修改OTA接口,如果你只是部署了8000端口的xiaozhi-server,可以继续沿用虾哥团队的OTA接口。如果你不修改OTA接口,请直接忽略本第4步,直接看第5步
|
如果你是全模块部署本项目,就修改OTA接口,如果你只是部署了8000端口的xiaozhi-server,可以继续沿用虾哥团队的OTA接口。如果你不修改OTA接口,请直接忽略本第4步,直接看第5步
|
||||||
|
|
||||||
找到`OTA_VERSION_URL`的`default`的内容,把`https://api.tenclass.net/xiaozhi/ota/`
|
找到`OTA_VERSION_URL`的`default`的内容,把`https://api.tenclass.net/xiaozhi/ota/`
|
||||||
改成你自己的地址,例如,我的接口地址是`http://192.168.1.25:8000/xiaozhi/ota/`,就把内容改成这个。
|
改成你自己的地址,例如,我的接口地址是`http://192.168.1.25:8002/xiaozhi/ota/`,就把内容改成这个。
|
||||||
|
|
||||||
修改前:
|
修改前:
|
||||||
```
|
```
|
||||||
@@ -56,7 +56,7 @@ config OTA_VERSION_URL
|
|||||||
```
|
```
|
||||||
config OTA_VERSION_URL
|
config OTA_VERSION_URL
|
||||||
string "OTA Version URL"
|
string "OTA Version URL"
|
||||||
default "http://192.168.1.25:8000/xiaozhi/ota/"
|
default "http://192.168.1.25:8002/xiaozhi/ota/"
|
||||||
help
|
help
|
||||||
The application will access this URL to check for updates.
|
The application will access this URL to check for updates.
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -84,6 +84,16 @@ public interface Constant {
|
|||||||
*/
|
*/
|
||||||
String SERVER_SECRET = "server.secret";
|
String SERVER_SECRET = "server.secret";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否允许用户注册
|
||||||
|
*/
|
||||||
|
String SERVER_ALLOW_USER_REGISTER = "server.allow_user_register";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下发六位验证码时显示的控制面板地址
|
||||||
|
*/
|
||||||
|
String SERVER_FRONTED_URL = "server.fronted_url";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 路径分割符
|
* 路径分割符
|
||||||
*/
|
*/
|
||||||
@@ -153,4 +163,9 @@ public interface Constant {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 版本号
|
||||||
|
*/
|
||||||
|
public static final String VERSION = "0.3.7";
|
||||||
}
|
}
|
||||||
@@ -52,4 +52,7 @@ public interface ErrorCode {
|
|||||||
int PARAM_BOOLEAN_INVALID = 10038;
|
int PARAM_BOOLEAN_INVALID = 10038;
|
||||||
int PARAM_ARRAY_INVALID = 10039;
|
int PARAM_ARRAY_INVALID = 10039;
|
||||||
int PARAM_JSON_INVALID = 10040;
|
int PARAM_JSON_INVALID = 10040;
|
||||||
|
|
||||||
|
int OTA_DEVICE_NOT_FOUND = 10041;
|
||||||
|
int OTA_DEVICE_NEED_BIND = 10042;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,4 +75,11 @@ public class RedisKeys {
|
|||||||
public static String getTimbreDetailsKey(String id) {
|
public static String getTimbreDetailsKey(String id) {
|
||||||
return "timbre:details:" + id;
|
return "timbre:details:" + id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取版本号Key
|
||||||
|
*/
|
||||||
|
public static String getVersionKey() {
|
||||||
|
return "system:version";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
package xiaozhi.common.redis;
|
package xiaozhi.common.redis;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import org.springframework.data.redis.core.HashOperations;
|
import org.springframework.data.redis.core.HashOperations;
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.data.redis.core.script.DefaultRedisScript;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
@@ -27,7 +30,7 @@ public class RedisUtils {
|
|||||||
/**
|
/**
|
||||||
* 过期时长为1小时,单位:秒
|
* 过期时长为1小时,单位:秒
|
||||||
*/
|
*/
|
||||||
public final static long HOUR_ONE_EXPIRE = 60 * 60 * 1L;
|
public final static long HOUR_ONE_EXPIRE = (long) 60 * 60;
|
||||||
/**
|
/**
|
||||||
* 过期时长为6小时,单位:秒
|
* 过期时长为6小时,单位:秒
|
||||||
*/
|
*/
|
||||||
@@ -124,4 +127,24 @@ public class RedisUtils {
|
|||||||
public Object rightPop(String key) {
|
public Object rightPop(String key) {
|
||||||
return redisTemplate.opsForList().rightPop(key);
|
return redisTemplate.opsForList().rightPop(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空所有 Redis 数据库中的所有键
|
||||||
|
*/
|
||||||
|
public void emptyAll() {
|
||||||
|
// Lua 脚本 FLUSHALL是redis清空所有库的命令
|
||||||
|
String luaScript ="redis.call('FLUSHALL')";
|
||||||
|
|
||||||
|
// 创建 DefaultRedisScript 对象
|
||||||
|
DefaultRedisScript<Void> redisScript = new DefaultRedisScript<>();
|
||||||
|
redisScript.setScriptText(luaScript); // 设置 Lua 脚本内容
|
||||||
|
redisScript.setResultType(Void.class); // 设置返回值类型
|
||||||
|
|
||||||
|
// 执行 Lua 脚本
|
||||||
|
List<String> keys = Collections.emptyList(); // 如果脚本不依赖 key,可以传入空列表
|
||||||
|
redisTemplate.execute(redisScript, keys);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
+25
-23
@@ -3,6 +3,7 @@ package xiaozhi.modules.agent.service.impl;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
|
||||||
import xiaozhi.modules.agent.dao.AgentTemplateDao;
|
import xiaozhi.modules.agent.dao.AgentTemplateDao;
|
||||||
@@ -39,29 +40,30 @@ public class AgentTemplateServiceImpl extends ServiceImpl<AgentTemplateDao, Agen
|
|||||||
@Override
|
@Override
|
||||||
public void updateDefaultTemplateModelId(String modelType, String modelId) {
|
public void updateDefaultTemplateModelId(String modelType, String modelId) {
|
||||||
modelType = modelType.toUpperCase();
|
modelType = modelType.toUpperCase();
|
||||||
AgentTemplateEntity defaultTemplate = getDefaultTemplate();
|
|
||||||
if (defaultTemplate != null) {
|
UpdateWrapper<AgentTemplateEntity> wrapper = new UpdateWrapper<>();
|
||||||
switch (modelType) {
|
switch (modelType) {
|
||||||
case "ASR":
|
case "ASR":
|
||||||
defaultTemplate.setAsrModelId(modelId);
|
wrapper.set("asr_model_id", modelId);
|
||||||
break;
|
break;
|
||||||
case "VAD":
|
case "VAD":
|
||||||
defaultTemplate.setVadModelId(modelId);
|
wrapper.set("vad_model_id", modelId);
|
||||||
break;
|
break;
|
||||||
case "LLM":
|
case "LLM":
|
||||||
defaultTemplate.setLlmModelId(modelId);
|
wrapper.set("llm_model_id", modelId);
|
||||||
break;
|
break;
|
||||||
case "TTS":
|
case "TTS":
|
||||||
defaultTemplate.setTtsModelId(modelId);
|
wrapper.set("tts_model_id", modelId);
|
||||||
break;
|
wrapper.set("tts_voice_id", null);
|
||||||
case "Memory":
|
break;
|
||||||
defaultTemplate.setMemModelId(modelId);
|
case "MEMORY":
|
||||||
break;
|
wrapper.set("mem_model_id", modelId);
|
||||||
case "Intent":
|
break;
|
||||||
defaultTemplate.setIntentModelId(modelId);
|
case "INTENT":
|
||||||
break;
|
wrapper.set("intent_model_id", modelId);
|
||||||
}
|
break;
|
||||||
this.updateById(defaultTemplate);
|
|
||||||
}
|
}
|
||||||
|
wrapper.ge("sort", 0);
|
||||||
|
update(wrapper);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
import org.springframework.context.annotation.DependsOn;
|
import org.springframework.context.annotation.DependsOn;
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
import xiaozhi.modules.config.service.ConfigService;
|
import xiaozhi.modules.config.service.ConfigService;
|
||||||
import xiaozhi.modules.sys.service.SysParamsService;
|
import xiaozhi.modules.sys.service.SysParamsService;
|
||||||
|
|
||||||
@@ -18,8 +21,20 @@ public class SystemInitConfig {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private ConfigService configService;
|
private ConfigService configService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RedisUtils redisUtils;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() {
|
public void init() {
|
||||||
|
// 检查版本号
|
||||||
|
String redisVersion = (String) redisUtils.get(RedisKeys.getVersionKey());
|
||||||
|
if (!Constant.VERSION.equals(redisVersion)) {
|
||||||
|
// 如果版本不一致,清空Redis
|
||||||
|
redisUtils.emptyAll();
|
||||||
|
// 存储新版本号
|
||||||
|
redisUtils.set(RedisKeys.getVersionKey(), Constant.VERSION);
|
||||||
|
}
|
||||||
|
|
||||||
sysParamsService.initServerSecret();
|
sysParamsService.initServerSecret();
|
||||||
configService.getConfig(false);
|
configService.getConfig(false);
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-2
@@ -9,6 +9,7 @@ import org.apache.commons.lang3.StringUtils;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
import xiaozhi.common.exception.RenException;
|
import xiaozhi.common.exception.RenException;
|
||||||
import xiaozhi.common.redis.RedisKeys;
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
import xiaozhi.common.redis.RedisUtils;
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
@@ -83,8 +84,14 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
// 根据MAC地址查找设备
|
// 根据MAC地址查找设备
|
||||||
DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress);
|
DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress);
|
||||||
if (device == null) {
|
if (device == null) {
|
||||||
throw new RenException("设备未找到");
|
// 如果设备,去redis里看看有没有需要连接的设备
|
||||||
|
String cachedCode = deviceService.geCodeByDeviceId(macAddress);
|
||||||
|
if (StringUtils.isNotBlank(cachedCode)) {
|
||||||
|
throw new RenException(ErrorCode.OTA_DEVICE_NEED_BIND, cachedCode);
|
||||||
|
}
|
||||||
|
throw new RenException(ErrorCode.OTA_DEVICE_NOT_FOUND, "not found device");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取智能体信息
|
// 获取智能体信息
|
||||||
AgentEntity agent = agentService.getAgentById(device.getAgentId());
|
AgentEntity agent = agentService.getAgentById(device.getAgentId());
|
||||||
if (agent == null) {
|
if (agent == null) {
|
||||||
@@ -98,7 +105,9 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
}
|
}
|
||||||
// 构建返回数据
|
// 构建返回数据
|
||||||
Map<String, Object> result = new HashMap<>();
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
// 获取单台设备每天最多输出字数
|
||||||
|
String deviceMaxOutputSize = sysParamsService.getValue("device_max_output_size", true);
|
||||||
|
result.put("device_max_output_size", deviceMaxOutputSize);
|
||||||
// 如果客户端已实例化模型,则不返回
|
// 如果客户端已实例化模型,则不返回
|
||||||
String alreadySelectedVadModelId = (String) selectedModule.get("VAD");
|
String alreadySelectedVadModelId = (String) selectedModule.get("VAD");
|
||||||
if (alreadySelectedVadModelId != null && alreadySelectedVadModelId.equals(agent.getVadModelId())) {
|
if (alreadySelectedVadModelId != null && alreadySelectedVadModelId.equals(agent.getVadModelId())) {
|
||||||
@@ -261,6 +270,12 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
if (intentLLMModelId != null && intentLLMModelId.equals(llmModelId)) {
|
if (intentLLMModelId != null && intentLLMModelId.equals(llmModelId)) {
|
||||||
intentLLMModelId = null;
|
intentLLMModelId = null;
|
||||||
}
|
}
|
||||||
|
} else if ("function_call".equals(map.get("type"))) {
|
||||||
|
String functionStr = (String) map.get("functions");
|
||||||
|
if (StringUtils.isNotBlank(functionStr)) {
|
||||||
|
String[] functions = functionStr.split("\\;");
|
||||||
|
map.put("functions", functions);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 如果是LLM类型,且intentLLMModelId不为空,则添加附加模型
|
// 如果是LLM类型,且intentLLMModelId不为空,则添加附加模型
|
||||||
|
|||||||
@@ -37,23 +37,23 @@ public class OTAController {
|
|||||||
@PostMapping
|
@PostMapping
|
||||||
public ResponseEntity<String> checkOTAVersion(
|
public ResponseEntity<String> checkOTAVersion(
|
||||||
@RequestBody DeviceReportReqDTO deviceReportReqDTO,
|
@RequestBody DeviceReportReqDTO deviceReportReqDTO,
|
||||||
|
|
||||||
@Parameter(name = "Device-Id", description = "设备唯一标识", required = true, in = ParameterIn.HEADER) @RequestHeader("Device-Id") String deviceId,
|
@Parameter(name = "Device-Id", description = "设备唯一标识", required = true, in = ParameterIn.HEADER) @RequestHeader("Device-Id") String deviceId,
|
||||||
|
@Parameter(name = "Client-Id", description = "客户端标识", required = false, in = ParameterIn.HEADER) @RequestHeader(value = "Client-Id", required = false) String clientId) {
|
||||||
@Parameter(name = "Client-Id", description = "客户端标识", required = true, in = ParameterIn.HEADER) @RequestHeader("Client-Id") String clientId) {
|
if (StringUtils.isBlank(deviceId)) {
|
||||||
if (StringUtils.isAnyBlank(deviceId, clientId)) {
|
|
||||||
return createResponse(DeviceReportRespDTO.createError("Device ID is required"));
|
return createResponse(DeviceReportRespDTO.createError("Device ID is required"));
|
||||||
}
|
}
|
||||||
|
if (StringUtils.isBlank(clientId)) {
|
||||||
|
clientId = deviceId;
|
||||||
|
}
|
||||||
String macAddress = deviceReportReqDTO.getMacAddress();
|
String macAddress = deviceReportReqDTO.getMacAddress();
|
||||||
boolean macAddressValid = NetworkUtil.isMacAddressValid(macAddress);
|
boolean macAddressValid = NetworkUtil.isMacAddressValid(macAddress);
|
||||||
// 设备Id和Mac地址应是一致的, 并且必须需要application字段
|
// 设备Id和Mac地址应是一致的, 并且必须需要application字段
|
||||||
if (!deviceId.equals(macAddress) || !macAddressValid || deviceReportReqDTO.getApplication() == null) {
|
if (!deviceId.equals(macAddress) || !macAddressValid || deviceReportReqDTO.getApplication() == null) {
|
||||||
return createResponse(DeviceReportRespDTO.createError("Invalid OTA request"));
|
return createResponse(DeviceReportRespDTO.createError("Invalid OTA request"));
|
||||||
}
|
}
|
||||||
return createResponse(deviceService.checkDeviceActive(macAddress, deviceId, clientId, deviceReportReqDTO));
|
return createResponse(deviceService.checkDeviceActive(macAddress, clientId, deviceReportReqDTO));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "获取 OTA 提示信息")
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
public ResponseEntity<String> getOTAPrompt() {
|
public ResponseEntity<String> getOTAPrompt() {
|
||||||
return createResponse(DeviceReportRespDTO.createError("请提交正确的ota参数"));
|
return createResponse(DeviceReportRespDTO.createError("请提交正确的ota参数"));
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ public interface DeviceService {
|
|||||||
/**
|
/**
|
||||||
* 检查设备是否激活
|
* 检查设备是否激活
|
||||||
*/
|
*/
|
||||||
DeviceReportRespDTO checkDeviceActive(String macAddress, String deviceId, String clientId,
|
DeviceReportRespDTO checkDeviceActive(String macAddress, String clientId,
|
||||||
DeviceReportReqDTO deviceReport);
|
DeviceReportReqDTO deviceReport);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -74,4 +74,12 @@ public interface DeviceService {
|
|||||||
* @return 设备信息
|
* @return 设备信息
|
||||||
*/
|
*/
|
||||||
DeviceEntity getDeviceByMacAddress(String macAddress);
|
DeviceEntity getDeviceByMacAddress(String macAddress);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据设备ID获取激活码
|
||||||
|
*
|
||||||
|
* @param deviceId 设备ID
|
||||||
|
* @return 激活码
|
||||||
|
*/
|
||||||
|
String geCodeByDeviceId(String deviceId);
|
||||||
}
|
}
|
||||||
+62
-40
@@ -9,7 +9,6 @@ import java.util.TimeZone;
|
|||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@@ -34,6 +33,7 @@ import xiaozhi.modules.device.entity.DeviceEntity;
|
|||||||
import xiaozhi.modules.device.service.DeviceService;
|
import xiaozhi.modules.device.service.DeviceService;
|
||||||
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
|
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
|
||||||
import xiaozhi.modules.security.user.SecurityUser;
|
import xiaozhi.modules.security.user.SecurityUser;
|
||||||
|
import xiaozhi.modules.sys.service.SysParamsService;
|
||||||
import xiaozhi.modules.sys.service.SysUserUtilService;
|
import xiaozhi.modules.sys.service.SysUserUtilService;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@@ -49,11 +49,11 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
|
|
||||||
// 添加构造函数来初始化 deviceMapper
|
// 添加构造函数来初始化 deviceMapper
|
||||||
public DeviceServiceImpl(DeviceDao deviceDao, SysUserUtilService sysUserUtilService,
|
public DeviceServiceImpl(DeviceDao deviceDao, SysUserUtilService sysUserUtilService,
|
||||||
@Value("${app.fronted-url:http://localhost:8001}") String frontedUrl,
|
SysParamsService sysParamsService,
|
||||||
RedisTemplate<String, Object> redisTemplate) {
|
RedisTemplate<String, Object> redisTemplate) {
|
||||||
this.deviceDao = deviceDao;
|
this.deviceDao = deviceDao;
|
||||||
this.sysUserUtilService = sysUserUtilService;
|
this.sysUserUtilService = sysUserUtilService;
|
||||||
this.frontedUrl = frontedUrl;
|
this.frontedUrl = sysParamsService.getValue(Constant.SERVER_FRONTED_URL, true);
|
||||||
this.redisTemplate = redisTemplate;
|
this.redisTemplate = redisTemplate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DeviceReportRespDTO checkDeviceActive(String macAddress, String deviceId, String clientId,
|
public DeviceReportRespDTO checkDeviceActive(String macAddress, String clientId,
|
||||||
DeviceReportReqDTO deviceReport) {
|
DeviceReportReqDTO deviceReport) {
|
||||||
DeviceReportRespDTO response = new DeviceReportRespDTO();
|
DeviceReportRespDTO response = new DeviceReportRespDTO();
|
||||||
response.setServer_time(buildServerTime());
|
response.setServer_time(buildServerTime());
|
||||||
@@ -132,46 +132,12 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
firmware.setUrl("http://localhost:8002/xiaozhi/ota/download");
|
firmware.setUrl("http://localhost:8002/xiaozhi/ota/download");
|
||||||
response.setFirmware(firmware);
|
response.setFirmware(firmware);
|
||||||
|
|
||||||
DeviceEntity deviceById = getDeviceById(deviceId);
|
DeviceEntity deviceById = getDeviceById(macAddress);
|
||||||
if (deviceById != null) { // 如果设备存在,则更新上次连接时间
|
if (deviceById != null) { // 如果设备存在,则更新上次连接时间
|
||||||
deviceById.setLastConnectedAt(new Date());
|
deviceById.setLastConnectedAt(new Date());
|
||||||
deviceDao.updateById(deviceById);
|
deviceDao.updateById(deviceById);
|
||||||
} else { // 如果设备不存在,则生成激活码
|
} else { // 如果设备不存在,则生成激活码
|
||||||
String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
|
DeviceReportRespDTO.Activation code = buildActivation(macAddress, deviceReport);
|
||||||
String dataKey = String.format("ota:activation:data:%s", safeDeviceId);
|
|
||||||
|
|
||||||
Map<Object, Object> cacheMap = redisTemplate.opsForHash().entries(dataKey);
|
|
||||||
DeviceReportRespDTO.Activation code = new DeviceReportRespDTO.Activation();
|
|
||||||
|
|
||||||
if (cacheMap != null && cacheMap.containsKey("activation_code")) {
|
|
||||||
String cachedCode = (String) cacheMap.get("activation_code");
|
|
||||||
code.setCode(cachedCode);
|
|
||||||
code.setMessage(frontedUrl + "\n" + cachedCode);
|
|
||||||
} else {
|
|
||||||
String newCode = RandomUtil.randomNumbers(6);
|
|
||||||
code.setCode(newCode);
|
|
||||||
code.setMessage(frontedUrl + "\n" + newCode);
|
|
||||||
|
|
||||||
Map<String, Object> dataMap = new HashMap<>();
|
|
||||||
dataMap.put("id", deviceId);
|
|
||||||
dataMap.put("mac_address", macAddress);
|
|
||||||
dataMap.put("board", (deviceReport.getChipModelName() != null) ? deviceReport.getChipModelName()
|
|
||||||
: (deviceReport.getBoard() != null ? deviceReport.getBoard().getType() : "unknown"));
|
|
||||||
dataMap.put("app_version", (deviceReport.getApplication() != null)
|
|
||||||
? deviceReport.getApplication().getVersion()
|
|
||||||
: null);
|
|
||||||
dataMap.put("deviceId", deviceId);
|
|
||||||
dataMap.put("activation_code", newCode);
|
|
||||||
|
|
||||||
// 写入主数据 key
|
|
||||||
redisTemplate.opsForHash().putAll(dataKey, dataMap);
|
|
||||||
redisTemplate.expire(dataKey, 24, TimeUnit.HOURS);
|
|
||||||
|
|
||||||
// 写入反查激活码 key
|
|
||||||
String codeKey = "ota:activation:code:" + newCode;
|
|
||||||
redisTemplate.opsForValue().set(codeKey, deviceId, 24, TimeUnit.HOURS);
|
|
||||||
}
|
|
||||||
|
|
||||||
response.setActivation(code);
|
response.setActivation(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,4 +224,60 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
serverTime.setTimezone_offset(tz.getOffset(System.currentTimeMillis()) / (60 * 1000));
|
serverTime.setTimezone_offset(tz.getOffset(System.currentTimeMillis()) / (60 * 1000));
|
||||||
return serverTime;
|
return serverTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String geCodeByDeviceId(String deviceId) {
|
||||||
|
String dataKey = getDeviceCacheKey(deviceId);
|
||||||
|
|
||||||
|
Map<Object, Object> cacheMap = redisTemplate.opsForHash().entries(dataKey);
|
||||||
|
if (cacheMap != null && cacheMap.containsKey("activation_code")) {
|
||||||
|
String cachedCode = (String) cacheMap.get("activation_code");
|
||||||
|
return cachedCode;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getDeviceCacheKey(String deviceId) {
|
||||||
|
String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
|
||||||
|
String dataKey = String.format("ota:activation:data:%s", safeDeviceId);
|
||||||
|
return dataKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DeviceReportRespDTO.Activation buildActivation(String deviceId, DeviceReportReqDTO deviceReport) {
|
||||||
|
DeviceReportRespDTO.Activation code = new DeviceReportRespDTO.Activation();
|
||||||
|
|
||||||
|
String cachedCode = geCodeByDeviceId(deviceId);
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(cachedCode)) {
|
||||||
|
code.setCode(cachedCode);
|
||||||
|
code.setMessage(frontedUrl + "\n" + cachedCode);
|
||||||
|
} else {
|
||||||
|
String newCode = RandomUtil.randomNumbers(6);
|
||||||
|
code.setCode(newCode);
|
||||||
|
code.setMessage(frontedUrl + "\n" + newCode);
|
||||||
|
|
||||||
|
Map<String, Object> dataMap = new HashMap<>();
|
||||||
|
dataMap.put("id", deviceId);
|
||||||
|
dataMap.put("mac_address", deviceId);
|
||||||
|
|
||||||
|
dataMap.put("board", (deviceReport.getChipModelName() != null) ? deviceReport.getChipModelName()
|
||||||
|
: (deviceReport.getBoard() != null ? deviceReport.getBoard().getType() : "unknown"));
|
||||||
|
dataMap.put("app_version", (deviceReport.getApplication() != null)
|
||||||
|
? deviceReport.getApplication().getVersion()
|
||||||
|
: null);
|
||||||
|
|
||||||
|
dataMap.put("deviceId", deviceId);
|
||||||
|
dataMap.put("activation_code", newCode);
|
||||||
|
|
||||||
|
// 写入主数据 key
|
||||||
|
String dataKey = getDeviceCacheKey(deviceId);
|
||||||
|
redisTemplate.opsForHash().putAll(dataKey, dataMap);
|
||||||
|
redisTemplate.expire(dataKey, 24, TimeUnit.HOURS);
|
||||||
|
|
||||||
|
// 写入反查激活码 key
|
||||||
|
String codeKey = "ota:activation:code:" + newCode;
|
||||||
|
redisTemplate.opsForValue().set(codeKey, deviceId, 24, TimeUnit.HOURS);
|
||||||
|
}
|
||||||
|
return code;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -135,6 +135,7 @@ public class ModelController {
|
|||||||
}
|
}
|
||||||
// 将其他模型设置为非默认
|
// 将其他模型设置为非默认
|
||||||
modelConfigService.setDefaultModel(entity.getModelType(), 0);
|
modelConfigService.setDefaultModel(entity.getModelType(), 0);
|
||||||
|
entity.setIsEnabled(1);
|
||||||
entity.setIsDefault(1);
|
entity.setIsDefault(1);
|
||||||
modelConfigService.updateById(entity);
|
modelConfigService.updateById(entity);
|
||||||
|
|
||||||
|
|||||||
+1
@@ -42,6 +42,7 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
|||||||
List<ModelConfigEntity> entities = modelConfigDao.selectList(
|
List<ModelConfigEntity> entities = modelConfigDao.selectList(
|
||||||
new QueryWrapper<ModelConfigEntity>()
|
new QueryWrapper<ModelConfigEntity>()
|
||||||
.eq("model_type", modelType)
|
.eq("model_type", modelType)
|
||||||
|
.eq("is_enabled", 1)
|
||||||
.like(StringUtils.isNotBlank(modelName), "model_name", "%" + modelName + "%")
|
.like(StringUtils.isNotBlank(modelName), "model_name", "%" + modelName + "%")
|
||||||
.select("id", "model_name"));
|
.select("id", "model_name"));
|
||||||
return ConvertUtils.sourceToTarget(entities, ModelBasicInfoDTO.class);
|
return ConvertUtils.sourceToTarget(entities, ModelBasicInfoDTO.class);
|
||||||
|
|||||||
+6
-1
@@ -15,6 +15,7 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
import xiaozhi.common.exception.ErrorCode;
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
import xiaozhi.common.exception.RenException;
|
import xiaozhi.common.exception.RenException;
|
||||||
import xiaozhi.common.page.TokenDTO;
|
import xiaozhi.common.page.TokenDTO;
|
||||||
@@ -77,6 +78,9 @@ public class LoginController {
|
|||||||
@PostMapping("/register")
|
@PostMapping("/register")
|
||||||
@Operation(summary = "注册")
|
@Operation(summary = "注册")
|
||||||
public Result<Void> register(@RequestBody LoginDTO login) {
|
public Result<Void> register(@RequestBody LoginDTO login) {
|
||||||
|
if (!sysUserService.getAllowUserRegister()) {
|
||||||
|
throw new RenException("当前不允许普通用户注册");
|
||||||
|
}
|
||||||
// 验证是否正确输入验证码
|
// 验证是否正确输入验证码
|
||||||
boolean validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha());
|
boolean validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha());
|
||||||
if (!validate) {
|
if (!validate) {
|
||||||
@@ -118,7 +122,8 @@ public class LoginController {
|
|||||||
@Operation(summary = "公共配置")
|
@Operation(summary = "公共配置")
|
||||||
public Result<Map<String, Object>> pubConfig() {
|
public Result<Map<String, Object>> pubConfig() {
|
||||||
Map<String, Object> config = new HashMap<>();
|
Map<String, Object> config = new HashMap<>();
|
||||||
config.put("version", "0.3.3");
|
config.put("version", Constant.VERSION);
|
||||||
|
config.put("allowUserRegister", sysUserService.getAllowUserRegister());
|
||||||
return new Result<Map<String, Object>>().ok(config);
|
return new Result<Map<String, Object>>().ok(config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+5
-3
@@ -27,6 +27,7 @@ import xiaozhi.common.validator.ValidatorUtils;
|
|||||||
import xiaozhi.common.validator.group.AddGroup;
|
import xiaozhi.common.validator.group.AddGroup;
|
||||||
import xiaozhi.common.validator.group.DefaultGroup;
|
import xiaozhi.common.validator.group.DefaultGroup;
|
||||||
import xiaozhi.common.validator.group.UpdateGroup;
|
import xiaozhi.common.validator.group.UpdateGroup;
|
||||||
|
import xiaozhi.modules.config.service.ConfigService;
|
||||||
import xiaozhi.modules.sys.dto.SysParamsDTO;
|
import xiaozhi.modules.sys.dto.SysParamsDTO;
|
||||||
import xiaozhi.modules.sys.service.SysParamsService;
|
import xiaozhi.modules.sys.service.SysParamsService;
|
||||||
|
|
||||||
@@ -42,6 +43,7 @@ import xiaozhi.modules.sys.service.SysParamsService;
|
|||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class SysParamsController {
|
public class SysParamsController {
|
||||||
private final SysParamsService sysParamsService;
|
private final SysParamsService sysParamsService;
|
||||||
|
private final ConfigService configService;
|
||||||
|
|
||||||
@GetMapping("page")
|
@GetMapping("page")
|
||||||
@Operation(summary = "分页")
|
@Operation(summary = "分页")
|
||||||
@@ -77,7 +79,7 @@ public class SysParamsController {
|
|||||||
ValidatorUtils.validateEntity(dto, AddGroup.class, DefaultGroup.class);
|
ValidatorUtils.validateEntity(dto, AddGroup.class, DefaultGroup.class);
|
||||||
|
|
||||||
sysParamsService.save(dto);
|
sysParamsService.save(dto);
|
||||||
|
configService.getConfig(false);
|
||||||
return new Result<Void>();
|
return new Result<Void>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +92,7 @@ public class SysParamsController {
|
|||||||
ValidatorUtils.validateEntity(dto, UpdateGroup.class, DefaultGroup.class);
|
ValidatorUtils.validateEntity(dto, UpdateGroup.class, DefaultGroup.class);
|
||||||
|
|
||||||
sysParamsService.update(dto);
|
sysParamsService.update(dto);
|
||||||
|
configService.getConfig(false);
|
||||||
return new Result<Void>();
|
return new Result<Void>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +105,7 @@ public class SysParamsController {
|
|||||||
AssertUtils.isArrayEmpty(ids, "id");
|
AssertUtils.isArrayEmpty(ids, "id");
|
||||||
|
|
||||||
sysParamsService.delete(ids);
|
sysParamsService.delete(ids);
|
||||||
|
configService.getConfig(false);
|
||||||
return new Result<Void>();
|
return new Result<Void>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,4 +65,11 @@ public interface SysUserService extends BaseService<SysUserEntity> {
|
|||||||
* @param userIds 用户ID数组
|
* @param userIds 用户ID数组
|
||||||
*/
|
*/
|
||||||
void changeStatus(Integer status, String[] userIds);
|
void changeStatus(Integer status, String[] userIds);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取是否允许用户注册
|
||||||
|
*
|
||||||
|
* @return 是否允许用户注册
|
||||||
|
*/
|
||||||
|
boolean getAllowUserRegister();
|
||||||
}
|
}
|
||||||
|
|||||||
+16
@@ -30,6 +30,7 @@ import xiaozhi.modules.sys.dto.PasswordDTO;
|
|||||||
import xiaozhi.modules.sys.dto.SysUserDTO;
|
import xiaozhi.modules.sys.dto.SysUserDTO;
|
||||||
import xiaozhi.modules.sys.entity.SysUserEntity;
|
import xiaozhi.modules.sys.entity.SysUserEntity;
|
||||||
import xiaozhi.modules.sys.enums.SuperAdminEnum;
|
import xiaozhi.modules.sys.enums.SuperAdminEnum;
|
||||||
|
import xiaozhi.modules.sys.service.SysParamsService;
|
||||||
import xiaozhi.modules.sys.service.SysUserService;
|
import xiaozhi.modules.sys.service.SysUserService;
|
||||||
import xiaozhi.modules.sys.vo.AdminPageUserVO;
|
import xiaozhi.modules.sys.vo.AdminPageUserVO;
|
||||||
|
|
||||||
@@ -45,6 +46,8 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
|
|||||||
|
|
||||||
private final AgentService agentService;
|
private final AgentService agentService;
|
||||||
|
|
||||||
|
private final SysParamsService sysParamsService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SysUserDTO getByUsername(String username) {
|
public SysUserDTO getByUsername(String username) {
|
||||||
QueryWrapper<SysUserEntity> queryWrapper = new QueryWrapper<>();
|
QueryWrapper<SysUserEntity> queryWrapper = new QueryWrapper<>();
|
||||||
@@ -205,4 +208,17 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
|
|||||||
updateById(entity);
|
updateById(entity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean getAllowUserRegister() {
|
||||||
|
String allowUserRegister = sysParamsService.getValue(Constant.SERVER_ALLOW_USER_REGISTER, true);
|
||||||
|
if (allowUserRegister.equals("true")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Long userCount = baseDao.selectCount(new QueryWrapper<SysUserEntity>());
|
||||||
|
if (userCount == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,3 @@ mybatis-plus:
|
|||||||
prefix:
|
prefix:
|
||||||
blobType: BLOB
|
blobType: BLOB
|
||||||
boolValue: TRUE
|
boolValue: TRUE
|
||||||
|
|
||||||
app:
|
|
||||||
fronted-url: http://localhost:8001
|
|
||||||
+10
@@ -16,3 +16,13 @@ INSERT INTO `ai_tts_voice` VALUES
|
|||||||
('TTS_EdgeTTS0009', 'TTS_EdgeTTS', 'EdgeTTS女声-香港海佳', 'zh-HK-HiuGaaiNeural', '粤语', 'General', 'Friendly, Positive', 1, NULL, NULL, NULL, NULL),
|
('TTS_EdgeTTS0009', 'TTS_EdgeTTS', 'EdgeTTS女声-香港海佳', 'zh-HK-HiuGaaiNeural', '粤语', 'General', 'Friendly, Positive', 1, NULL, NULL, NULL, NULL),
|
||||||
('TTS_EdgeTTS0010', 'TTS_EdgeTTS', 'EdgeTTS女声-香港海曼', 'zh-HK-HiuMaanNeural', '粤语', 'General', 'Friendly, Positive', 1, NULL, NULL, NULL, NULL),
|
('TTS_EdgeTTS0010', 'TTS_EdgeTTS', 'EdgeTTS女声-香港海曼', 'zh-HK-HiuMaanNeural', '粤语', 'General', 'Friendly, Positive', 1, NULL, NULL, NULL, NULL),
|
||||||
('TTS_EdgeTTS0011', 'TTS_EdgeTTS', 'EdgeTTS男声-香港万龙', 'zh-HK-WanLungNeural', '粤语', 'General', 'Friendly, Positive', 1, NULL, NULL, NULL, NULL);
|
('TTS_EdgeTTS0011', 'TTS_EdgeTTS', 'EdgeTTS男声-香港万龙', 'zh-HK-WanLungNeural', '粤语', 'General', 'Friendly, Positive', 1, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- 增加是否允许用户注册参数
|
||||||
|
delete from `sys_params` where id in (103,104);
|
||||||
|
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (103, 'server.allow_user_register', 'false', 'boolean', 1, '是否运行管理员以外的人注册');
|
||||||
|
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (104, 'server.fronted_url', 'http://xiaozhi.server.com', 'string', 1, '下发六位验证码时显示的控制面板地址');
|
||||||
|
|
||||||
|
-- 修正CosyVoiceSiliconflow音色
|
||||||
|
delete from `ai_tts_voice` where tts_model_id = 'TTS_CosyVoiceSiliconflow';
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_CosyVoiceSiliconflow0001', 'TTS_CosyVoiceSiliconflow', 'CosyVoice男声', 'FunAudioLLM/CosyVoice2-0.5B:alex', '中文', 'https://example.com/cosyvoice/alex.mp3', NULL, 6, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_CosyVoiceSiliconflow0002', 'TTS_CosyVoiceSiliconflow', 'CosyVoice女声', 'FunAudioLLM/CosyVoice2-0.5B:bella', '中文', 'https://example.com/cosyvoice/bella.mp3', NULL, 6, NULL, NULL, NULL, NULL);
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
delete from `ai_model_config` where id = 'Intent_function_call';
|
||||||
|
INSERT INTO `ai_model_config` VALUES ('Intent_function_call', 'Intent', 'function_call', '函数调用意图识别', 0, 1, '{\"type\": \"function_call\", \"functions\": \"change_role;get_weather;get_news;play_music\"}', NULL, NULL, 3, NULL, NULL, NULL, NULL);
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
-- 调整意图识别配置
|
||||||
|
delete from `ai_model_config` where id = 'Intent_function_call';
|
||||||
|
INSERT INTO `ai_model_config` VALUES ('Intent_function_call', 'Intent', 'function_call', '函数调用意图识别', 0, 1, '{\"type\": \"function_call\", \"functions\": \"change_role;get_weather;get_news;play_music\"}', NULL, NULL, 3, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- 增加单台设备每天最多聊天句数
|
||||||
|
delete from `sys_params` where id = 105;
|
||||||
|
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (105, 'device_max_output_size', '0', 'number', 1, '单台设备每天最多输出字数,0表示不限制');
|
||||||
@@ -52,9 +52,16 @@ databaseChangeLog:
|
|||||||
encoding: utf8
|
encoding: utf8
|
||||||
path: classpath:db/changelog/202504112058.sql
|
path: classpath:db/changelog/202504112058.sql
|
||||||
- changeSet:
|
- changeSet:
|
||||||
id: 202504131543
|
id: 202504151206
|
||||||
author: John
|
author: John
|
||||||
changes:
|
changes:
|
||||||
- sqlFile:
|
- sqlFile:
|
||||||
encoding: utf8
|
encoding: utf8
|
||||||
path: classpath:db/changelog/202504131543.sql
|
path: classpath:db/changelog/202504151206.sql
|
||||||
|
- changeSet:
|
||||||
|
id: 202504181536
|
||||||
|
author: John
|
||||||
|
changes:
|
||||||
|
- sqlFile:
|
||||||
|
encoding: utf8
|
||||||
|
path: classpath:db/changelog/202504181536.sql
|
||||||
|
|||||||
@@ -41,3 +41,6 @@
|
|||||||
10038=\u53C2\u6570\u503C\u5FC5\u987B\u662Ftrue\u6216false
|
10038=\u53C2\u6570\u503C\u5FC5\u987B\u662Ftrue\u6216false
|
||||||
10039=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u6570\u7EC4\u683C\u5F0F
|
10039=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u6570\u7EC4\u683C\u5F0F
|
||||||
10040=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u683C\u5F0F
|
10040=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u683C\u5F0F
|
||||||
|
|
||||||
|
10041=\u8BBE\u5907\u672A\u627E\u5230
|
||||||
|
10042={0}
|
||||||
|
|||||||
@@ -41,3 +41,6 @@
|
|||||||
10038=Parameter value must be true or false
|
10038=Parameter value must be true or false
|
||||||
10039=Parameter value must be a valid JSON array format
|
10039=Parameter value must be a valid JSON array format
|
||||||
10040=Parameter value must be a valid JSON format
|
10040=Parameter value must be a valid JSON format
|
||||||
|
|
||||||
|
10041=Device not found
|
||||||
|
10042={0}
|
||||||
@@ -41,3 +41,6 @@
|
|||||||
10038=\u53C2\u6570\u503C\u5FC5\u987B\u662Ftrue\u6216false
|
10038=\u53C2\u6570\u503C\u5FC5\u987B\u662Ftrue\u6216false
|
||||||
10039=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u6570\u7EC4\u683C\u5F0F
|
10039=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u6570\u7EC4\u683C\u5F0F
|
||||||
10040=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u683C\u5F0F
|
10040=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u683C\u5F0F
|
||||||
|
|
||||||
|
10041=\u8BBE\u5907\u672A\u627E\u5230
|
||||||
|
10042={0}
|
||||||
@@ -41,3 +41,6 @@
|
|||||||
10038=\u53C3\u6578\u503C\u5FC5\u9808\u662Ftrue\u6216false
|
10038=\u53C3\u6578\u503C\u5FC5\u9808\u662Ftrue\u6216false
|
||||||
10039=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684JSON\u6578\u7D44\u683C\u5F0F
|
10039=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684JSON\u6578\u7D44\u683C\u5F0F
|
||||||
10040=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684JSON\u683C\u5F0F
|
10040=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684JSON\u683C\u5F0F
|
||||||
|
|
||||||
|
10041=\u8A2D\u5099\u672A\u627E\u5230
|
||||||
|
10042={0}
|
||||||
@@ -33,3 +33,5 @@ timbre.ttsModelId.require=\u97F3\u8272\u7684tts\u4E3B\u952E\u4E0D\u53EF\u4EE5\u4
|
|||||||
timbre.ttsVoice.require=\u97F3\u8272\u7684\u7F16\u7801\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
timbre.ttsVoice.require=\u97F3\u8272\u7684\u7F16\u7801\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||||
timbre.voiceDemo.require=\u97F3\u8272\u7684\u64AD\u653EDemo\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
timbre.voiceDemo.require=\u97F3\u8272\u7684\u64AD\u653EDemo\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||||
|
|
||||||
|
ota.device.not.found=\u8BBE\u5907\u672A\u627E\u5230
|
||||||
|
ota.device.need.bind={0}
|
||||||
|
|||||||
@@ -32,3 +32,5 @@ timbre.ttsModelId.require=The TTS model ID of the timbre cannot be empty
|
|||||||
timbre.ttsVoice.require=The TTS voice of the timbre cannot be empty
|
timbre.ttsVoice.require=The TTS voice of the timbre cannot be empty
|
||||||
timbre.voiceDemo.require=The voice demo of the timbre cannot be empty
|
timbre.voiceDemo.require=The voice demo of the timbre cannot be empty
|
||||||
|
|
||||||
|
ota.device.not.found=Device not found
|
||||||
|
ota.device.need.bind={0}
|
||||||
@@ -32,3 +32,5 @@ timbre.ttsModelId.require=\u97F3\u8272\u7684tts\u4E3B\u9375\u4E0D\u53EF\u4EE5\u7
|
|||||||
timbre.ttsVoice.require=\u97F3\u8272\u7684\u7DE8\u78BC\u4E0D\u53EF\u4EE5\u70BA\u7A7A
|
timbre.ttsVoice.require=\u97F3\u8272\u7684\u7DE8\u78BC\u4E0D\u53EF\u4EE5\u70BA\u7A7A
|
||||||
timbre.voiceDemo.require=\u97F3\u8272\u7684\u64AD\u653E\u5730\u5740\u4E0D\u53EF\u4EE5\u70BA\u7A7A
|
timbre.voiceDemo.require=\u97F3\u8272\u7684\u64AD\u653E\u5730\u5740\u4E0D\u53EF\u4EE5\u70BA\u7A7A
|
||||||
|
|
||||||
|
ota.device.not.found=\u8A2D\u5099\u672A\u627E\u5230
|
||||||
|
ota.device.need.bind={0}
|
||||||
@@ -31,3 +31,6 @@ timbre.remark.require=\u97F3\u8272\u7684\u5907\u6CE8\u4E0D\u53EF\u4EE5\u4E3A\u7A
|
|||||||
timbre.ttsModelId.require=\u97F3\u8272\u7684tts\u4E3B\u952E\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
timbre.ttsModelId.require=\u97F3\u8272\u7684tts\u4E3B\u952E\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||||
timbre.ttsVoice.require=\u97F3\u8272\u7684\u7F16\u7801\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
timbre.ttsVoice.require=\u97F3\u8272\u7684\u7F16\u7801\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||||
timbre.voiceDemo.require=\u97F3\u8272\u7684\u64AD\u653EDemo\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
timbre.voiceDemo.require=\u97F3\u8272\u7684\u64AD\u653EDemo\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||||
|
|
||||||
|
ota.device.not.found=\u8A2D\u5099\u672A\u627E\u5230
|
||||||
|
ota.device.need.bind={0}
|
||||||
@@ -1 +1,3 @@
|
|||||||
VUE_APP_API_BASE_URL=/xiaozhi
|
VUE_APP_API_BASE_URL=/xiaozhi
|
||||||
|
# 是否开启CDN
|
||||||
|
VUE_APP_USE_CDN=false
|
||||||
Generated
+3248
-10
File diff suppressed because it is too large
Load Diff
@@ -31,7 +31,8 @@
|
|||||||
"sass": "^1.32.7",
|
"sass": "^1.32.7",
|
||||||
"sass-loader": "^12.0.0",
|
"sass-loader": "^12.0.0",
|
||||||
"vue-template-compiler": "^2.6.14",
|
"vue-template-compiler": "^2.6.14",
|
||||||
"webpack-bundle-analyzer": "^4.10.2"
|
"webpack-bundle-analyzer": "^4.10.2",
|
||||||
|
"workbox-webpack-plugin": "^7.3.0"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"> 1%",
|
"> 1%",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<title>
|
<title>
|
||||||
<%= process.env.VUE_APP_TITLE %>
|
<%= process.env.VUE_APP_TITLE %>
|
||||||
</title>
|
</title>
|
||||||
<% if (htmlWebpackPlugin.options.cdn) { %>
|
<% if (htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) { %>
|
||||||
<% for (var i in htmlWebpackPlugin.options.cdn.css) { %>
|
<% for (var i in htmlWebpackPlugin.options.cdn.css) { %>
|
||||||
<link rel="stylesheet" href="<%= htmlWebpackPlugin.options.cdn.css[i] %>">
|
<link rel="stylesheet" href="<%= htmlWebpackPlugin.options.cdn.css[i] %>">
|
||||||
<% } %>
|
<% } %>
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
</noscript>
|
</noscript>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<!-- built files will be auto injected -->
|
<!-- built files will be auto injected -->
|
||||||
<% if (htmlWebpackPlugin.options.cdn) { %>
|
<% if (htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %>
|
||||||
<% for (var i in htmlWebpackPlugin.options.cdn.js) { %>
|
<% for (var i in htmlWebpackPlugin.options.cdn.js) { %>
|
||||||
<script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
|
<script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>离线模式 - 小智控制台</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
|
||||||
|
max-width: 80%;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: #409EFF;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
background-color: #409EFF;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 0.8rem 1.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
.btn:hover {
|
||||||
|
background-color: #337ecc;
|
||||||
|
}
|
||||||
|
.icon {
|
||||||
|
font-size: 4rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: #409EFF;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="icon">📶</div>
|
||||||
|
<h1>您当前处于离线模式</h1>
|
||||||
|
<p>看起来您的网络连接有问题,无法连接到小智控制台服务器。</p>
|
||||||
|
<p>部分已缓存的内容和静态资源可能仍然可用,但功能可能受到限制。</p>
|
||||||
|
<button class="btn" onclick="window.location.reload()">重新尝试连接</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 检测网络状态变化
|
||||||
|
window.addEventListener('online', () => {
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<router-view />
|
<router-view />
|
||||||
|
<cache-viewer v-if="isCDNEnabled" :visible.sync="showCacheViewer" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -45,4 +46,118 @@ nav {
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
|
import CacheViewer from '@/components/CacheViewer.vue';
|
||||||
|
import { logCacheStatus } from '@/utils/cacheViewer';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
CacheViewer
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showCacheViewer: false,
|
||||||
|
isCDNEnabled: process.env.VUE_APP_USE_CDN === 'true'
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
// 只有在启用CDN时才添加相关事件和功能
|
||||||
|
if (this.isCDNEnabled) {
|
||||||
|
// 添加全局快捷键Alt+C用于显示缓存查看器
|
||||||
|
document.addEventListener('keydown', this.handleKeyDown);
|
||||||
|
|
||||||
|
// 在全局对象上添加缓存检查方法,便于调试
|
||||||
|
window.checkCDNCacheStatus = () => {
|
||||||
|
this.showCacheViewer = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 在控制台输出提示信息
|
||||||
|
console.info(
|
||||||
|
'%c[小智服务] CDN缓存检查工具已加载',
|
||||||
|
'color: #409EFF; font-weight: bold;'
|
||||||
|
);
|
||||||
|
console.info(
|
||||||
|
'按下 Alt+C 组合键或在控制台运行 checkCDNCacheStatus() 可以查看CDN缓存状态'
|
||||||
|
);
|
||||||
|
|
||||||
|
// 检查Service Worker状态
|
||||||
|
this.checkServiceWorkerStatus();
|
||||||
|
} else {
|
||||||
|
console.info(
|
||||||
|
'%c[小智服务] CDN模式已禁用,使用本地打包资源',
|
||||||
|
'color: #67C23A; font-weight: bold;'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
// 只有在启用CDN时才需要移除事件监听
|
||||||
|
if (this.isCDNEnabled) {
|
||||||
|
document.removeEventListener('keydown', this.handleKeyDown);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleKeyDown(e) {
|
||||||
|
// Alt+C 快捷键
|
||||||
|
if (e.altKey && e.key === 'c') {
|
||||||
|
this.showCacheViewer = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async checkServiceWorkerStatus() {
|
||||||
|
// 检查Service Worker是否已注册
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
try {
|
||||||
|
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||||
|
if (registrations.length > 0) {
|
||||||
|
console.info(
|
||||||
|
'%c[小智服务] Service Worker已注册',
|
||||||
|
'color: #67C23A; font-weight: bold;'
|
||||||
|
);
|
||||||
|
|
||||||
|
// 输出缓存状态到控制台
|
||||||
|
setTimeout(async () => {
|
||||||
|
const hasCaches = await logCacheStatus();
|
||||||
|
if (!hasCaches) {
|
||||||
|
console.info(
|
||||||
|
'%c[小智服务] 还未检测到缓存,请刷新页面或等待缓存建立',
|
||||||
|
'color: #E6A23C; font-weight: bold;'
|
||||||
|
);
|
||||||
|
|
||||||
|
// 开发环境下提供额外提示
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.info(
|
||||||
|
'%c[小智服务] 在开发环境中,Service Worker可能无法正常初始化缓存',
|
||||||
|
'color: #E6A23C; font-weight: bold;'
|
||||||
|
);
|
||||||
|
console.info('请尝试以下方法检查Service Worker是否生效:');
|
||||||
|
console.info('1. 在开发者工具的Application/Application标签页中查看Service Worker状态');
|
||||||
|
console.info('2. 在开发者工具的Application/Cache/Cache Storage中查看缓存内容');
|
||||||
|
console.info('3. 使用生产构建(npm run build)并通过HTTP服务器访问以测试完整功能');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
} else {
|
||||||
|
console.info(
|
||||||
|
'%c[小智服务] Service Worker未注册,CDN资源可能无法缓存',
|
||||||
|
'color: #F56C6C; font-weight: bold;'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.info(
|
||||||
|
'%c[小智服务] 在开发环境中,这是正常现象',
|
||||||
|
'color: #E6A23C; font-weight: bold;'
|
||||||
|
);
|
||||||
|
console.info('Service Worker通常只在生产环境中生效');
|
||||||
|
console.info('要测试Service Worker功能:');
|
||||||
|
console.info('1. 运行npm run build构建生产版本');
|
||||||
|
console.info('2. 通过HTTP服务器访问构建后的页面');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查Service Worker状态失败:', error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('当前浏览器不支持Service Worker,CDN资源缓存功能不可用');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog :visible.sync="visible" width="400px" center>
|
<el-dialog :visible="visible" @close="handleClose" width="400px" center>
|
||||||
<div
|
<div
|
||||||
style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
|
style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
|
||||||
<div
|
<div
|
||||||
@@ -79,7 +79,10 @@ export default {
|
|||||||
cancel() {
|
cancel() {
|
||||||
this.$emit('update:visible', false)
|
this.$emit('update:visible', false)
|
||||||
this.deviceCode = ""
|
this.deviceCode = ""
|
||||||
}
|
},
|
||||||
|
handleClose() {
|
||||||
|
this.$emit('update:visible', false);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog :visible="dialogVisible" @update:visible="handleVisibleChange" width="975px" center custom-class="custom-dialog" :show-close="false"
|
<el-dialog :visible="dialogVisible" @update:visible="handleVisibleChange" width="975px" center
|
||||||
class="center-dialog">
|
custom-class="custom-dialog" :show-close="false" class="center-dialog">
|
||||||
<div style="margin: 0 18px; text-align: left; padding: 10px; border-radius: 10px;">
|
<div style="margin: 0 18px; text-align: left; padding: 10px; border-radius: 10px;">
|
||||||
<div style="font-size: 30px; color: #3d4566; margin-top: -10px; margin-bottom: 10px; text-align: center;">
|
<div style="font-size: 30px; color: #3d4566; margin-top: -10px; margin-bottom: 10px; text-align: center;">
|
||||||
添加模型
|
添加模型
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<button class="custom-close-btn" @click="handleClose">
|
<button class="custom-close-btn" @click="handleClose">
|
||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
@@ -19,7 +18,7 @@
|
|||||||
<span style="margin-right: 8px;">是否启用</span>
|
<span style="margin-right: 8px;">是否启用</span>
|
||||||
<el-switch v-model="formData.isEnabled" class="custom-switch"></el-switch>
|
<el-switch v-model="formData.isEnabled" class="custom-switch"></el-switch>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; align-items: center;">
|
<div style="display: none; align-items: center;">
|
||||||
<span style="margin-right: 8px;">设为默认</span>
|
<span style="margin-right: 8px;">设为默认</span>
|
||||||
<el-switch v-model="formData.isDefault" class="custom-switch"></el-switch>
|
<el-switch v-model="formData.isDefault" class="custom-switch"></el-switch>
|
||||||
</div>
|
</div>
|
||||||
@@ -45,7 +44,7 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="排序号" prop="sortOrder" style="flex: 1;">
|
<el-form-item label="排序号" prop="sortOrder" style="flex: 1;">
|
||||||
<el-input v-model="formData.sort" placeholder="请输入排序号" class="custom-input-bg"></el-input>
|
<el-input v-model="formData.sort" type="number" placeholder="请输入排序号" class="custom-input-bg"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -66,18 +65,10 @@
|
|||||||
<el-form :model="formData.configJson" label-width="auto" label-position="left" class="custom-form">
|
<el-form :model="formData.configJson" label-width="auto" label-position="left" class="custom-form">
|
||||||
<template v-for="(row, rowIndex) in chunkedCallInfoFields">
|
<template v-for="(row, rowIndex) in chunkedCallInfoFields">
|
||||||
<div :key="rowIndex" style="display: flex; gap: 20px; margin-bottom: 0;">
|
<div :key="rowIndex" style="display: flex; gap: 20px; margin-bottom: 0;">
|
||||||
<el-form-item
|
<el-form-item v-for="field in row" :key="field.prop" :label="field.label" :prop="field.prop"
|
||||||
v-for="field in row"
|
|
||||||
:key="field.prop"
|
|
||||||
:label="field.label"
|
|
||||||
:prop="field.prop"
|
|
||||||
style="flex: 1;">
|
style="flex: 1;">
|
||||||
<el-input
|
<el-input v-model="formData.configJson[field.prop]" :placeholder="field.placeholder"
|
||||||
v-model="formData.configJson[field.prop]"
|
:type="field.type || 'text'" class="custom-input-bg" :show-password="field.type === 'password'">
|
||||||
:placeholder="field.placeholder"
|
|
||||||
:type="field.type || 'text'"
|
|
||||||
class="custom-input-bg"
|
|
||||||
:show-password="field.type === 'password'">
|
|
||||||
</el-input>
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
@@ -124,7 +115,7 @@ export default {
|
|||||||
watch: {
|
watch: {
|
||||||
visible(val) {
|
visible(val) {
|
||||||
this.dialogVisible = val;
|
this.dialogVisible = val;
|
||||||
if(val) {
|
if (val) {
|
||||||
this.initConfigJson();
|
this.initConfigJson();
|
||||||
} else {
|
} else {
|
||||||
this.resetForm();
|
this.resetForm();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog :visible.sync="visible" width="400px" center @open="handleOpen">
|
<el-dialog :visible="visible" @close="handleClose" width="400px" center @open="handleOpen">
|
||||||
<div
|
<div
|
||||||
style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
|
style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
|
||||||
<div
|
<div
|
||||||
@@ -66,7 +66,10 @@ export default {
|
|||||||
cancel() {
|
cancel() {
|
||||||
this.$emit('update:visible', false)
|
this.$emit('update:visible', false)
|
||||||
this.wisdomBodyName = ""
|
this.wisdomBodyName = ""
|
||||||
}
|
},
|
||||||
|
handleClose() {
|
||||||
|
this.$emit('update:visible', false);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,207 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
title="CDN资源缓存状态"
|
||||||
|
:visible.sync="visible"
|
||||||
|
width="70%"
|
||||||
|
:before-close="handleClose"
|
||||||
|
>
|
||||||
|
<div v-if="isLoading" class="loading-container">
|
||||||
|
<p>正在加载缓存信息...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<div v-if="!cacheAvailable" class="no-cache-message">
|
||||||
|
<i class="el-icon-warning-outline"></i>
|
||||||
|
<p>您的浏览器不支持Cache API或Service Worker未安装</p>
|
||||||
|
<el-button type="primary" @click="refreshPage">刷新页面</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<el-alert
|
||||||
|
v-if="cacheData.totalCached === 0"
|
||||||
|
title="未发现缓存的CDN资源"
|
||||||
|
type="warning"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
>
|
||||||
|
<p>Service Worker可能尚未完成初始化或缓存尚未建立。请刷新页面或等待一会后再试。</p>
|
||||||
|
</el-alert>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<el-alert
|
||||||
|
title="CDN资源缓存状态"
|
||||||
|
type="success"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
>
|
||||||
|
共发现 {{ cacheData.totalCached }} 个缓存资源
|
||||||
|
</el-alert>
|
||||||
|
|
||||||
|
<h3>JavaScript 资源 ({{ cacheData.js.length }})</h3>
|
||||||
|
<el-table :data="cacheData.js" stripe style="width: 100%">
|
||||||
|
<el-table-column prop="url" label="URL" width="auto" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="cached" label="状态" width="100">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag type="success" v-if="scope.row.cached">已缓存</el-tag>
|
||||||
|
<el-tag type="danger" v-else>未缓存</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<h3>CSS 资源 ({{ cacheData.css.length }})</h3>
|
||||||
|
<el-table :data="cacheData.css" stripe style="width: 100%">
|
||||||
|
<el-table-column prop="url" label="URL" width="auto" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="cached" label="状态" width="100">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag type="success" v-if="scope.row.cached">已缓存</el-tag>
|
||||||
|
<el-tag type="danger" v-else>未缓存</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="handleClose">关闭</el-button>
|
||||||
|
<el-button type="primary" @click="refreshCache">刷新缓存状态</el-button>
|
||||||
|
<el-button type="danger" @click="clearCache">清除缓存</el-button>
|
||||||
|
</span>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
getCacheNames,
|
||||||
|
checkCdnCacheStatus,
|
||||||
|
clearAllCaches,
|
||||||
|
logCacheStatus
|
||||||
|
} from '../utils/cacheViewer';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CacheViewer',
|
||||||
|
props: {
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isLoading: true,
|
||||||
|
cacheAvailable: false,
|
||||||
|
cacheData: {
|
||||||
|
css: [],
|
||||||
|
js: [],
|
||||||
|
totalCached: 0,
|
||||||
|
totalNotCached: 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
visible(newVal) {
|
||||||
|
if (newVal) {
|
||||||
|
this.loadCacheData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async loadCacheData() {
|
||||||
|
this.isLoading = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 先检查是否支持缓存API
|
||||||
|
if (!('caches' in window)) {
|
||||||
|
this.cacheAvailable = false;
|
||||||
|
this.isLoading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有Service Worker缓存
|
||||||
|
const cacheNames = await getCacheNames();
|
||||||
|
this.cacheAvailable = cacheNames.length > 0;
|
||||||
|
|
||||||
|
if (this.cacheAvailable) {
|
||||||
|
// 获取CDN缓存状态
|
||||||
|
this.cacheData = await checkCdnCacheStatus();
|
||||||
|
|
||||||
|
// 在控制台输出完整缓存状态
|
||||||
|
await logCacheStatus();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载缓存数据失败:', error);
|
||||||
|
this.$message.error('加载缓存数据失败');
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async refreshCache() {
|
||||||
|
this.loadCacheData();
|
||||||
|
this.$message.success('正在刷新缓存状态');
|
||||||
|
},
|
||||||
|
|
||||||
|
async clearCache() {
|
||||||
|
this.$confirm('确定要清除所有缓存吗?', '警告', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(async () => {
|
||||||
|
try {
|
||||||
|
const success = await clearAllCaches();
|
||||||
|
if (success) {
|
||||||
|
this.$message.success('缓存已清除');
|
||||||
|
await this.loadCacheData();
|
||||||
|
} else {
|
||||||
|
this.$message.error('清除缓存失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('清除缓存失败:', error);
|
||||||
|
this.$message.error('清除缓存失败');
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
this.$message.info('已取消清除');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshPage() {
|
||||||
|
window.location.reload();
|
||||||
|
},
|
||||||
|
|
||||||
|
handleClose() {
|
||||||
|
this.$emit('update:visible', false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.loading-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-cache-message {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-cache-message i {
|
||||||
|
font-size: 48px;
|
||||||
|
color: #E6A23C;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -83,7 +83,8 @@ export default {
|
|||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: #5778ff;
|
color: #5778ff;
|
||||||
background: #e6ebff;
|
background: #e6ebff;
|
||||||
width: 57px;
|
width: auto;
|
||||||
|
padding: 0 12px;
|
||||||
height: 21px;
|
height: 21px;
|
||||||
line-height: 21px;
|
line-height: 21px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<span style="margin-right: 8px;">是否启用</span>
|
<span style="margin-right: 8px;">是否启用</span>
|
||||||
<el-switch v-model="form.isEnabled" :active-value="1" :inactive-value="0" class="custom-switch"></el-switch>
|
<el-switch v-model="form.isEnabled" :active-value="1" :inactive-value="0" class="custom-switch"></el-switch>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; align-items: center;">
|
<div style="display: none; align-items: center;">
|
||||||
<span style="margin-right: 8px;">设为默认</span>
|
<span style="margin-right: 8px;">设为默认</span>
|
||||||
<el-switch v-model="form.isDefault" :active-value="1" :inactive-value="0" class="custom-switch"></el-switch>
|
<el-switch v-model="form.isDefault" :active-value="1" :inactive-value="0" class="custom-switch"></el-switch>
|
||||||
</div>
|
</div>
|
||||||
@@ -64,18 +64,10 @@
|
|||||||
<el-form :model="form.configJson" ref="callInfoForm" label-width="auto" class="custom-form">
|
<el-form :model="form.configJson" ref="callInfoForm" label-width="auto" class="custom-form">
|
||||||
<template v-for="(row, rowIndex) in chunkedCallInfoFields">
|
<template v-for="(row, rowIndex) in chunkedCallInfoFields">
|
||||||
<div :key="rowIndex" style="display: flex; gap: 20px; margin-bottom: 0;">
|
<div :key="rowIndex" style="display: flex; gap: 20px; margin-bottom: 0;">
|
||||||
<el-form-item
|
<el-form-item v-for="field in row" :key="field.prop" :label="field.label" :prop="field.prop"
|
||||||
v-for="field in row"
|
|
||||||
:key="field.prop"
|
|
||||||
:label="field.label"
|
|
||||||
:prop="field.prop"
|
|
||||||
style="flex: 1;">
|
style="flex: 1;">
|
||||||
<el-input
|
<el-input v-model="form.configJson[field.prop]" :placeholder="field.placeholder" :type="field.type"
|
||||||
v-model="form.configJson[field.prop]"
|
class="custom-input-bg" :show-password="field.type === 'password'">
|
||||||
:placeholder="field.placeholder"
|
|
||||||
:type="field.type"
|
|
||||||
class="custom-input-bg"
|
|
||||||
:show-password="field.type === 'password'">
|
|
||||||
</el-input>
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
@@ -165,57 +157,57 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
resetForm() {
|
resetForm() {
|
||||||
this.form = {
|
this.form = {
|
||||||
id: "",
|
id: "",
|
||||||
modelType: "",
|
modelType: "",
|
||||||
modelCode: "",
|
modelCode: "",
|
||||||
modelName: "",
|
modelName: "",
|
||||||
isDefault: false,
|
isDefault: false,
|
||||||
isEnabled: false,
|
isEnabled: false,
|
||||||
docLink: "",
|
docLink: "",
|
||||||
remark: "",
|
remark: "",
|
||||||
sort: 0,
|
sort: 0,
|
||||||
configJson: {}
|
configJson: {}
|
||||||
};
|
};
|
||||||
this.dynamicCallInfoFields.forEach(field => {
|
this.dynamicCallInfoFields.forEach(field => {
|
||||||
this.$set(this.form.configJson, field.prop, '');
|
this.$set(this.form.configJson, field.prop, '');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
resetProviders() {
|
resetProviders() {
|
||||||
this.providers = [];
|
this.providers = [];
|
||||||
this.providersLoaded = false;
|
this.providersLoaded = false;
|
||||||
},
|
},
|
||||||
loadModelData() {
|
loadModelData() {
|
||||||
if (this.modelData.id) {
|
if (this.modelData.id) {
|
||||||
Api.model.getModelConfig(this.modelData.id, ({ data }) => {
|
Api.model.getModelConfig(this.modelData.id, ({ data }) => {
|
||||||
if (data.code === 0 && data.data) {
|
if (data.code === 0 && data.data) {
|
||||||
const model = data.data;
|
const model = data.data;
|
||||||
this.pendingProviderType = model.configJson.type;
|
this.pendingProviderType = model.configJson.type;
|
||||||
this.pendingModelData = model;
|
this.pendingModelData = model;
|
||||||
|
|
||||||
if (this.providersLoaded) {
|
if (this.providersLoaded) {
|
||||||
this.loadProviderFields(model.configJson.type);
|
this.loadProviderFields(model.configJson.type);
|
||||||
} else {
|
} else {
|
||||||
this.loadProviders();
|
this.loadProviders();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleSave() {
|
handleSave() {
|
||||||
const provideCode = this.form.configJson.type;
|
const provideCode = this.form.configJson.type;
|
||||||
const formData = {
|
const formData = {
|
||||||
id: this.modelData.id,
|
id: this.modelData.id,
|
||||||
modelCode: this.form.modelCode,
|
modelCode: this.form.modelCode,
|
||||||
modelName: this.form.modelName,
|
modelName: this.form.modelName,
|
||||||
isDefault: this.form.isDefault ? 1 : 0,
|
isDefault: this.form.isDefault ? 1 : 0,
|
||||||
isEnabled: this.form.isEnabled ? 1 : 0,
|
isEnabled: this.form.isEnabled ? 1 : 0,
|
||||||
docLink: this.form.docLink,
|
docLink: this.form.docLink,
|
||||||
remark: this.form.remark,
|
remark: this.form.remark,
|
||||||
sort: this.form.sort || 0,
|
sort: this.form.sort || 0,
|
||||||
configJson: {
|
configJson: {
|
||||||
...this.form.configJson,
|
...this.form.configJson,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
this.$emit("save", { provideCode, formData });
|
this.$emit("save", { provideCode, formData });
|
||||||
this.dialogVisible = false;
|
this.dialogVisible = false;
|
||||||
|
|||||||
@@ -551,28 +551,26 @@ export default {
|
|||||||
|
|
||||||
/* 备注文本 */
|
/* 备注文本 */
|
||||||
::v-deep .remark-input .el-textarea__inner {
|
::v-deep .remark-input .el-textarea__inner {
|
||||||
background-color: #f5f5f5;
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid #e6e6e6;
|
border: 1px solid #e6e6e6;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
resize: none;
|
resize: none;
|
||||||
max-height: 40px !important;
|
max-height: 40px !important;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
background-color: transparent !important;
|
||||||
|
|
||||||
::v-deep .remark-input .el-textarea__inner::placeholder {
|
|
||||||
color: black !important;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
::v-deep .remark-input .el-textarea__inner {
|
|
||||||
background-color: #f4f6fa;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .remark-input .el-textarea__inner:focus {
|
::v-deep .remark-input .el-textarea__inner:focus {
|
||||||
background-color: #edeffb;
|
border-color: #409EFF !important;
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::v-deep .remark-input .el-textarea__inner::placeholder {
|
||||||
|
color: #c0c4cc !important;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* 滚动容器 */
|
/* 滚动容器 */
|
||||||
.scroll-wrapper {
|
.scroll-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -582,7 +580,7 @@ export default {
|
|||||||
|
|
||||||
.table-container {
|
.table-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: scroll;
|
overflow: auto;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
padding-right: 15px;
|
padding-right: 15px;
|
||||||
width: calc(100% - 16px);
|
width: calc(100% - 16px);
|
||||||
@@ -641,8 +639,8 @@ export default {
|
|||||||
|
|
||||||
/* 音频播放器容器样式 */
|
/* 音频播放器容器样式 */
|
||||||
.custom-audio-container {
|
.custom-audio-container {
|
||||||
width: 280px;
|
width: 90%;
|
||||||
margin: auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 新增按钮组样式 */
|
/* 新增按钮组样式 */
|
||||||
@@ -650,6 +648,12 @@ export default {
|
|||||||
bottom: 20px;
|
bottom: 20px;
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action-buttons .el-button {
|
||||||
|
padding: 8px 15px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
.edit-btn,
|
.edit-btn,
|
||||||
.delete-btn,
|
.delete-btn,
|
||||||
.save-btn {
|
.save-btn {
|
||||||
@@ -668,4 +672,40 @@ export default {
|
|||||||
.save-btn {
|
.save-btn {
|
||||||
color: #5cca8e !important;
|
color: #5cca8e !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 表格单元格自适应 */
|
||||||
|
::v-deep .el-table__body-wrapper {
|
||||||
|
overflow-x: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .el-table td {
|
||||||
|
white-space: pre-wrap !important;
|
||||||
|
word-break: break-all !important;
|
||||||
|
}
|
||||||
|
/* 按钮组定位调整 */
|
||||||
|
.action-buttons {
|
||||||
|
position: static;
|
||||||
|
padding: 15px 0;
|
||||||
|
background: white;
|
||||||
|
box-shadow: 0 -2px 12px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 输入框自适应 */
|
||||||
|
::v-deep .el-input__inner,
|
||||||
|
::v-deep .el-textarea__inner {
|
||||||
|
width: 100% !important;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 音频输入框特殊处理 */
|
||||||
|
.audio-input ::v-deep .el-input__inner {
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 操作按钮弹性布局 */
|
||||||
|
::v-deep .el-table__row .el-button {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin: 2px !important;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
@@ -5,33 +5,17 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Api from '@/apis/api';
|
import { mapState } from 'vuex';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'VersionFooter',
|
name: 'VersionFooter',
|
||||||
data() {
|
computed: {
|
||||||
return {
|
...mapState({
|
||||||
version: ''
|
version: state => state.pubConfig.version
|
||||||
}
|
})
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.getSystemVersion();
|
this.$store.dispatch('fetchPubConfig');
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
getSystemVersion() {
|
|
||||||
const storedVersion = sessionStorage.getItem('systemVersion');
|
|
||||||
if (storedVersion) {
|
|
||||||
this.version = storedVersion;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Api.user.getPubConfig(({ data }) => {
|
|
||||||
if (data.code === 0 && data.data.version) {
|
|
||||||
this.version = data.data.version;
|
|
||||||
sessionStorage.setItem('systemVersion', data.data.version);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -6,11 +6,16 @@ import App from './App.vue';
|
|||||||
import router from './router';
|
import router from './router';
|
||||||
import store from './store';
|
import store from './store';
|
||||||
import './styles/global.scss';
|
import './styles/global.scss';
|
||||||
|
import { register as registerServiceWorker } from './registerServiceWorker';
|
||||||
|
|
||||||
Vue.use(ElementUI);
|
Vue.use(ElementUI);
|
||||||
|
|
||||||
Vue.config.productionTip = false
|
Vue.config.productionTip = false
|
||||||
|
|
||||||
|
// 注册Service Worker
|
||||||
|
registerServiceWorker();
|
||||||
|
|
||||||
|
// 创建Vue实例
|
||||||
new Vue({
|
new Vue({
|
||||||
router,
|
router,
|
||||||
store,
|
store,
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
/* eslint-disable no-console */
|
||||||
|
|
||||||
|
export const register = () => {
|
||||||
|
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
const swUrl = `${process.env.BASE_URL}service-worker.js`;
|
||||||
|
|
||||||
|
console.info(`[小智服务] 正在尝试注册Service Worker,URL: ${swUrl}`);
|
||||||
|
|
||||||
|
// 先检查Service Worker是否已注册
|
||||||
|
navigator.serviceWorker.getRegistrations().then(registrations => {
|
||||||
|
if (registrations.length > 0) {
|
||||||
|
console.info('[小智服务] 发现已有Service Worker注册,正在检查更新');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 继续注册Service Worker
|
||||||
|
navigator.serviceWorker
|
||||||
|
.register(swUrl)
|
||||||
|
.then(registration => {
|
||||||
|
console.info('[小智服务] Service Worker注册成功');
|
||||||
|
|
||||||
|
// 更新处理
|
||||||
|
registration.onupdatefound = () => {
|
||||||
|
const installingWorker = registration.installing;
|
||||||
|
if (installingWorker == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
installingWorker.onstatechange = () => {
|
||||||
|
if (installingWorker.state === 'installed') {
|
||||||
|
if (navigator.serviceWorker.controller) {
|
||||||
|
// 内容已缓存更新,通知用户刷新
|
||||||
|
console.log('[小智服务] 新内容可用,请刷新页面');
|
||||||
|
// 可以在这里展示更新提示
|
||||||
|
const updateNotification = document.createElement('div');
|
||||||
|
updateNotification.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: #409EFF;
|
||||||
|
color: white;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
|
||||||
|
z-index: 9999;
|
||||||
|
`;
|
||||||
|
updateNotification.innerHTML = `
|
||||||
|
<div style="display: flex; align-items: center;">
|
||||||
|
<span style="margin-right: 10px;">发现新版本,点击刷新应用</span>
|
||||||
|
<button style="background: white; color: #409EFF; border: none; padding: 5px 10px; border-radius: 3px; cursor: pointer;">刷新</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(updateNotification);
|
||||||
|
updateNotification.querySelector('button').addEventListener('click', () => {
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 一切正常,Service Worker已成功安装
|
||||||
|
console.log('[小智服务] 内容已缓存供离线使用');
|
||||||
|
|
||||||
|
// 可以在这里初始化缓存
|
||||||
|
setTimeout(() => {
|
||||||
|
// 预热CDN缓存
|
||||||
|
const cdnUrls = [
|
||||||
|
'https://unpkg.com/element-ui@2.15.14/lib/theme-chalk/index.css',
|
||||||
|
'https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css',
|
||||||
|
'https://unpkg.com/vue@2.6.14/dist/vue.min.js',
|
||||||
|
'https://unpkg.com/vue-router@3.6.5/dist/vue-router.min.js',
|
||||||
|
'https://unpkg.com/vuex@3.6.2/dist/vuex.min.js',
|
||||||
|
'https://unpkg.com/element-ui@2.15.14/lib/index.js',
|
||||||
|
'https://unpkg.com/axios@0.27.2/dist/axios.min.js',
|
||||||
|
'https://unpkg.com/opus-decoder@0.7.7/dist/opus-decoder.min.js'
|
||||||
|
];
|
||||||
|
|
||||||
|
// 预热缓存
|
||||||
|
cdnUrls.forEach(url => {
|
||||||
|
fetch(url, { mode: 'no-cors' }).catch(err => {
|
||||||
|
console.log(`预热缓存 ${url} 失败`, err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Service Worker 注册失败:', error);
|
||||||
|
|
||||||
|
if (error.name === 'TypeError' && error.message.includes('Failed to register a ServiceWorker')) {
|
||||||
|
console.warn('[小智服务] 注册Service Worker时出现网络错误,CDN资源可能无法缓存');
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
console.info(
|
||||||
|
'可能原因:1. 服务器未配置正确的MIME类型 2. 服务器SSL证书问题 3. 服务器未返回service-worker.js文件'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const unregister = () => {
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
navigator.serviceWorker.ready
|
||||||
|
.then(registration => {
|
||||||
|
registration.unregister();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error(error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
/* global self, workbox */
|
||||||
|
|
||||||
|
// 自定义Service Worker安装和激活的处理逻辑
|
||||||
|
self.addEventListener('message', (event) => {
|
||||||
|
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||||
|
self.skipWaiting();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// CDN资源列表
|
||||||
|
const CDN_CSS = [
|
||||||
|
'https://unpkg.com/element-ui@2.15.14/lib/theme-chalk/index.css',
|
||||||
|
'https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css'
|
||||||
|
];
|
||||||
|
|
||||||
|
const CDN_JS = [
|
||||||
|
'https://unpkg.com/vue@2.6.14/dist/vue.min.js',
|
||||||
|
'https://unpkg.com/vue-router@3.6.5/dist/vue-router.min.js',
|
||||||
|
'https://unpkg.com/vuex@3.6.2/dist/vuex.min.js',
|
||||||
|
'https://unpkg.com/element-ui@2.15.14/lib/index.js',
|
||||||
|
'https://unpkg.com/axios@0.27.2/dist/axios.min.js',
|
||||||
|
'https://unpkg.com/opus-decoder@0.7.7/dist/opus-decoder.min.js'
|
||||||
|
];
|
||||||
|
|
||||||
|
// 当Service Worker被注入manifest后会自动执行
|
||||||
|
const manifest = self.__WB_MANIFEST || [];
|
||||||
|
|
||||||
|
// 检查是否启用CDN模式
|
||||||
|
const isCDNEnabled = manifest.some(entry =>
|
||||||
|
entry.url === 'cdn-mode' && entry.revision === 'enabled'
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`Service Worker 已初始化, CDN模式: ${isCDNEnabled ? '启用' : '禁用'}`);
|
||||||
|
|
||||||
|
// 注入workbox相关代码
|
||||||
|
importScripts('https://storage.googleapis.com/workbox-cdn/releases/7.0.0/workbox-sw.js');
|
||||||
|
workbox.setConfig({ debug: false });
|
||||||
|
|
||||||
|
// 开启workbox
|
||||||
|
workbox.core.skipWaiting();
|
||||||
|
workbox.core.clientsClaim();
|
||||||
|
|
||||||
|
// 预缓存离线页面
|
||||||
|
const OFFLINE_URL = '/offline.html';
|
||||||
|
workbox.precaching.precacheAndRoute([
|
||||||
|
{ url: OFFLINE_URL, revision: null }
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 添加安装完成事件处理器,在控制台显示安装消息
|
||||||
|
self.addEventListener('install', event => {
|
||||||
|
if (isCDNEnabled) {
|
||||||
|
console.log('Service Worker 已安装,开始缓存CDN资源');
|
||||||
|
} else {
|
||||||
|
console.log('Service Worker 已安装,CDN模式禁用,仅缓存本地资源');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保离线页面被缓存
|
||||||
|
event.waitUntil(
|
||||||
|
caches.open('offline-cache').then((cache) => {
|
||||||
|
return cache.add(OFFLINE_URL);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加激活事件处理器
|
||||||
|
self.addEventListener('activate', event => {
|
||||||
|
console.log('Service Worker 已激活,现在控制着页面');
|
||||||
|
|
||||||
|
// 清理旧版本缓存
|
||||||
|
event.waitUntil(
|
||||||
|
caches.keys().then(cacheNames => {
|
||||||
|
return Promise.all(
|
||||||
|
cacheNames.filter(cacheName => {
|
||||||
|
// 清理除当前版本外的缓存
|
||||||
|
return cacheName.startsWith('workbox-') && !workbox.core.cacheNames.runtime.includes(cacheName);
|
||||||
|
}).map(cacheName => {
|
||||||
|
return caches.delete(cacheName);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加fetch事件拦截器,用于查看CDN资源是否命中缓存
|
||||||
|
self.addEventListener('fetch', event => {
|
||||||
|
// 只有启用CDN模式时才进行CDN资源缓存监控
|
||||||
|
if (isCDNEnabled) {
|
||||||
|
const url = new URL(event.request.url);
|
||||||
|
|
||||||
|
// 针对CDN资源,输出是否命中缓存的信息
|
||||||
|
if ([...CDN_CSS, ...CDN_JS].includes(url.href)) {
|
||||||
|
// 不干扰正常的fetch流程,只添加日志
|
||||||
|
console.log(`请求CDN资源: ${url.href}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 仅在CDN模式下缓存CDN资源
|
||||||
|
if (isCDNEnabled) {
|
||||||
|
// 缓存CDN的CSS资源
|
||||||
|
workbox.routing.registerRoute(
|
||||||
|
({ url }) => CDN_CSS.includes(url.href),
|
||||||
|
new workbox.strategies.CacheFirst({
|
||||||
|
cacheName: 'cdn-stylesheets',
|
||||||
|
plugins: [
|
||||||
|
new workbox.expiration.ExpirationPlugin({
|
||||||
|
maxAgeSeconds: 365 * 24 * 60 * 60, // 增加到1年缓存
|
||||||
|
maxEntries: 10, // 最多缓存10个CSS文件
|
||||||
|
}),
|
||||||
|
new workbox.cacheableResponse.CacheableResponsePlugin({
|
||||||
|
statuses: [0, 200], // 缓存成功响应
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// 缓存CDN的JS资源
|
||||||
|
workbox.routing.registerRoute(
|
||||||
|
({ url }) => CDN_JS.includes(url.href),
|
||||||
|
new workbox.strategies.CacheFirst({
|
||||||
|
cacheName: 'cdn-scripts',
|
||||||
|
plugins: [
|
||||||
|
new workbox.expiration.ExpirationPlugin({
|
||||||
|
maxAgeSeconds: 365 * 24 * 60 * 60, // 增加到1年缓存
|
||||||
|
maxEntries: 20, // 最多缓存20个JS文件
|
||||||
|
}),
|
||||||
|
new workbox.cacheableResponse.CacheableResponsePlugin({
|
||||||
|
statuses: [0, 200], // 缓存成功响应
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 无论是否启用CDN模式,都缓存本地静态资源
|
||||||
|
workbox.routing.registerRoute(
|
||||||
|
/\.(?:js|css|png|jpg|jpeg|svg|gif|ico|woff|woff2|eot|ttf|otf)$/,
|
||||||
|
new workbox.strategies.StaleWhileRevalidate({
|
||||||
|
cacheName: 'static-resources',
|
||||||
|
plugins: [
|
||||||
|
new workbox.expiration.ExpirationPlugin({
|
||||||
|
maxAgeSeconds: 7 * 24 * 60 * 60, // 7天缓存
|
||||||
|
maxEntries: 50, // 最多缓存50个文件
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// 缓存HTML页面
|
||||||
|
workbox.routing.registerRoute(
|
||||||
|
/\.html$/,
|
||||||
|
new workbox.strategies.NetworkFirst({
|
||||||
|
cacheName: 'html-cache',
|
||||||
|
plugins: [
|
||||||
|
new workbox.expiration.ExpirationPlugin({
|
||||||
|
maxAgeSeconds: 1 * 24 * 60 * 60, // 1天缓存
|
||||||
|
maxEntries: 10, // 最多缓存10个HTML文件
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// 离线页面 - 使用更可靠的处理方式
|
||||||
|
workbox.routing.setCatchHandler(async ({ event }) => {
|
||||||
|
// 根据请求类型返回适当的默认页面
|
||||||
|
switch (event.request.destination) {
|
||||||
|
case 'document':
|
||||||
|
// 如果是网页请求,返回离线页面
|
||||||
|
return caches.match(OFFLINE_URL);
|
||||||
|
default:
|
||||||
|
// 所有其他请求返回错误
|
||||||
|
return Response.error();
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { goToPage } from "@/utils";
|
import { goToPage } from "@/utils";
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Vuex from 'vuex';
|
import Vuex from 'vuex';
|
||||||
|
import Api from '../apis/api';
|
||||||
import Constant from '../utils/constant';
|
import Constant from '../utils/constant';
|
||||||
|
|
||||||
Vue.use(Vuex)
|
Vue.use(Vuex)
|
||||||
@@ -9,7 +10,11 @@ export default new Vuex.Store({
|
|||||||
state: {
|
state: {
|
||||||
token: '',
|
token: '',
|
||||||
userInfo: {}, // 添加用户信息存储
|
userInfo: {}, // 添加用户信息存储
|
||||||
isSuperAdmin: false // 添加superAdmin状态
|
isSuperAdmin: false, // 添加superAdmin状态
|
||||||
|
pubConfig: { // 添加公共配置存储
|
||||||
|
version: '',
|
||||||
|
allowUserRegister: false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
getToken(state) {
|
getToken(state) {
|
||||||
@@ -26,6 +31,9 @@ export default new Vuex.Store({
|
|||||||
return state.isSuperAdmin
|
return state.isSuperAdmin
|
||||||
}
|
}
|
||||||
return localStorage.getItem('isSuperAdmin') === 'true'
|
return localStorage.getItem('isSuperAdmin') === 'true'
|
||||||
|
},
|
||||||
|
getPubConfig(state) {
|
||||||
|
return state.pubConfig
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
@@ -39,6 +47,9 @@ export default new Vuex.Store({
|
|||||||
state.isSuperAdmin = isSuperAdmin
|
state.isSuperAdmin = isSuperAdmin
|
||||||
localStorage.setItem('isSuperAdmin', isSuperAdmin)
|
localStorage.setItem('isSuperAdmin', isSuperAdmin)
|
||||||
},
|
},
|
||||||
|
setPubConfig(state, config) {
|
||||||
|
state.pubConfig = config
|
||||||
|
},
|
||||||
clearAuth(state) {
|
clearAuth(state) {
|
||||||
state.token = ''
|
state.token = ''
|
||||||
state.userInfo = {}
|
state.userInfo = {}
|
||||||
@@ -55,6 +66,17 @@ export default new Vuex.Store({
|
|||||||
goToPage(Constant.PAGE.LOGIN, true);
|
goToPage(Constant.PAGE.LOGIN, true);
|
||||||
window.location.reload(); // 彻底重置状态
|
window.location.reload(); // 彻底重置状态
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
// 添加获取公共配置的 action
|
||||||
|
fetchPubConfig({ commit }) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
Api.user.getPubConfig(({ data }) => {
|
||||||
|
if (data.code === 0) {
|
||||||
|
commit('setPubConfig', data.data);
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modules: {
|
modules: {
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
/**
|
||||||
|
* 缓存查看工具 - 用于检查CDN资源是否已被Service Worker缓存
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有Service Worker缓存的名称
|
||||||
|
* @returns {Promise<string[]>} 缓存名称列表
|
||||||
|
*/
|
||||||
|
export const getCacheNames = async () => {
|
||||||
|
if (!('caches' in window)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await caches.keys();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取缓存名称失败:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定缓存中的所有URL
|
||||||
|
* @param {string} cacheName 缓存名称
|
||||||
|
* @returns {Promise<string[]>} 缓存的URL列表
|
||||||
|
*/
|
||||||
|
export const getCacheUrls = async (cacheName) => {
|
||||||
|
if (!('caches' in window)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cache = await caches.open(cacheName);
|
||||||
|
const requests = await cache.keys();
|
||||||
|
return requests.map(request => request.url);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`获取缓存 ${cacheName} 的URL失败:`, error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查特定URL是否已被缓存
|
||||||
|
* @param {string} url 要检查的URL
|
||||||
|
* @returns {Promise<boolean>} 是否已缓存
|
||||||
|
*/
|
||||||
|
export const isUrlCached = async (url) => {
|
||||||
|
if (!('caches' in window)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cacheNames = await getCacheNames();
|
||||||
|
for (const cacheName of cacheNames) {
|
||||||
|
const cache = await caches.open(cacheName);
|
||||||
|
const match = await cache.match(url);
|
||||||
|
if (match) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`检查URL ${url} 是否缓存失败:`, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前页面所有CDN资源的缓存状态
|
||||||
|
* @returns {Promise<Object>} 缓存状态对象
|
||||||
|
*/
|
||||||
|
export const checkCdnCacheStatus = async () => {
|
||||||
|
// 从CDN缓存中查找资源
|
||||||
|
const cdnCaches = ['cdn-stylesheets', 'cdn-scripts'];
|
||||||
|
const results = {
|
||||||
|
css: [],
|
||||||
|
js: [],
|
||||||
|
totalCached: 0,
|
||||||
|
totalNotCached: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const cacheName of cdnCaches) {
|
||||||
|
try {
|
||||||
|
const urls = await getCacheUrls(cacheName);
|
||||||
|
|
||||||
|
// 区分CSS和JS资源
|
||||||
|
for (const url of urls) {
|
||||||
|
if (url.endsWith('.css')) {
|
||||||
|
results.css.push({ url, cached: true });
|
||||||
|
} else if (url.endsWith('.js')) {
|
||||||
|
results.js.push({ url, cached: true });
|
||||||
|
}
|
||||||
|
results.totalCached++;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`获取 ${cacheName} 缓存信息失败:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除所有Service Worker缓存
|
||||||
|
* @returns {Promise<boolean>} 是否成功清除
|
||||||
|
*/
|
||||||
|
export const clearAllCaches = async () => {
|
||||||
|
if (!('caches' in window)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cacheNames = await getCacheNames();
|
||||||
|
for (const cacheName of cacheNames) {
|
||||||
|
await caches.delete(cacheName);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('清除所有缓存失败:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将缓存状态输出到控制台
|
||||||
|
*/
|
||||||
|
export const logCacheStatus = async () => {
|
||||||
|
console.group('Service Worker 缓存状态');
|
||||||
|
|
||||||
|
const cacheNames = await getCacheNames();
|
||||||
|
console.log('已发现的缓存:', cacheNames);
|
||||||
|
|
||||||
|
for (const cacheName of cacheNames) {
|
||||||
|
const urls = await getCacheUrls(cacheName);
|
||||||
|
console.group(`缓存: ${cacheName} (${urls.length} 项)`);
|
||||||
|
urls.forEach(url => console.log(url));
|
||||||
|
console.groupEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.groupEnd();
|
||||||
|
return cacheNames.length > 0;
|
||||||
|
};
|
||||||
@@ -1,67 +1,101 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="welcome">
|
<div class="welcome">
|
||||||
<HeaderBar />
|
<HeaderBar />
|
||||||
<el-main style="padding: 20px; display: flex; flex-direction: column;">
|
|
||||||
<div class="table-container">
|
<div class="operation-bar">
|
||||||
<h3 class="device-list-title">设备列表</h3>
|
<h2 class="page-title">设备管理</h2>
|
||||||
<el-table ref="deviceTable" :data="paginatedDeviceList" @selection-change="handleSelectionChange"
|
<div class="right-operations">
|
||||||
style="width: 100%; margin-top: 20px" border stripe>
|
<el-input placeholder="请输入设备型号或Mac地址查询" v-model="searchKeyword"
|
||||||
<el-table-column type="selection" align="center" width="60"></el-table-column>
|
class="search-input" @keyup.enter.native="handleSearch" clearable />
|
||||||
<el-table-column label="设备型号" prop="model" flex></el-table-column>
|
<el-button class="btn-search" @click="handleSearch">搜索</el-button>
|
||||||
<el-table-column label="固件版本" prop="firmwareVersion" width="120"></el-table-column>
|
</div>
|
||||||
<el-table-column label="Mac地址" prop="macAddress"></el-table-column>
|
</div>
|
||||||
<el-table-column label="绑定时间" prop="bindTime" width="200"></el-table-column>
|
|
||||||
<el-table-column label="最近对话" prop="lastConversation" width="140"></el-table-column>
|
<div class="main-wrapper">
|
||||||
<el-table-column label="备注" width="180">
|
<div class="content-panel">
|
||||||
<template slot-scope="scope">
|
<div class="content-area">
|
||||||
<el-input v-if="scope.row.isEdit" v-model="scope.row.remark" size="mini"
|
<el-card class="device-card" shadow="never">
|
||||||
@blur="stopEditRemark(scope.$index)"></el-input>
|
<el-table
|
||||||
<span v-else>
|
ref="deviceTable"
|
||||||
<i v-if="!scope.row.remark" class="el-icon-edit" @click="startEditRemark(scope.$index, scope.row)"></i>
|
:data="paginatedDeviceList"
|
||||||
<span v-else @click="startEditRemark(scope.$index, scope.row)">
|
@selection-change="handleSelectionChange"
|
||||||
{{ scope.row.remark }}
|
class="transparent-table"
|
||||||
</span>
|
:header-cell-class-name="headerCellClassName">
|
||||||
</span>
|
<el-table-column type="selection" align="center" width="120"></el-table-column>
|
||||||
</template>
|
<el-table-column label="设备型号" prop="model" align="center"></el-table-column>
|
||||||
</el-table-column>
|
<el-table-column label="固件版本" prop="firmwareVersion" align="center" ></el-table-column>
|
||||||
<el-table-column label="OTA升级" width="120">
|
<el-table-column label="Mac地址" prop="macAddress" align="center"></el-table-column>
|
||||||
<template slot-scope="scope">
|
<el-table-column label="绑定时间" prop="bindTime" align="center"></el-table-column>
|
||||||
<el-switch v-model="scope.row.otaSwitch" size="mini" active-color="#13ce66"
|
<el-table-column label="最近对话" prop="lastConversation" align="center"></el-table-column>
|
||||||
inactive-color="#ff4949"></el-switch>
|
<el-table-column label="备注" align="center">
|
||||||
</template>
|
<template slot-scope="scope">
|
||||||
</el-table-column>
|
<el-input v-if="scope.row.isEdit" v-model="scope.row.remark" size="mini"
|
||||||
<el-table-column label="操作" width="80">
|
@blur="stopEditRemark(scope.$index)"></el-input>
|
||||||
<template slot-scope="scope">
|
<span v-else>
|
||||||
<el-button size="mini" type="text" @click="handleUnbind(scope.row.device_id)" style="color: #ff4949">
|
<i v-if="!scope.row.remark" class="el-icon-edit" @click="startEditRemark(scope.$index, scope.row)"></i>
|
||||||
解绑
|
<span v-else @click="startEditRemark(scope.$index, scope.row)">
|
||||||
</el-button>
|
{{ scope.row.remark }}
|
||||||
</template>
|
</span>
|
||||||
</el-table-column>
|
</span>
|
||||||
</el-table>
|
</template>
|
||||||
<div class="table_bottom">
|
</el-table-column>
|
||||||
<div class="ctrl_btn">
|
<el-table-column label="OTA升级" align="center">
|
||||||
<el-button size="mini" type="primary" class="select-all-btn" @click="toggleAllSelection">
|
<template slot-scope="scope">
|
||||||
{{ isAllSelected ? '取消全选' : '全选' }}
|
<el-switch v-model="scope.row.otaSwitch" size="mini" active-color="#13ce66"
|
||||||
</el-button>
|
inactive-color="#ff4949"></el-switch>
|
||||||
<el-button type="primary" size="mini" class="add-device-btn" @click="handleAddDevice">
|
</template>
|
||||||
新增
|
</el-table-column>
|
||||||
</el-button>
|
<el-table-column label="操作" align="center">
|
||||||
</div>
|
<template slot-scope="scope">
|
||||||
<div class="custom-pagination">
|
<el-button size="mini" type="text" @click="handleUnbind(scope.row.device_id)">
|
||||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">首页</button>
|
解绑
|
||||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">上一页</button>
|
</el-button>
|
||||||
<button v-for="page in visiblePages" :key="page" class="pagination-btn"
|
</template>
|
||||||
:class="{ active: page === currentPage }" @click="goToPage(page)">
|
</el-table-column>
|
||||||
{{ page }}
|
</el-table>
|
||||||
</button>
|
|
||||||
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">下一页</button>
|
<div class="table_bottom">
|
||||||
<span class="total-text">共{{ deviceList.length }}条记录</span>
|
<div class="ctrl_btn">
|
||||||
</div>
|
<el-button size="mini" type="primary" class="select-all-btn" @click="toggleAllSelection">
|
||||||
|
{{ isAllSelected ? '取消全选' : '全选' }}
|
||||||
|
</el-button>
|
||||||
|
<el-button type="success" size="mini" class="add-device-btn" @click="handleAddDevice">
|
||||||
|
新增
|
||||||
|
</el-button>
|
||||||
|
<el-button size="mini" type="danger" icon="el-icon-delete"
|
||||||
|
@click="deleteSelected">解绑</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="custom-pagination">
|
||||||
|
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
|
||||||
|
<el-option
|
||||||
|
v-for="item in pageSizeOptions"
|
||||||
|
:key="item"
|
||||||
|
:label="`${item}条/页`"
|
||||||
|
:value="item">
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">首页</button>
|
||||||
|
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">上一页</button>
|
||||||
|
<button
|
||||||
|
v-for="page in visiblePages"
|
||||||
|
:key="page"
|
||||||
|
class="pagination-btn"
|
||||||
|
:class="{ active: page === currentPage }"
|
||||||
|
@click="goToPage(page)">
|
||||||
|
{{ page }}
|
||||||
|
</button>
|
||||||
|
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">下一页</button>
|
||||||
|
<span class="total-text">共{{ deviceList.length }}条记录</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<AddDeviceDialog :visible.sync="addDeviceDialogVisible" :agent-id="currentAgentId"
|
</div>
|
||||||
@refresh="fetchBindDevices(currentAgentId)" />
|
|
||||||
</el-main>
|
<AddDeviceDialog :visible.sync="addDeviceDialogVisible" :agent-id="currentAgentId"
|
||||||
|
@refresh="fetchBindDevices(currentAgentId)" />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -77,22 +111,35 @@ export default {
|
|||||||
addDeviceDialogVisible: false,
|
addDeviceDialogVisible: false,
|
||||||
selectedDevices: [],
|
selectedDevices: [],
|
||||||
isAllSelected: false,
|
isAllSelected: false,
|
||||||
|
searchKeyword: "",
|
||||||
|
activeSearchKeyword: "",
|
||||||
currentAgentId: this.$route.query.agentId || '',
|
currentAgentId: this.$route.query.agentId || '',
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: 5,
|
pageSize: 10,
|
||||||
|
pageSizeOptions: [10, 20, 50, 100],
|
||||||
deviceList: [],
|
deviceList: [],
|
||||||
loading: false,
|
loading: false,
|
||||||
userApi: null,
|
userApi: null,
|
||||||
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
filteredDeviceList() {
|
||||||
|
const keyword = this.activeSearchKeyword.toLowerCase();
|
||||||
|
if (!keyword) return this.deviceList;
|
||||||
|
return this.deviceList.filter(device =>
|
||||||
|
(device.model && device.model.toLowerCase().includes(keyword)) ||
|
||||||
|
(device.macAddress && device.macAddress.toLowerCase().includes(keyword))
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
paginatedDeviceList() {
|
paginatedDeviceList() {
|
||||||
const start = (this.currentPage - 1) * this.pageSize;
|
const start = (this.currentPage - 1) * this.pageSize;
|
||||||
const end = start + this.pageSize;
|
const end = start + this.pageSize;
|
||||||
return this.deviceList.slice(start, end);
|
return this.filteredDeviceList.slice(start, end);
|
||||||
},
|
},
|
||||||
pageCount() {
|
pageCount() {
|
||||||
return Math.ceil(this.deviceList.length / this.pageSize);
|
return Math.ceil(this.filteredDeviceList.length / this.pageSize);
|
||||||
},
|
},
|
||||||
visiblePages() {
|
visiblePages() {
|
||||||
const pages = [];
|
const pages = [];
|
||||||
@@ -117,6 +164,14 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
handlePageSizeChange(val) {
|
||||||
|
this.pageSize = val;
|
||||||
|
this.currentPage = 1;
|
||||||
|
},
|
||||||
|
handleSearch() {
|
||||||
|
this.activeSearchKeyword = this.searchKeyword;
|
||||||
|
this.currentPage = 1;
|
||||||
|
},
|
||||||
|
|
||||||
handleSelectionChange(val) {
|
handleSelectionChange(val) {
|
||||||
this.selectedDevices = val;
|
this.selectedDevices = val;
|
||||||
@@ -126,6 +181,55 @@ export default {
|
|||||||
this.$refs.deviceTable.toggleAllSelection();
|
this.$refs.deviceTable.toggleAllSelection();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
deleteSelected() {
|
||||||
|
if (this.selectedDevices.length === 0) {
|
||||||
|
this.$message.warning({
|
||||||
|
message: '请至少选择一条记录',
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$confirm(`确认要解绑选中的 ${this.selectedDevices.length} 台设备吗?`, '警告', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
const deviceIds = this.selectedDevices.map(device => device.device_id);
|
||||||
|
this.batchUnbindDevices(deviceIds);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
batchUnbindDevices(deviceIds) {
|
||||||
|
const promises = deviceIds.map(id => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
Api.device.unbindDevice(id, ({ data }) => {
|
||||||
|
if (data.code === 0) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject(data.msg || '解绑失败');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Promise.all(promises)
|
||||||
|
.then(() => {
|
||||||
|
this.$message.success({
|
||||||
|
message: `成功解绑 ${deviceIds.length} 台设备`,
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
this.fetchBindDevices(this.currentAgentId);
|
||||||
|
this.selectedDevices = [];
|
||||||
|
this.isAllSelected = false;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.$message.error({
|
||||||
|
message: error || '批量解绑过程中出现错误',
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
handleAddDevice() {
|
handleAddDevice() {
|
||||||
this.addDeviceDialogVisible = true;
|
this.addDeviceDialogVisible = true;
|
||||||
@@ -176,35 +280,43 @@ export default {
|
|||||||
Api.device.getAgentBindDevices(agentId, ({ data }) => {
|
Api.device.getAgentBindDevices(agentId, ({ data }) => {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
// 格式化日期并按照绑定时间降序排列
|
|
||||||
this.deviceList = data.data.map(device => {
|
this.deviceList = data.data.map(device => {
|
||||||
// 格式化绑定时间
|
|
||||||
const bindDate = new Date(device.createDate);
|
const bindDate = new Date(device.createDate);
|
||||||
const formattedBindTime = `${bindDate.getFullYear()}-${(bindDate.getMonth() + 1).toString().padStart(2, '0')}-${bindDate.getDate().toString().padStart(2, '0')} ${bindDate.getHours().toString().padStart(2, '0')}:${bindDate.getMinutes().toString().padStart(2, '0')}:${bindDate.getSeconds().toString().padStart(2, '0')}`;
|
const formattedBindTime = `${bindDate.getFullYear()}-${(bindDate.getMonth() + 1).toString().padStart(2, '0')}-${bindDate.getDate().toString().padStart(2, '0')} ${bindDate.getHours().toString().padStart(2, '0')}:${bindDate.getMinutes().toString().padStart(2, '0')}:${bindDate.getSeconds().toString().padStart(2, '0')}`;
|
||||||
|
let formattedLastConversation = '';
|
||||||
|
if (device.lastConnectedAt) {
|
||||||
|
const lastConvoDate = new Date(device.lastConnectedAt);
|
||||||
|
formattedLastConversation = `${lastConvoDate.getFullYear()}-${(lastConvoDate.getMonth() + 1).toString().padStart(2, '0')}-${lastConvoDate.getDate().toString().padStart(2, '0')} ${lastConvoDate.getHours().toString().padStart(2, '0')}:${lastConvoDate.getMinutes().toString().padStart(2, '0')}:${lastConvoDate.getSeconds().toString().padStart(2, '0')}`;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
device_id: device.id,
|
device_id: device.id,
|
||||||
model: device.board,
|
model: device.board,
|
||||||
firmwareVersion: device.appVersion,
|
firmwareVersion: device.appVersion,
|
||||||
macAddress: device.macAddress,
|
macAddress: device.macAddress,
|
||||||
bindTime: formattedBindTime, // 使用格式化后的时间
|
bindTime: formattedBindTime,
|
||||||
lastConversation: device.lastConnectedAt,
|
lastConversation: formattedLastConversation,
|
||||||
remark: device.alias,
|
remark: device.alias,
|
||||||
isEdit: false,
|
isEdit: false,
|
||||||
otaSwitch: device.autoUpdate === 1,
|
otaSwitch: device.autoUpdate === 1,
|
||||||
// 添加原始时间用于排序
|
|
||||||
rawBindTime: new Date(device.createDate).getTime()
|
rawBindTime: new Date(device.createDate).getTime()
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
// 按照绑定时间降序排序
|
.sort((a, b) => a.rawBindTime - b.rawBindTime);
|
||||||
.sort((a, b) => a.rawBindTime - b.rawBindTime);
|
this.activeSearchKeyword = "";
|
||||||
|
this.searchKeyword = "";
|
||||||
} else {
|
} else {
|
||||||
this.$message.error(data.msg || '获取设备列表失败');
|
this.$message.error(data.msg || '获取设备列表失败');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
headerCellClassName({columnIndex}) {
|
||||||
|
if (columnIndex === 0) {
|
||||||
|
return "custom-selection-header";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -213,62 +325,203 @@ export default {
|
|||||||
min-height: 506px;
|
min-height: 506px;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
position: relative;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: linear-gradient(145deg, #e6eeff, #eff0ff);
|
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd);
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
|
||||||
-webkit-background-size: cover;
|
-webkit-background-size: cover;
|
||||||
-o-background-size: cover;
|
-o-background-size: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-container {
|
.main-wrapper {
|
||||||
background: #f9fafc;
|
margin: 5px 22px;
|
||||||
padding: 20px;
|
border-radius: 15px;
|
||||||
border-radius: 20px;
|
min-height: calc(100vh - 24vh);
|
||||||
|
height: auto;
|
||||||
|
max-height: 80vh;
|
||||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||||
margin-top: 15px;
|
position: relative;
|
||||||
|
background: rgba(237, 242, 255, 0.5);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-device-btn {
|
.operation-bar {
|
||||||
background: linear-gradient(135deg, #6b8cff, #a966ff) !important;
|
display: flex;
|
||||||
border: none !important;
|
justify-content: space-between;
|
||||||
color: white !important;
|
align-items: center;
|
||||||
margin-left: 10px;
|
padding: 16px 24px;
|
||||||
height: 32px;
|
|
||||||
padding: 7px 12px;
|
|
||||||
border-radius: 4px !important;
|
|
||||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-list-title {
|
.page-title {
|
||||||
float: left;
|
font-size: 24px;
|
||||||
font-size: 18px;
|
margin: 0;
|
||||||
font-weight: 700;
|
|
||||||
margin: 5px;
|
|
||||||
color: #2c3e50;
|
color: #2c3e50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-icon-edit {
|
.right-operations {
|
||||||
color: #409eff;
|
display: flex;
|
||||||
cursor: pointer;
|
gap: 10px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 280px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-search {
|
||||||
|
background: linear-gradient(135deg, #6b8cff, #a966ff);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .search-input .el-input__inner {
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #DCDFE6;
|
||||||
|
background-color: white;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .page-size-select{
|
||||||
|
width: 100px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .page-size-select .el-input__inner{
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
background: #dee7ff;
|
||||||
|
color: #606266;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
vertical-align: middle;
|
}
|
||||||
|
::v-deep .page-size-select .el-input__suffix{
|
||||||
|
right: 6px;
|
||||||
|
width: 15px;
|
||||||
|
height: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
top: 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .page-size-select .el-input__suffix-inner{
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
::v-deep .page-size-select .el-icon-arrow-up:before{
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
border-left: 6px solid transparent;
|
||||||
|
border-right: 6px solid transparent;
|
||||||
|
border-top: 9px solid #606266;
|
||||||
|
position: relative;
|
||||||
|
transform: rotate(0deg);
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .search-input .el-input__inner:focus {
|
||||||
|
border-color: #6b8cff;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-panel {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 15px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-area {
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
min-width: 600px;
|
||||||
|
overflow: auto;
|
||||||
|
background-color: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-card {
|
||||||
|
background: white;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table_bottom {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctrl_btn {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
padding-left: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctrl_btn .el-button {
|
||||||
|
min-width: 72px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 7px 12px 7px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
line-height: 1;
|
||||||
|
font-weight: 500;
|
||||||
|
border: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctrl_btn .el-button:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctrl_btn .el-button--primary {
|
||||||
|
background: #5f70f3;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctrl_btn .el-button--success {
|
||||||
|
background: #5bc98c;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctrl_btn .el-button--danger {
|
||||||
|
background: #fd5b63;
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-pagination {
|
.custom-pagination {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 10px;
|
||||||
margin-top: 15px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-btn {
|
.custom-pagination .el-select {
|
||||||
min-width: 28px;
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-pagination .pagination-btn:first-child,
|
||||||
|
.custom-pagination .pagination-btn:nth-child(2),
|
||||||
|
.custom-pagination .pagination-btn:nth-last-child(2),
|
||||||
|
.custom-pagination .pagination-btn:nth-child(3) {
|
||||||
|
min-width: 60px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@@ -280,63 +533,127 @@ export default {
|
|||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-btn:first-child,
|
.custom-pagination .pagination-btn:first-child:hover,
|
||||||
.pagination-btn:nth-child(2),
|
.custom-pagination .pagination-btn:nth-child(2):hover,
|
||||||
.pagination-btn:nth-last-child(2) {
|
.custom-pagination .pagination-btn:nth-last-child(2):hover,
|
||||||
min-width: 60px;
|
.custom-pagination .pagination-btn:nth-child(3):hover {
|
||||||
}
|
|
||||||
|
|
||||||
.pagination-btn:hover {
|
|
||||||
background: #d7dce6;
|
background: #d7dce6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-btn:disabled {
|
.custom-pagination .pagination-btn:disabled {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-btn.active {
|
.custom-pagination .pagination-btn:not(:first-child):not(:nth-child(3)):not(:nth-child(2)):not(:nth-last-child(2)) {
|
||||||
|
min-width: 28px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
background: transparent;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-pagination .pagination-btn:not(:first-child):not(:nth-child(3)):not(:nth-child(2)):not(:nth-last-child(2)):hover {
|
||||||
|
background: rgba(245, 247, 250, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-pagination .pagination-btn.active {
|
||||||
background: #5f70f3 !important;
|
background: #5f70f3 !important;
|
||||||
color: #ffffff !important;
|
color: #ffffff !important;
|
||||||
border-color: #5f70f3 !important;
|
border-color: #5f70f3 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.total-text {
|
.custom-pagination .pagination-btn.active:hover {
|
||||||
|
background: #6d7cf5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-pagination .total-text {
|
||||||
color: #909399;
|
color: #909399;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table_bottom {
|
:deep(.transparent-table) {
|
||||||
display: flex;
|
background: white;
|
||||||
justify-content: space-between;
|
border: none;
|
||||||
align-items: center;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ctrl_btn {
|
:deep(.transparent-table .el-table__header th) {
|
||||||
|
background: white !important;
|
||||||
|
color: black;
|
||||||
|
border-right: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.transparent-table .el-table__body tr td) {
|
||||||
|
border-top: 1px solid rgba(0, 0, 0, 0.04);
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
||||||
|
border-right: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.transparent-table .el-table__header tr th:first-child .cell),
|
||||||
|
:deep(.transparent-table .el-table__body tr td:first-child .cell) {
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-icon-edit) {
|
||||||
|
color: #7079aa;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-icon-edit:hover) {
|
||||||
|
color: #5a64b5;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.custom-selection-header .el-checkbox) {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.custom-selection-header::after) {
|
||||||
|
content: "选择";
|
||||||
|
display: inline-block;
|
||||||
|
color: black;
|
||||||
|
font-weight: bold;
|
||||||
|
padding-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table .el-button--text) {
|
||||||
|
color: #7079aa;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table .el-button--text:hover) {
|
||||||
|
color: #5a64b5;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.transparent-table) {
|
||||||
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
flex-direction: column;
|
||||||
padding-left: 26px;
|
max-height: calc(100vh - 40vh);
|
||||||
|
}
|
||||||
|
|
||||||
.el-button {
|
:deep(.el-table__body-wrapper) {
|
||||||
min-width: 72px;
|
flex: 1;
|
||||||
height: 32px;
|
overflow-y: auto;
|
||||||
padding: 7px 12px;
|
max-height: none !important;
|
||||||
border-radius: 4px;
|
}
|
||||||
border: none;
|
|
||||||
transition: all 0.3s;
|
|
||||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
|
||||||
|
|
||||||
&:hover {
|
:deep(.el-table__header-wrapper) {
|
||||||
transform: translateY(-1px);
|
flex-shrink: 0;
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
}
|
||||||
}
|
|
||||||
|
@media (min-width: 1144px) {
|
||||||
|
.table_bottom {
|
||||||
|
margin-top: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-button--primary {
|
:deep(.transparent-table) .el-table__body tr td {
|
||||||
background: #5f70f3;
|
padding-top: 16px;
|
||||||
color: white;
|
padding-bottom: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -43,49 +43,49 @@
|
|||||||
|
|
||||||
<!-- 右侧内容 -->
|
<!-- 右侧内容 -->
|
||||||
<div class="content-area">
|
<div class="content-area">
|
||||||
<el-table ref="modelTable" style="width: 100%" :header-cell-style="{ background: 'transparent' }"
|
<el-card class="model-card" shadow="never">
|
||||||
:data="modelList" class="data-table" header-row-class-name="table-header"
|
<el-table ref="modelTable" style="width: 100%" :header-cell-style="{ background: 'transparent' }"
|
||||||
:header-cell-class-name="headerCellClassName" @selection-change="handleSelectionChange">
|
:data="modelList" class="data-table" header-row-class-name="table-header"
|
||||||
<el-table-column type="selection" width="55" align="center"></el-table-column>
|
:header-cell-class-name="headerCellClassName" @selection-change="handleSelectionChange">
|
||||||
<el-table-column label="模型名称" prop="modelName" align="center"></el-table-column>
|
<el-table-column type="selection" width="55" align="center"></el-table-column>
|
||||||
<el-table-column label="模型编码" prop="modelCode" align="center"></el-table-column>
|
<el-table-column label="模型名称" prop="modelName" align="center"></el-table-column>
|
||||||
<el-table-column label="提供商" align="center">
|
<el-table-column label="模型编码" prop="modelCode" align="center"></el-table-column>
|
||||||
<template slot-scope="scope">
|
<el-table-column label="提供商" align="center">
|
||||||
{{ scope.row.configJson.type || '未知' }}
|
<template slot-scope="scope">
|
||||||
</template>
|
{{ scope.row.configJson.type || '未知' }}
|
||||||
</el-table-column>
|
</template>
|
||||||
<el-table-column label="是否启用" align="center">
|
</el-table-column>
|
||||||
<template slot-scope="scope">
|
<el-table-column label="是否启用" align="center">
|
||||||
<el-switch v-model="scope.row.isEnabled" class="custom-switch" :active-value="1" :inactive-value="0"
|
<template slot-scope="scope">
|
||||||
@change="handleStatusChange(scope.row)" />
|
<el-switch v-model="scope.row.isEnabled" class="custom-switch" :active-value="1" :inactive-value="0"
|
||||||
</template>
|
@change="handleStatusChange(scope.row)" />
|
||||||
</el-table-column>
|
</template>
|
||||||
<el-table-column label="是否默认" align="center">
|
</el-table-column>
|
||||||
<template slot-scope="scope">
|
<el-table-column label="是否默认" align="center">
|
||||||
<el-switch v-model="scope.row.isDefault" class="custom-switch" :active-value="1" :inactive-value="0"
|
<template slot-scope="scope">
|
||||||
@change="handleDefaultChange(scope.row)" />
|
<el-switch v-model="scope.row.isDefault" class="custom-switch" :active-value="1" :inactive-value="0"
|
||||||
</template>
|
@change="handleDefaultChange(scope.row)" />
|
||||||
</el-table-column>
|
</template>
|
||||||
<el-table-column v-if="activeTab === 'tts'" label="音色管理" align="center">
|
</el-table-column>
|
||||||
<template slot-scope="scope">
|
<el-table-column v-if="activeTab === 'tts'" label="音色管理" align="center">
|
||||||
<el-button type="text" size="mini" @click="openTtsDialog(scope.row)" class="voice-management-btn">
|
<template slot-scope="scope">
|
||||||
音色管理
|
<el-button type="text" size="mini" @click="openTtsDialog(scope.row)" class="voice-management-btn">
|
||||||
</el-button>
|
音色管理
|
||||||
</template>
|
</el-button>
|
||||||
</el-table-column>
|
</template>
|
||||||
<el-table-column label="操作" align="center" width="150px">
|
</el-table-column>
|
||||||
<template slot-scope="scope">
|
<el-table-column label="操作" align="center" width="150px">
|
||||||
<el-button type="text" size="mini" @click="editModel(scope.row)" class="edit-btn">
|
<template slot-scope="scope">
|
||||||
修改
|
<el-button type="text" size="mini" @click="editModel(scope.row)" class="edit-btn">
|
||||||
</el-button>
|
修改
|
||||||
<el-button type="text" size="mini" @click="deleteModel(scope.row)" class="delete-btn">
|
</el-button>
|
||||||
删除
|
<el-button type="text" size="mini" @click="deleteModel(scope.row)" class="delete-btn">
|
||||||
</el-button>
|
删除
|
||||||
</template>
|
</el-button>
|
||||||
</el-table-column>
|
</template>
|
||||||
</el-table>
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
<div class="table-footer">
|
<div class="table-footer">
|
||||||
<div class="batch-actions">
|
<div class="batch-actions">
|
||||||
<el-button size="mini" type="primary" @click="selectAll">
|
<el-button size="mini" type="primary" @click="selectAll">
|
||||||
{{ isAllSelected ?
|
{{ isAllSelected ?
|
||||||
@@ -99,6 +99,16 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="custom-pagination">
|
<div class="custom-pagination">
|
||||||
|
|
||||||
|
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
|
||||||
|
<el-option
|
||||||
|
v-for="item in pageSizeOptions"
|
||||||
|
:key="item"
|
||||||
|
:label="`${item}条/页`"
|
||||||
|
:value="item">
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
|
||||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">首页</button>
|
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">首页</button>
|
||||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">上一页</button>
|
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">上一页</button>
|
||||||
|
|
||||||
@@ -111,6 +121,7 @@
|
|||||||
<span class="total-text">共{{ total }}条记录</span>
|
<span class="total-text">共{{ total }}条记录</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -141,8 +152,9 @@ export default {
|
|||||||
ttsDialogVisible: false,
|
ttsDialogVisible: false,
|
||||||
selectedTtsModelId: '',
|
selectedTtsModelId: '',
|
||||||
modelList: [],
|
modelList: [],
|
||||||
|
pageSizeOptions: [10, 20, 50, 100],
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: 5,
|
pageSize: 10,
|
||||||
total: 0,
|
total: 0,
|
||||||
selectedModels: [],
|
selectedModels: [],
|
||||||
isAllSelected: false
|
isAllSelected: false
|
||||||
@@ -154,7 +166,6 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
|
||||||
modelTypeText() {
|
modelTypeText() {
|
||||||
const map = {
|
const map = {
|
||||||
vad: '语言活动检测模型(VAD)',
|
vad: '语言活动检测模型(VAD)',
|
||||||
@@ -166,8 +177,6 @@ export default {
|
|||||||
}
|
}
|
||||||
return map[this.activeTab] || '模型配置'
|
return map[this.activeTab] || '模型配置'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
pageCount() {
|
pageCount() {
|
||||||
return Math.ceil(this.total / this.pageSize);
|
return Math.ceil(this.total / this.pageSize);
|
||||||
},
|
},
|
||||||
@@ -189,6 +198,11 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
handlePageSizeChange(val) {
|
||||||
|
this.pageSize = val;
|
||||||
|
this.currentPage = 1;
|
||||||
|
this.loadData();
|
||||||
|
},
|
||||||
openTtsDialog(row) {
|
openTtsDialog(row) {
|
||||||
this.selectedTtsModelId = row.id;
|
this.selectedTtsModelId = row.id;
|
||||||
this.ttsDialogVisible = true;
|
this.ttsDialogVisible = true;
|
||||||
@@ -201,7 +215,8 @@ export default {
|
|||||||
},
|
},
|
||||||
handleMenuSelect(index) {
|
handleMenuSelect(index) {
|
||||||
this.activeTab = index;
|
this.activeTab = index;
|
||||||
this.currentPage = 1;
|
this.currentPage = 1; // 重置到第一页
|
||||||
|
this.pageSize = 10; // 可选:重置每页条数
|
||||||
this.loadData();
|
this.loadData();
|
||||||
},
|
},
|
||||||
handleSearch() {
|
handleSearch() {
|
||||||
@@ -446,7 +461,9 @@ export default {
|
|||||||
.main-wrapper {
|
.main-wrapper {
|
||||||
margin: 5px 22px;
|
margin: 5px 22px;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
min-height: 600px;
|
min-height: calc(100vh - 24vh);
|
||||||
|
height: auto;
|
||||||
|
max-height: 80vh;
|
||||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||||
position: relative;
|
position: relative;
|
||||||
background: rgba(237, 242, 255, 0.5);
|
background: rgba(237, 242, 255, 0.5);
|
||||||
@@ -464,12 +481,6 @@ export default {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-operations {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-panel {
|
.content-panel {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -542,17 +553,10 @@ export default {
|
|||||||
padding: 24px;
|
padding: 24px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-width: 600px;
|
min-width: 600px;
|
||||||
overflow-x: auto;
|
overflow: hidden;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-bar {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
flex-direction: column;
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-group {
|
.action-group {
|
||||||
@@ -576,6 +580,11 @@ export default {
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-search:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
::v-deep .search-input .el-input__inner {
|
::v-deep .search-input .el-input__inner {
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid #DCDFE6;
|
border: 1px solid #DCDFE6;
|
||||||
@@ -583,22 +592,53 @@ export default {
|
|||||||
transition: border-color 0.2s;
|
transition: border-color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::v-deep .page-size-select{
|
||||||
|
width: 100px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .page-size-select .el-input__inner{
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
background: #dee7ff;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
::v-deep .page-size-select .el-input__suffix{
|
||||||
|
right: 6px;
|
||||||
|
width: 15px;
|
||||||
|
height: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
top: 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .page-size-select .el-input__suffix-inner{
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
::v-deep .page-size-select .el-icon-arrow-up:before{
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
border-left: 6px solid transparent;
|
||||||
|
border-right: 6px solid transparent;
|
||||||
|
border-top: 9px solid #606266;
|
||||||
|
position: relative;
|
||||||
|
transform: rotate(0deg);
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
::v-deep .search-input .el-input__inner:focus {
|
::v-deep .search-input .el-input__inner:focus {
|
||||||
border-color: #6b8cff;
|
border-color: #6b8cff;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-search {
|
|
||||||
background: linear-gradient(135deg, #6b8cff, #a966ff);
|
|
||||||
border: none;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-search:hover {
|
|
||||||
opacity: 0.9;
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-table {
|
.data-table {
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -616,12 +656,14 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.table-footer {
|
.table-footer {
|
||||||
margin-top: 24px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 16px 0;
|
padding: 16px 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-height: 60px;
|
||||||
|
background: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.batch-actions {
|
.batch-actions {
|
||||||
@@ -629,12 +671,6 @@ export default {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-wrapper {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.batch-actions .el-button {
|
.batch-actions .el-button {
|
||||||
min-width: 72px;
|
min-width: 72px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
@@ -686,11 +722,6 @@ export default {
|
|||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
::v-deep .el-table .custom-selection-header .cell .el-checkbox__inner {
|
::v-deep .el-table .custom-selection-header .cell .el-checkbox__inner {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
@@ -791,11 +822,11 @@ export default {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-top: 15px;
|
|
||||||
|
|
||||||
/* 导航按钮样式 (首页、上一页、下一页) */
|
/* 导航按钮样式 (首页、上一页、下一页) */
|
||||||
.pagination-btn:first-child,
|
.pagination-btn:first-child,
|
||||||
.pagination-btn:nth-child(2),
|
.pagination-btn:nth-child(2),
|
||||||
|
.pagination-btn:nth-child(3),
|
||||||
.pagination-btn:nth-last-child(2) {
|
.pagination-btn:nth-last-child(2) {
|
||||||
min-width: 60px;
|
min-width: 60px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
@@ -819,7 +850,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 数字按钮样式 */
|
/* 数字按钮样式 */
|
||||||
.pagination-btn:not(:first-child):not(:nth-child(2)):not(:nth-last-child(2)) {
|
.pagination-btn:not(:first-child):not(:nth-child(2)):not(:nth-child(3)):not(:nth-last-child(2)) {
|
||||||
min-width: 28px;
|
min-width: 28px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -852,4 +883,33 @@ export default {
|
|||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.model-card{
|
||||||
|
background: white;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-card ::v-deep .el-card__body{
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table {
|
||||||
|
--table-max-height: calc(100vh - 45vh);
|
||||||
|
max-height: var(--table-max-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table ::v-deep .el-table__body-wrapper {
|
||||||
|
max-height: calc(var(--table-max-height) - 80px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -17,7 +17,11 @@
|
|||||||
<el-card class="params-card" shadow="never">
|
<el-card class="params-card" shadow="never">
|
||||||
<el-table ref="paramsTable" :data="paramsList" class="transparent-table"
|
<el-table ref="paramsTable" :data="paramsList" class="transparent-table"
|
||||||
:header-cell-class-name="headerCellClassName">
|
:header-cell-class-name="headerCellClassName">
|
||||||
<el-table-column label="选择" type="selection" align="center" width="120"></el-table-column>
|
<el-table-column label="选择" align="center" width="120">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="参数编码" prop="paramCode" align="center"></el-table-column>
|
<el-table-column label="参数编码" prop="paramCode" align="center"></el-table-column>
|
||||||
<el-table-column label="参数值" prop="paramValue" align="center"
|
<el-table-column label="参数值" prop="paramValue" align="center"
|
||||||
show-overflow-tooltip></el-table-column>
|
show-overflow-tooltip></el-table-column>
|
||||||
@@ -37,9 +41,17 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
<el-button size="mini" type="success" @click="showAddDialog">新增</el-button>
|
<el-button size="mini" type="success" @click="showAddDialog">新增</el-button>
|
||||||
<el-button size="mini" type="danger" icon="el-icon-delete"
|
<el-button size="mini" type="danger" icon="el-icon-delete"
|
||||||
@click="deleteParam($refs.paramsTable.selection)">删除</el-button>
|
@click="deleteSelectedParams">删除</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="custom-pagination">
|
<div class="custom-pagination">
|
||||||
|
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
|
||||||
|
<el-option
|
||||||
|
v-for="item in pageSizeOptions"
|
||||||
|
:key="item"
|
||||||
|
:label="`${item}条/页`"
|
||||||
|
:value="item">
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
|
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
|
||||||
首页
|
首页
|
||||||
</button>
|
</button>
|
||||||
@@ -64,6 +76,7 @@
|
|||||||
<!-- 新增/编辑参数对话框 -->
|
<!-- 新增/编辑参数对话框 -->
|
||||||
<param-dialog :title="dialogTitle" :visible.sync="dialogVisible" :form="paramForm" @submit="handleSubmit"
|
<param-dialog :title="dialogTitle" :visible.sync="dialogVisible" :form="paramForm" @submit="handleSubmit"
|
||||||
@cancel="dialogVisible = false" />
|
@cancel="dialogVisible = false" />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -79,7 +92,8 @@ export default {
|
|||||||
searchCode: "",
|
searchCode: "",
|
||||||
paramsList: [],
|
paramsList: [],
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: 5,
|
pageSize: 10,
|
||||||
|
pageSizeOptions: [10, 20, 50, 100],
|
||||||
total: 0,
|
total: 0,
|
||||||
dialogVisible: false,
|
dialogVisible: false,
|
||||||
dialogTitle: "新增参数",
|
dialogTitle: "新增参数",
|
||||||
@@ -94,7 +108,9 @@ export default {
|
|||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.fetchParams();
|
this.fetchParams();
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
pageCount() {
|
pageCount() {
|
||||||
return Math.ceil(this.total / this.pageSize);
|
return Math.ceil(this.total / this.pageSize);
|
||||||
@@ -116,6 +132,11 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
handlePageSizeChange(val) {
|
||||||
|
this.pageSize = val;
|
||||||
|
this.currentPage = 1;
|
||||||
|
this.fetchParams();
|
||||||
|
},
|
||||||
fetchParams() {
|
fetchParams() {
|
||||||
Api.admin.getParamsList(
|
Api.admin.getParamsList(
|
||||||
{
|
{
|
||||||
@@ -125,10 +146,16 @@ export default {
|
|||||||
},
|
},
|
||||||
({ data }) => {
|
({ data }) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.paramsList = data.data.list;
|
this.paramsList = data.data.list.map(item => ({
|
||||||
|
...item,
|
||||||
|
selected: false
|
||||||
|
}));
|
||||||
this.total = data.data.total;
|
this.total = data.data.total;
|
||||||
} else {
|
} else {
|
||||||
this.$message.error(data.msg || '获取参数列表失败');
|
this.$message.error({
|
||||||
|
message: data.msg || '获取参数列表失败',
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -138,12 +165,10 @@ export default {
|
|||||||
this.fetchParams();
|
this.fetchParams();
|
||||||
},
|
},
|
||||||
handleSelectAll() {
|
handleSelectAll() {
|
||||||
if (this.isAllSelected) {
|
|
||||||
this.$refs.paramsTable.clearSelection();
|
|
||||||
} else {
|
|
||||||
this.$refs.paramsTable.toggleAllSelection();
|
|
||||||
}
|
|
||||||
this.isAllSelected = !this.isAllSelected;
|
this.isAllSelected = !this.isAllSelected;
|
||||||
|
this.paramsList.forEach(row => {
|
||||||
|
row.selected = this.isAllSelected;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
showAddDialog() {
|
showAddDialog() {
|
||||||
this.dialogTitle = "新增参数";
|
this.dialogTitle = "新增参数";
|
||||||
@@ -165,7 +190,10 @@ export default {
|
|||||||
// 编辑
|
// 编辑
|
||||||
Api.admin.updateParam(form, ({ data }) => {
|
Api.admin.updateParam(form, ({ data }) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.$message.success("修改成功");
|
this.$message.success({
|
||||||
|
message:"修改成功",
|
||||||
|
showClose:true
|
||||||
|
});
|
||||||
this.dialogVisible = false;
|
this.dialogVisible = false;
|
||||||
this.fetchParams();
|
this.fetchParams();
|
||||||
}
|
}
|
||||||
@@ -174,23 +202,40 @@ export default {
|
|||||||
// 新增
|
// 新增
|
||||||
Api.admin.addParam(form, ({ data }) => {
|
Api.admin.addParam(form, ({ data }) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.$message.success("新增成功");
|
this.$message.success({
|
||||||
|
message:"新增成功",
|
||||||
|
showClose:true
|
||||||
|
});
|
||||||
this.dialogVisible = false;
|
this.dialogVisible = false;
|
||||||
this.fetchParams();
|
this.fetchParams();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
deleteSelectedParams() {
|
||||||
|
const selectedRows = this.paramsList.filter(row => row.selected);
|
||||||
|
if (selectedRows.length === 0) {
|
||||||
|
this.$message.warning({
|
||||||
|
message: "请先选择需要删除的参数",
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.deleteParam(selectedRows);
|
||||||
|
},
|
||||||
deleteParam(row) {
|
deleteParam(row) {
|
||||||
// 处理单个参数或参数数组
|
// 处理单个参数或参数数组
|
||||||
const params = Array.isArray(row) ? row : [row];
|
const params = Array.isArray(row) ? row : [row];
|
||||||
|
|
||||||
if (Array.isArray(row) && row.length === 0) {
|
if (Array.isArray(row) && row.length === 0) {
|
||||||
this.$message.warning("请先选择需要删除的参数");
|
this.$message.warning({
|
||||||
|
message:"请先选择需要删除的参数",
|
||||||
|
showClose:true
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const paramCount = params.length;
|
const paramCount = params.length;
|
||||||
this.$confirm(`确定要删除选中的${paramCount}个参数吗?`, '警告', {
|
this.$confirm(`确定要删除选中的${paramCount}个参数吗?`, '警告', {
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: '确定',
|
||||||
@@ -200,7 +245,10 @@ export default {
|
|||||||
}).then(() => {
|
}).then(() => {
|
||||||
const ids = params.map(param => param.id);
|
const ids = params.map(param => param.id);
|
||||||
if (ids.some(id => isNaN(id))) {
|
if (ids.some(id => isNaN(id))) {
|
||||||
this.$message.error('存在无效的参数ID');
|
this.$message.error({
|
||||||
|
message:'存在无效的参数ID',
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,7 +258,7 @@ export default {
|
|||||||
message: `成功删除${paramCount}个参数`,
|
message: `成功删除${paramCount}个参数`,
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
this.fetchParams(); // 刷新参数列表
|
this.fetchParams();
|
||||||
} else {
|
} else {
|
||||||
this.$message.error({
|
this.$message.error({
|
||||||
message: data.msg || '删除失败,请重试',
|
message: data.msg || '删除失败,请重试',
|
||||||
@@ -276,15 +324,20 @@ export default {
|
|||||||
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
||||||
-webkit-background-size: cover;
|
-webkit-background-size: cover;
|
||||||
-o-background-size: cover;
|
-o-background-size: cover;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-wrapper {
|
.main-wrapper {
|
||||||
margin: 5px 22px;
|
margin: 5px 22px;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
min-height: 600px;
|
min-height: calc(100vh - 24vh);
|
||||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
height: auto;
|
||||||
position: relative;
|
max-height: 80vh;
|
||||||
background: rgba(237, 242, 255, 0.5);
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
position: relative;
|
||||||
|
background: rgba(237, 242, 255, 0.5);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.operation-bar {
|
.operation-bar {
|
||||||
@@ -329,14 +382,20 @@ export default {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-width: 600px;
|
min-width: 600px;
|
||||||
overflow-x: auto;
|
overflow: auto;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.params-card {
|
.params-card {
|
||||||
background: white;
|
background: white;
|
||||||
border: none;
|
border: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table_bottom {
|
.table_bottom {
|
||||||
@@ -344,6 +403,7 @@ export default {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ctrl_btn {
|
.ctrl_btn {
|
||||||
@@ -383,12 +443,16 @@ export default {
|
|||||||
.custom-pagination {
|
.custom-pagination {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 10px;
|
||||||
margin-top: 15px;
|
|
||||||
|
.el-select {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.pagination-btn:first-child,
|
.pagination-btn:first-child,
|
||||||
.pagination-btn:nth-child(2),
|
.pagination-btn:nth-child(2),
|
||||||
.pagination-btn:nth-last-child(2) {
|
.pagination-btn:nth-last-child(2),
|
||||||
|
.pagination-btn:nth-child(3) {
|
||||||
min-width: 60px;
|
min-width: 60px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
@@ -410,7 +474,7 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-btn:not(:first-child):not(:nth-child(2)):not(:nth-last-child(2)) {
|
.pagination-btn:not(:first-child):not(:nth-child(3)):not(:nth-child(2)):not(:nth-last-child(2)) {
|
||||||
min-width: 28px;
|
min-width: 28px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -446,7 +510,19 @@ export default {
|
|||||||
|
|
||||||
:deep(.transparent-table) {
|
:deep(.transparent-table) {
|
||||||
background: white;
|
background: white;
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
.el-table__body-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table__header-wrapper {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
.el-table__header th {
|
.el-table__header th {
|
||||||
background: white !important;
|
background: white !important;
|
||||||
color: black;
|
color: black;
|
||||||
@@ -466,19 +542,6 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.custom-selection-header) {
|
|
||||||
.el-checkbox {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: "选择";
|
|
||||||
display: inline-block;
|
|
||||||
color: black;
|
|
||||||
font-weight: bold;
|
|
||||||
padding-bottom: 18px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-checkbox__inner) {
|
:deep(.el-checkbox__inner) {
|
||||||
background-color: #eeeeee !important;
|
background-color: #eeeeee !important;
|
||||||
@@ -534,4 +597,63 @@ export default {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-size-select {
|
||||||
|
width: 100px;
|
||||||
|
margin-right: 10px;
|
||||||
|
|
||||||
|
:deep(.el-input__inner) {
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
background: #dee7ff;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-input__suffix) {
|
||||||
|
right: 6px;
|
||||||
|
width: 15px;
|
||||||
|
height: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
top: 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-input__suffix-inner) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-icon-arrow-up:before) {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
border-left: 6px solid transparent;
|
||||||
|
border-right: 6px solid transparent;
|
||||||
|
border-top: 9px solid #606266;
|
||||||
|
position: relative;
|
||||||
|
transform: rotate(0deg);
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table) {
|
||||||
|
.el-table__body-wrapper {
|
||||||
|
transition: height 0.3s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table {
|
||||||
|
--table-max-height: calc(100vh - 40vh);
|
||||||
|
max-height: var(--table-max-height);
|
||||||
|
|
||||||
|
.el-table__body-wrapper {
|
||||||
|
max-height: calc(var(--table-max-height) - 40px);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -15,9 +15,12 @@
|
|||||||
<div class="content-panel">
|
<div class="content-panel">
|
||||||
<div class="content-area">
|
<div class="content-area">
|
||||||
<el-card class="user-card" shadow="never">
|
<el-card class="user-card" shadow="never">
|
||||||
<el-table ref="userTable" :data="userList" class="transparent-table"
|
<el-table ref="userTable" :data="userList" class="transparent-table">
|
||||||
:header-cell-class-name="headerCellClassName">
|
<el-table-column label="选择" align="center" width="120">
|
||||||
<el-table-column label="选择" type="selection" align="center" width="120"></el-table-column>
|
<template slot-scope="scope">
|
||||||
|
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="用户Id" prop="userid" align="center"></el-table-column>
|
<el-table-column label="用户Id" prop="userid" align="center"></el-table-column>
|
||||||
<el-table-column label="手机号码" prop="mobile" align="center"></el-table-column>
|
<el-table-column label="手机号码" prop="mobile" align="center"></el-table-column>
|
||||||
<el-table-column label="设备数量" prop="deviceCount" align="center"></el-table-column>
|
<el-table-column label="设备数量" prop="deviceCount" align="center"></el-table-column>
|
||||||
@@ -50,6 +53,15 @@
|
|||||||
<el-button size="mini" type="danger" icon="el-icon-delete" @click="batchDelete">删除</el-button>
|
<el-button size="mini" type="danger" icon="el-icon-delete" @click="batchDelete">删除</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="custom-pagination">
|
<div class="custom-pagination">
|
||||||
|
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
|
||||||
|
<el-option
|
||||||
|
v-for="item in pageSizeOptions"
|
||||||
|
:key="item"
|
||||||
|
:label="`${item}条/页`"
|
||||||
|
:value="item">
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
|
||||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
|
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
|
||||||
首页
|
首页
|
||||||
</button>
|
</button>
|
||||||
@@ -69,6 +81,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<view-password-dialog :visible.sync="showViewPassword" :password="currentPassword" />
|
<view-password-dialog :visible.sync="showViewPassword" :password="currentPassword" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -86,8 +99,9 @@ export default {
|
|||||||
currentPassword: "",
|
currentPassword: "",
|
||||||
searchPhone: "",
|
searchPhone: "",
|
||||||
userList: [],
|
userList: [],
|
||||||
|
pageSizeOptions: [10, 20, 50, 100],
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: 5,
|
pageSize: 10,
|
||||||
total: 0,
|
total: 0,
|
||||||
isAllSelected: false
|
isAllSelected: false
|
||||||
};
|
};
|
||||||
@@ -116,35 +130,42 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
handlePageSizeChange(val) {
|
||||||
|
this.pageSize = val;
|
||||||
|
this.currentPage = 1;
|
||||||
|
this.fetchUsers();
|
||||||
|
},
|
||||||
|
|
||||||
fetchUsers() {
|
fetchUsers() {
|
||||||
Api.admin.getUserList(
|
Api.admin.getUserList(
|
||||||
{
|
{
|
||||||
page: this.currentPage,
|
page: this.currentPage,
|
||||||
limit: this.pageSize,
|
limit: this.pageSize,
|
||||||
mobile: this.searchPhone,
|
mobile: this.searchPhone,
|
||||||
},
|
},
|
||||||
({ data }) => {
|
({ data }) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.userList = data.data.list
|
this.userList = data.data.list.map(item => ({
|
||||||
this.total = data.data.total;
|
...item,
|
||||||
}
|
selected: false
|
||||||
}
|
}));
|
||||||
);
|
this.total = data.data.total;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
},
|
},
|
||||||
handleSearch() {
|
handleSearch() {
|
||||||
this.currentPage = 1;
|
this.currentPage = 1;
|
||||||
this.fetchUsers();
|
this.fetchUsers();
|
||||||
},
|
},
|
||||||
handleSelectAll() {
|
handleSelectAll() {
|
||||||
if (this.isAllSelected) {
|
this.isAllSelected = !this.isAllSelected;
|
||||||
this.$refs.userTable.clearSelection();
|
this.userList.forEach(row => {
|
||||||
} else {
|
row.selected = this.isAllSelected;
|
||||||
this.$refs.userTable.toggleAllSelection();
|
});
|
||||||
}
|
|
||||||
this.isAllSelected = !this.isAllSelected;
|
|
||||||
},
|
},
|
||||||
batchDelete() {
|
batchDelete() {
|
||||||
const selectedUsers = this.$refs.userTable.selection;
|
const selectedUsers = this.userList.filter(user => user.selected);
|
||||||
if (selectedUsers.length === 0) {
|
if (selectedUsers.length === 0) {
|
||||||
this.$message.warning("请先选择需要删除的用户");
|
this.$message.warning("请先选择需要删除的用户");
|
||||||
return;
|
return;
|
||||||
@@ -209,20 +230,12 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
batchEnable() {
|
batchEnable() {
|
||||||
const selectedUsers = this.$refs.userTable.selection;
|
const selectedUsers = this.userList.filter(user => user.selected);
|
||||||
if (selectedUsers.length === 0) {
|
this.handleChangeStatus(selectedUsers, 1);
|
||||||
this.$message.warning("请先选择需要启用的用户");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.handleChangeStatus(selectedUsers, 1);
|
|
||||||
},
|
},
|
||||||
batchDisable() {
|
batchDisable() {
|
||||||
const selectedUsers = this.$refs.userTable.selection;
|
const selectedUsers = this.userList.filter(user => user.selected);
|
||||||
if (selectedUsers.length === 0) {
|
this.handleChangeStatus(selectedUsers, 0);
|
||||||
this.$message.warning("请先选择需要禁用的用户");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.handleChangeStatus(selectedUsers, 0);
|
|
||||||
},
|
},
|
||||||
resetPassword(row) {
|
resetPassword(row) {
|
||||||
this.$confirm("重置后将会生成新密码,是否继续?", "提示", {
|
this.$confirm("重置后将会生成新密码,是否继续?", "提示", {
|
||||||
@@ -265,12 +278,6 @@ export default {
|
|||||||
})
|
})
|
||||||
.catch(() => { });
|
.catch(() => { });
|
||||||
},
|
},
|
||||||
headerCellClassName({ columnIndex }) {
|
|
||||||
if (columnIndex === 0) {
|
|
||||||
return "custom-selection-header";
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
},
|
|
||||||
goFirst() {
|
goFirst() {
|
||||||
this.currentPage = 1;
|
this.currentPage = 1;
|
||||||
this.fetchUsers();
|
this.fetchUsers();
|
||||||
@@ -342,15 +349,20 @@ export default {
|
|||||||
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
||||||
-webkit-background-size: cover;
|
-webkit-background-size: cover;
|
||||||
-o-background-size: cover;
|
-o-background-size: cover;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-wrapper {
|
.main-wrapper {
|
||||||
margin: 5px 22px;
|
margin: 5px 22px;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
min-height: 600px;
|
min-height: calc(100vh - 24vh);
|
||||||
|
height: auto;
|
||||||
|
max-height: 80vh;
|
||||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||||
position: relative;
|
position: relative;
|
||||||
background: rgba(237, 242, 255, 0.5);
|
background: rgba(237, 242, 255, 0.5);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.operation-bar {
|
.operation-bar {
|
||||||
@@ -397,12 +409,18 @@ export default {
|
|||||||
min-width: 600px;
|
min-width: 600px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-card {
|
.user-card {
|
||||||
background: white;
|
background: white;
|
||||||
border: none;
|
border: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table_bottom {
|
.table_bottom {
|
||||||
@@ -467,10 +485,14 @@ export default {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-top: 15px;
|
|
||||||
|
.el-select {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.pagination-btn:first-child,
|
.pagination-btn:first-child,
|
||||||
.pagination-btn:nth-child(2),
|
.pagination-btn:nth-child(2),
|
||||||
|
.pagination-btn:nth-child(3),
|
||||||
.pagination-btn:nth-last-child(2) {
|
.pagination-btn:nth-last-child(2) {
|
||||||
min-width: 60px;
|
min-width: 60px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
@@ -493,7 +515,7 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-btn:not(:first-child):not(:nth-child(2)):not(:nth-last-child(2)) {
|
.pagination-btn:not(:first-child):not(:nth-child(2)):not(:nth-child(3)):not(:nth-last-child(2)) {
|
||||||
min-width: 28px;
|
min-width: 28px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -529,6 +551,19 @@ export default {
|
|||||||
|
|
||||||
:deep(.transparent-table) {
|
:deep(.transparent-table) {
|
||||||
background: white;
|
background: white;
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
.el-table__body-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table__header-wrapper {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.el-table__header th {
|
.el-table__header th {
|
||||||
background: white !important;
|
background: white !important;
|
||||||
@@ -557,19 +592,6 @@ export default {
|
|||||||
color: #5a64b5 !important;
|
color: #5a64b5 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.custom-selection-header) {
|
|
||||||
.el-checkbox {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: "选择";
|
|
||||||
display: inline-block;
|
|
||||||
color: black;
|
|
||||||
font-weight: bold;
|
|
||||||
padding-bottom: 18px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-checkbox__inner) {
|
:deep(.el-checkbox__inner) {
|
||||||
background-color: #eeeeee !important;
|
background-color: #eeeeee !important;
|
||||||
@@ -606,4 +628,59 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-size-select {
|
||||||
|
width: 100px;
|
||||||
|
margin-right: 10px;
|
||||||
|
|
||||||
|
:deep(.el-input__inner) {
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
background: #dee7ff;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-input__suffix) {
|
||||||
|
right: 6px;
|
||||||
|
width: 15px;
|
||||||
|
height: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
top: 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-input__suffix-inner) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-icon-arrow-up:before) {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
border-left: 6px solid transparent;
|
||||||
|
border-right: 6px solid transparent;
|
||||||
|
border-top: 9px solid #606266;
|
||||||
|
position: relative;
|
||||||
|
transform: rotate(0deg);
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table {
|
||||||
|
--table-max-height: calc(100vh - 40vh);
|
||||||
|
max-height: var(--table-max-height);
|
||||||
|
|
||||||
|
.el-table__body-wrapper {
|
||||||
|
max-height: calc(var(--table-max-height) - 40px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -19,13 +19,13 @@
|
|||||||
<div class="hi-hint">
|
<div class="hi-hint">
|
||||||
Hello, Let's have a wonderful day!
|
Hello, Let's have a wonderful day!
|
||||||
</div>
|
</div>
|
||||||
<div class="add-device-btn" @click="showAddDialog">
|
<div class="add-device-btn">
|
||||||
<div class="left-add">
|
<div class="left-add" @click="showAddDialog">
|
||||||
添加智能体
|
添加智能体
|
||||||
</div>
|
</div>
|
||||||
<div style="width: 23px;height: 13px;background: #5778ff;margin-left: -10px;" />
|
<div style="width: 23px;height: 13px;background: #5778ff;margin-left: -10px;" />
|
||||||
<div class="right-add">
|
<div class="right-add">
|
||||||
<i class="el-icon-right" style="font-size: 20px;color: #fff;" />
|
<i class="el-icon-right" @click="showAddDialog" style="font-size: 20px;color: #fff;" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -261,4 +261,7 @@ export default {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
/* 居中显示 */
|
/* 居中显示 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style="font-weight: 400;font-size: 14px;text-align: left;color: #5778ff;display: flex;justify-content: space-between;margin-top: 20px;">
|
style="font-weight: 400;font-size: 14px;text-align: left;color: #5778ff;display: flex;justify-content: space-between;margin-top: 20px;">
|
||||||
<div style="cursor: pointer;" @click="goToRegister">新用户注册</div>
|
<div v-if="allowUserRegister" style="cursor: pointer;" @click="goToRegister">新用户注册</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="login-btn" @click="login">登录</div>
|
<div class="login-btn" @click="login">登录</div>
|
||||||
@@ -61,12 +61,18 @@
|
|||||||
import Api from '@/apis/api';
|
import Api from '@/apis/api';
|
||||||
import VersionFooter from '@/components/VersionFooter.vue';
|
import VersionFooter from '@/components/VersionFooter.vue';
|
||||||
import { getUUID, goToPage, showDanger, showSuccess } from '@/utils';
|
import { getUUID, goToPage, showDanger, showSuccess } from '@/utils';
|
||||||
|
import { mapState } from 'vuex';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'login',
|
name: 'login',
|
||||||
components: {
|
components: {
|
||||||
VersionFooter
|
VersionFooter
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
allowUserRegister: state => state.pubConfig.allowUserRegister
|
||||||
|
})
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
activeName: "username",
|
activeName: "username",
|
||||||
@@ -77,13 +83,12 @@ export default {
|
|||||||
captchaId: ''
|
captchaId: ''
|
||||||
},
|
},
|
||||||
captchaUuid: '',
|
captchaUuid: '',
|
||||||
captchaUrl: '',
|
captchaUrl: ''
|
||||||
version: ''
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.fetchCaptcha();
|
this.fetchCaptcha();
|
||||||
this.getSystemVersion();
|
this.$store.dispatch('fetchPubConfig');
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fetchCaptcha() {
|
fetchCaptcha() {
|
||||||
@@ -148,21 +153,6 @@ export default {
|
|||||||
goToRegister() {
|
goToRegister() {
|
||||||
goToPage('/register')
|
goToPage('/register')
|
||||||
},
|
},
|
||||||
|
|
||||||
getSystemVersion() {
|
|
||||||
const storedVersion = sessionStorage.getItem('systemVersion');
|
|
||||||
if (storedVersion) {
|
|
||||||
this.version = storedVersion;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Api.user.getPubConfig(({ data }) => {
|
|
||||||
if (data.code === 0 && data.data.version) {
|
|
||||||
this.version = data.data.version;
|
|
||||||
sessionStorage.setItem('systemVersion', data.data.version);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="welcome">
|
<div class="welcome" @keyup.enter="register">
|
||||||
<el-container style="height: 100%;">
|
<el-container style="height: 100%;">
|
||||||
<!-- 保持相同的头部 -->
|
<!-- 保持相同的头部 -->
|
||||||
<el-header>
|
<el-header>
|
||||||
@@ -82,12 +82,18 @@
|
|||||||
import Api from '@/apis/api';
|
import Api from '@/apis/api';
|
||||||
import VersionFooter from '@/components/VersionFooter.vue';
|
import VersionFooter from '@/components/VersionFooter.vue';
|
||||||
import { getUUID, goToPage, showDanger, showSuccess } from '@/utils';
|
import { getUUID, goToPage, showDanger, showSuccess } from '@/utils';
|
||||||
|
import { mapState } from 'vuex';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'register',
|
name: 'register',
|
||||||
components: {
|
components: {
|
||||||
VersionFooter
|
VersionFooter
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
allowUserRegister: state => state.pubConfig.allowUserRegister
|
||||||
|
})
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
form: {
|
form: {
|
||||||
@@ -101,6 +107,14 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.$store.dispatch('fetchPubConfig').then(() => {
|
||||||
|
if (!this.allowUserRegister) {
|
||||||
|
showDanger('当前不允许普通用户注册');
|
||||||
|
setTimeout(() => {
|
||||||
|
goToPage('/login');
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
});
|
||||||
this.fetchCaptcha();
|
this.fetchCaptcha();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -1,87 +1,126 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="welcome">
|
<div class="welcome">
|
||||||
<HeaderBar />
|
<HeaderBar/>
|
||||||
<el-main style="padding: 16px;display: flex;flex-direction: column;">
|
|
||||||
<div style="border-radius: 16px;background: #fafcfe; border: 1px solid #e8f0ff;">
|
<div class="operation-bar">
|
||||||
<div
|
<h2 class="page-title">角色配置</h2>
|
||||||
style="padding: 15px 24px;font-weight: 700;font-size: 19px;text-align: left;color: #3d4566;display: flex;gap: 13px;align-items: center;">
|
</div>
|
||||||
<div
|
|
||||||
style="width: 37px;height: 37px;background: #5778ff;border-radius: 50%;display: flex;align-items: center;justify-content: center;">
|
<div class="main-wrapper">
|
||||||
<img loading="lazy" src="@/assets/home/setting-user.png" alt="" style="width: 19px;height: 19px;" />
|
<div class="content-panel">
|
||||||
</div>
|
<div class="content-area">
|
||||||
{{ form.agentName }}
|
<el-card class="config-card" shadow="never">
|
||||||
</div>
|
<div class="config-header">
|
||||||
<div style="height: 1px;background: #e8f0ff;" />
|
<div class="header-icon">
|
||||||
<el-form ref="form" :model="form" label-width="72px">
|
<img loading="lazy" src="@/assets/home/setting-user.png" alt="">
|
||||||
<div style="padding: 16px 24px;">
|
|
||||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 24px;">
|
|
||||||
<div>
|
|
||||||
<el-form-item label="助手昵称:">
|
|
||||||
<div class="input-46" style="width: 100%;">
|
|
||||||
<el-input v-model="form.agentName" />
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="角色模版:">
|
|
||||||
<div style="display: flex;gap: 8px;flex-wrap: wrap;">
|
|
||||||
<div v-for="(template, index) in templates" :key="`template-${index}`" class="template-item"
|
|
||||||
:class="{ 'template-loading': loadingTemplate }" @click="selectTemplate(template)">
|
|
||||||
{{ template.agentName }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="角色介绍:">
|
|
||||||
<div class="textarea-box">
|
|
||||||
<el-input type="textarea" rows="5" resize="none" placeholder="请输入内容" v-model="form.systemPrompt"
|
|
||||||
maxlength="2000" show-word-limit />
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="语言编码:">
|
|
||||||
<div class="input-46" style="width: 100%;">
|
|
||||||
<el-input v-model="form.langCode" placeholder="请输入语言编码,如:zh_CN" maxlength="10" show-word-limit />
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="交互语种:">
|
|
||||||
<div class="input-46" style="width: 100%;">
|
|
||||||
<el-input v-model="form.language" placeholder="请输入交互语种,如:中文" maxlength="10" show-word-limit />
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<el-form-item v-for="(model, index) in models" :key="`model-${index}`" :label="model.label"
|
|
||||||
class="model-item">
|
|
||||||
<el-select v-model="form.model[model.key]" filterable placeholder="请选择" class="select-field">
|
|
||||||
<el-option v-for="(item, optionIndex) in modelOptions[model.type]"
|
|
||||||
:key="`option-${index}-${optionIndex}`" :label="item.label" :value="item.value" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="角色音色:">
|
|
||||||
<div style="display: flex;gap: 8px;align-items: center;">
|
|
||||||
<div class="input-46" style="width: 100%;">
|
|
||||||
<el-select v-model="form.ttsVoiceId" placeholder="请选择" style="width: 100%;">
|
|
||||||
<el-option v-for="(item, index) in voiceOptions" :key="`voice-${index}`" :label="item.label"
|
|
||||||
:value="item.value" />
|
|
||||||
</el-select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
</div>
|
</div>
|
||||||
|
<span class="header-title">{{ form.agentName }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="divider"></div>
|
||||||
</el-form>
|
|
||||||
<div style="display: flex;padding: 16px;gap: 8px;align-items: center;">
|
<el-form ref="form" :model="form" label-width="72px">
|
||||||
<div class="save-btn" @click="saveConfig">
|
<div class="form-content">
|
||||||
保存配置
|
<div class="form-grid">
|
||||||
</div>
|
<div class="form-column">
|
||||||
<div class="reset-btn" @click="resetConfig">
|
<el-form-item label="助手昵称:">
|
||||||
重制
|
<el-input v-model="form.agentName" class="form-input"/>
|
||||||
</div>
|
</el-form-item>
|
||||||
<div class="clear-text">
|
<el-form-item label="角色模版:">
|
||||||
<img loading="lazy" src="@/assets/home/red-info.png" alt="" style="width: 19px;height: 19px;" />
|
<div class="template-container">
|
||||||
保存配置后,需要重启设备,新的配置才会生效。
|
<div
|
||||||
</div>
|
v-for="(template, index) in templates"
|
||||||
|
:key="`template-${index}`"
|
||||||
|
class="template-item"
|
||||||
|
:class="{ 'template-loading': loadingTemplate }"
|
||||||
|
@click="selectTemplate(template)"
|
||||||
|
>
|
||||||
|
{{ template.agentName }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="角色介绍:">
|
||||||
|
<el-input
|
||||||
|
type="textarea"
|
||||||
|
rows="5"
|
||||||
|
resize="none"
|
||||||
|
placeholder="请输入内容"
|
||||||
|
v-model="form.systemPrompt"
|
||||||
|
maxlength="2000"
|
||||||
|
show-word-limit
|
||||||
|
class="form-textarea"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="语言编码:">
|
||||||
|
<el-input
|
||||||
|
v-model="form.langCode"
|
||||||
|
placeholder="请输入语言编码,如:zh_CN"
|
||||||
|
maxlength="10"
|
||||||
|
show-word-limit
|
||||||
|
class="form-input"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="交互语种:">
|
||||||
|
<el-input
|
||||||
|
v-model="form.language"
|
||||||
|
placeholder="请输入交互语种,如:中文"
|
||||||
|
maxlength="10"
|
||||||
|
show-word-limit
|
||||||
|
class="form-input"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<div class="action-bar">
|
||||||
|
<el-button type="primary" class="save-btn" @click="saveConfig">保存配置</el-button>
|
||||||
|
<el-button class="reset-btn" @click="resetConfig">重置</el-button>
|
||||||
|
<div class="hint-text">
|
||||||
|
<img loading="lazy" src="@/assets/home/red-info.png" alt="">
|
||||||
|
<span>保存配置后,需要重启设备,新的配置才会生效。</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-column">
|
||||||
|
<el-form-item
|
||||||
|
v-for="(model, index) in models"
|
||||||
|
:key="`model-${index}`"
|
||||||
|
:label="model.label"
|
||||||
|
class="model-item"
|
||||||
|
>
|
||||||
|
<el-select
|
||||||
|
v-model="form.model[model.key]"
|
||||||
|
filterable
|
||||||
|
placeholder="请选择"
|
||||||
|
class="form-select"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="(item, optionIndex) in modelOptions[model.type]"
|
||||||
|
:key="`option-${index}-${optionIndex}`"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="角色音色:">
|
||||||
|
<el-select
|
||||||
|
v-model="form.ttsVoiceId"
|
||||||
|
placeholder="请选择"
|
||||||
|
class="form-select"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="(item, index) in voiceOptions"
|
||||||
|
:key="`voice-${index}`"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-main>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -89,10 +128,9 @@
|
|||||||
import Api from '@/apis/api';
|
import Api from '@/apis/api';
|
||||||
import HeaderBar from "@/components/HeaderBar.vue";
|
import HeaderBar from "@/components/HeaderBar.vue";
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'RoleConfigPage',
|
name: 'RoleConfigPage',
|
||||||
components: { HeaderBar },
|
components: {HeaderBar},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
form: {
|
form: {
|
||||||
@@ -113,12 +151,12 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
models: [
|
models: [
|
||||||
{ label: '语音活动检测(VAD)', key: 'vadModelId', type: 'VAD' },
|
{label: '语音活动检测(VAD)', key: 'vadModelId', type: 'VAD'},
|
||||||
{ label: '语音识别(ASR)', key: 'asrModelId', type: 'ASR' },
|
{label: '语音识别(ASR)', key: 'asrModelId', type: 'ASR'},
|
||||||
{ label: '大语言模型(LLM)', key: 'llmModelId', type: 'LLM' },
|
{label: '大语言模型(LLM)', key: 'llmModelId', type: 'LLM'},
|
||||||
{ label: '意图识别(Intent)', key: 'intentModelId', type: 'Intent' },
|
{label: '意图识别(Intent)', key: 'intentModelId', type: 'Intent'},
|
||||||
{ label: '记忆(Memory)', key: 'memModelId', type: 'Memory' },
|
{label: '记忆(Memory)', key: 'memModelId', type: 'Memory'},
|
||||||
{ label: '语音合成(TTS)', key: 'ttsModelId', type: 'TTS' },
|
{label: '语音合成(TTS)', key: 'ttsModelId', type: 'TTS'},
|
||||||
],
|
],
|
||||||
modelOptions: {},
|
modelOptions: {},
|
||||||
templates: [],
|
templates: [],
|
||||||
@@ -143,7 +181,7 @@ export default {
|
|||||||
language: this.form.language,
|
language: this.form.language,
|
||||||
sort: this.form.sort
|
sort: this.form.sort
|
||||||
};
|
};
|
||||||
Api.agent.updateAgentConfig(this.$route.query.agentId, configData, ({ data }) => {
|
Api.agent.updateAgentConfig(this.$route.query.agentId, configData, ({data}) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.$message.success({
|
this.$message.success({
|
||||||
message: '配置保存成功',
|
message: '配置保存成功',
|
||||||
@@ -163,7 +201,6 @@ export default {
|
|||||||
cancelButtonText: '取消',
|
cancelButtonText: '取消',
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
// 重置表单
|
|
||||||
this.form = {
|
this.form = {
|
||||||
agentCode: "",
|
agentCode: "",
|
||||||
agentName: "",
|
agentName: "",
|
||||||
@@ -186,10 +223,10 @@ export default {
|
|||||||
showClose: true
|
showClose: true
|
||||||
})
|
})
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
fetchTemplates() {
|
fetchTemplates() {
|
||||||
Api.agent.getAgentTemplate(({ data }) => {
|
Api.agent.getAgentTemplate(({data}) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.templates = data.data;
|
this.templates = data.data;
|
||||||
} else {
|
} else {
|
||||||
@@ -234,7 +271,7 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
fetchAgentConfig(agentId) {
|
fetchAgentConfig(agentId) {
|
||||||
Api.agent.getDeviceConfig(agentId, ({ data }) => {
|
Api.agent.getDeviceConfig(agentId, ({data}) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.form = {
|
this.form = {
|
||||||
...this.form,
|
...this.form,
|
||||||
@@ -254,9 +291,8 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
fetchModelOptions() {
|
fetchModelOptions() {
|
||||||
// 为每个模型类型获取选项
|
|
||||||
this.models.forEach(model => {
|
this.models.forEach(model => {
|
||||||
Api.model.getModelNames(model.type, '', ({ data }) => {
|
Api.model.getModelNames(model.type, '', ({data}) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.$set(this.modelOptions, model.type, data.data.map(item => ({
|
this.$set(this.modelOptions, model.type, data.data.map(item => ({
|
||||||
value: item.id,
|
value: item.id,
|
||||||
@@ -273,7 +309,7 @@ export default {
|
|||||||
this.voiceOptions = [];
|
this.voiceOptions = [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Api.model.getModelVoices(modelId, '', ({ data }) => {
|
Api.model.getModelVoices(modelId, '', ({data}) => {
|
||||||
if (data.code === 0 && data.data) {
|
if (data.code === 0 && data.data) {
|
||||||
this.voiceOptions = data.data.map(voice => ({
|
this.voiceOptions = data.data.map(voice => ({
|
||||||
value: voice.id,
|
value: voice.id,
|
||||||
@@ -288,7 +324,6 @@ export default {
|
|||||||
watch: {
|
watch: {
|
||||||
'form.model.ttsModelId': {
|
'form.model.ttsModelId': {
|
||||||
handler(newVal, oldVal) {
|
handler(newVal, oldVal) {
|
||||||
console.log('TTS模型变化:', newVal);
|
|
||||||
if (oldVal && newVal !== oldVal) {
|
if (oldVal && newVal !== oldVal) {
|
||||||
this.form.ttsVoiceId = '';
|
this.form.ttsVoiceId = '';
|
||||||
this.fetchVoiceOptions(newVal);
|
this.fetchVoiceOptions(newVal);
|
||||||
@@ -324,60 +359,131 @@ export default {
|
|||||||
min-height: 506px;
|
min-height: 506px;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
position: relative;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: linear-gradient(145deg, #e6eeff, #eff0ff);
|
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd);
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
/* 确保背景图像覆盖整个元素 */
|
|
||||||
background-position: center;
|
|
||||||
/* 从顶部中心对齐 */
|
|
||||||
-webkit-background-size: cover;
|
-webkit-background-size: cover;
|
||||||
/* 兼容老版本WebKit浏览器 */
|
|
||||||
-o-background-size: cover;
|
-o-background-size: cover;
|
||||||
/* 兼容老版本Opera浏览器 */
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-form-item ::v-deep .el-form-item__label {
|
.operation-bar {
|
||||||
font-size: 10px !important;
|
display: flex;
|
||||||
color: #3d4566 !important;
|
justify-content: space-between;
|
||||||
font-weight: 400;
|
align-items: center;
|
||||||
line-height: 22px;
|
padding: 16px 24px;
|
||||||
padding-bottom: 2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-field {
|
.page-title {
|
||||||
width: 100%;
|
font-size: 24px;
|
||||||
max-width: 720px;
|
margin: 0;
|
||||||
border: 1px solid #e4e6ef;
|
color: #2c3e50;
|
||||||
background: #f6f8fb;
|
|
||||||
border-radius: 8px;
|
|
||||||
height: 36px !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.audio-box {
|
.main-wrapper {
|
||||||
|
margin: 5px 22px;
|
||||||
|
border-radius: 15px;
|
||||||
|
height: calc(100vh - 24vh);
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
position: relative;
|
||||||
|
background: rgba(237, 242, 255, 0.5);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-panel {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 37px;
|
display: flex;
|
||||||
border-radius: 20px;
|
overflow: hidden;
|
||||||
border: 1px solid #e4e6ef;
|
height: 100%;
|
||||||
|
border-radius: 15px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clear-btn {
|
.content-area {
|
||||||
width: 48px;
|
flex: 1;
|
||||||
height: 19px;
|
height: 100%;
|
||||||
background: #fd8383;
|
min-width: 600px;
|
||||||
border-radius: 10px;
|
overflow: auto;
|
||||||
line-height: 19px;
|
background-color: white;
|
||||||
font-size: 11px;
|
display: flex;
|
||||||
color: #fff;
|
flex-direction: column;
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.clear-text {
|
.config-card {
|
||||||
color: #979db1;
|
background: white;
|
||||||
font-size: 11px;
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 13px;
|
||||||
|
padding: 0 0 5px 0;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 19px;
|
||||||
|
color: #3d4566;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-icon {
|
||||||
|
width: 37px;
|
||||||
|
height: 37px;
|
||||||
|
background: #5778ff;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-icon img {
|
||||||
|
width: 19px;
|
||||||
|
height: 19px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
height: 1px;
|
||||||
|
background: #e8f0ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-content {
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-textarea {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-container {
|
||||||
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-left: 16px;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.template-item {
|
.template-item {
|
||||||
@@ -398,47 +504,64 @@ export default {
|
|||||||
background-color: #d0d8ff;
|
background-color: #d0d8ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prompt-bottom {
|
.action-bar {
|
||||||
margin-bottom: 4px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
flex-wrap: wrap;
|
||||||
padding: 0 16px;
|
gap: 8px;
|
||||||
|
margin-top: 20px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-46 {
|
.action-bar {
|
||||||
border: 1px solid #e4e6ef;
|
.el-button.save-btn {
|
||||||
background: #f6f8fb;
|
background: #5778ff;
|
||||||
border-radius: 8px;
|
color: white;
|
||||||
height: 36px !important;
|
border: none;
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
width: 100px;
|
||||||
|
height: 35px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button.reset-btn {
|
||||||
|
background: #e6ebff;
|
||||||
|
color: #5778ff;
|
||||||
|
border: 1px solid #adbdff;
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.save-btn,
|
.hint-text {
|
||||||
.reset-btn {
|
display: flex;
|
||||||
width: 112px;
|
align-items: center;
|
||||||
height: 37px;
|
gap: 8px;
|
||||||
border-radius: 18px;
|
color: #979db1;
|
||||||
line-height: 37px;
|
font-size: 11px;
|
||||||
box-sizing: border-box;
|
margin-left: 16px;
|
||||||
cursor: pointer;
|
|
||||||
font-size: 11px
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.save-btn {
|
.hint-text img {
|
||||||
border-radius: 18px;
|
width: 19px;
|
||||||
background: #5778ff;
|
height: 19px;
|
||||||
color: #fff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.reset-btn {
|
::v-deep .el-form-item__label {
|
||||||
border: 1px solid #adbdff;
|
font-size: 10px !important;
|
||||||
background: #e6ebff;
|
color: #3d4566 !important;
|
||||||
color: #5778ff;
|
font-weight: 400;
|
||||||
|
line-height: 22px;
|
||||||
|
padding-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.textarea-box {
|
::v-deep .el-textarea .el-input__count {
|
||||||
border: 1px solid #e4e6ef;
|
color: #909399;
|
||||||
border-radius: 8px;
|
background: none;
|
||||||
background: #f6f8fb;
|
position: absolute;
|
||||||
|
font-size: 12px;
|
||||||
|
bottom: -10px;
|
||||||
|
right: 21px;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
@@ -6,10 +6,38 @@ const TerserPlugin = require('terser-webpack-plugin');
|
|||||||
const CompressionPlugin = require('compression-webpack-plugin')
|
const CompressionPlugin = require('compression-webpack-plugin')
|
||||||
// BundleAnalyzerPlugin 用于分析打包后的文件
|
// BundleAnalyzerPlugin 用于分析打包后的文件
|
||||||
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||||
|
// WorkboxPlugin 用于生成Service Worker
|
||||||
|
const { InjectManifest } = require('workbox-webpack-plugin');
|
||||||
// 引入 path 模块
|
// 引入 path 模块
|
||||||
const path = require('path');
|
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
function resolve(dir) {
|
||||||
|
return path.join(__dirname, dir)
|
||||||
|
}
|
||||||
|
|
||||||
// 确保加载 .env 文件
|
// 确保加载 .env 文件
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
|
// 定义CDN资源列表,确保Service Worker也能访问
|
||||||
|
const cdnResources = {
|
||||||
|
css: [
|
||||||
|
'https://unpkg.com/element-ui@2.15.14/lib/theme-chalk/index.css',
|
||||||
|
'https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css'
|
||||||
|
],
|
||||||
|
js: [
|
||||||
|
'https://unpkg.com/vue@2.6.14/dist/vue.min.js',
|
||||||
|
'https://unpkg.com/vue-router@3.6.5/dist/vue-router.min.js',
|
||||||
|
'https://unpkg.com/vuex@3.6.2/dist/vuex.min.js',
|
||||||
|
'https://unpkg.com/element-ui@2.15.14/lib/index.js',
|
||||||
|
'https://unpkg.com/axios@0.27.2/dist/axios.min.js',
|
||||||
|
'https://unpkg.com/opus-decoder@0.7.7/dist/opus-decoder.min.js'
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// 判断是否使用CDN
|
||||||
|
const useCDN = process.env.VUE_APP_USE_CDN === 'true';
|
||||||
|
|
||||||
module.exports = defineConfig({
|
module.exports = defineConfig({
|
||||||
productionSourceMap: process.env.NODE_ENV === 'production' ? false : true, // 生产环境不生成 source map
|
productionSourceMap: process.env.NODE_ENV === 'production' ? false : true, // 生产环境不生成 source map
|
||||||
devServer: {
|
devServer: {
|
||||||
@@ -29,21 +57,9 @@ module.exports = defineConfig({
|
|||||||
// 修改 HTML 插件配置,动态插入 CDN 链接
|
// 修改 HTML 插件配置,动态插入 CDN 链接
|
||||||
config.plugin('html')
|
config.plugin('html')
|
||||||
.tap(args => {
|
.tap(args => {
|
||||||
if (process.env.NODE_ENV === 'production') {
|
// 根据配置决定是否使用CDN
|
||||||
args[0].cdn = {
|
if (process.env.NODE_ENV === 'production' && useCDN) {
|
||||||
css: [
|
args[0].cdn = cdnResources;
|
||||||
'https://cdn.jsdelivr.net/npm/element-ui@2.15.14/lib/theme-chalk/index.css',
|
|
||||||
'https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css'
|
|
||||||
],
|
|
||||||
js: [
|
|
||||||
'https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.min.js',
|
|
||||||
'https://cdn.jsdelivr.net/npm/vue-router@3.6.5/dist/vue-router.min.js',
|
|
||||||
'https://cdn.jsdelivr.net/npm/vuex@3.6.2/dist/vuex.min.js',
|
|
||||||
'https://cdn.jsdelivr.net/npm/element-ui@2.15.14/lib/index.js',
|
|
||||||
'https://cdn.jsdelivr.net/npm/axios@0.27.2/dist/axios.min.js',
|
|
||||||
'https://cdn.jsdelivr.net/npm/opus-decoder@0.7.7/dist/opus-decoder.min.js'
|
|
||||||
]
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return args;
|
return args;
|
||||||
});
|
});
|
||||||
@@ -101,14 +117,38 @@ module.exports = defineConfig({
|
|||||||
minRatio: 0.8
|
minRatio: 0.8
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
config.externals = {
|
|
||||||
'vue': 'Vue',
|
// 根据是否使用CDN来决定是否添加Service Worker
|
||||||
'vue-router': 'VueRouter',
|
config.plugins.push(
|
||||||
'vuex': 'Vuex',
|
new InjectManifest({
|
||||||
'element-ui': 'ELEMENT',
|
swSrc: path.resolve(__dirname, 'src/service-worker.js'),
|
||||||
'axios': 'axios',
|
swDest: 'service-worker.js',
|
||||||
'opus-decoder': 'OpusDecoder'
|
exclude: [/\.map$/, /asset-manifest\.json$/],
|
||||||
};
|
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024, // 5MB
|
||||||
|
// 自定义Service Worker注入点
|
||||||
|
injectionPoint: 'self.__WB_MANIFEST',
|
||||||
|
// 添加额外信息传递给Service Worker
|
||||||
|
additionalManifestEntries: useCDN ?
|
||||||
|
[{ url: 'cdn-mode', revision: 'enabled' }] :
|
||||||
|
[{ url: 'cdn-mode', revision: 'disabled' }]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// 如果使用CDN,则配置externals排除依赖包
|
||||||
|
if (useCDN) {
|
||||||
|
config.externals = {
|
||||||
|
'vue': 'Vue',
|
||||||
|
'vue-router': 'VueRouter',
|
||||||
|
'vuex': 'Vuex',
|
||||||
|
'element-ui': 'ELEMENT',
|
||||||
|
'axios': 'axios',
|
||||||
|
'opus-decoder': 'OpusDecoder'
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// 确保不使用CDN时不设置externals,让webpack打包所有依赖
|
||||||
|
config.externals = {};
|
||||||
|
}
|
||||||
|
|
||||||
if (process.env.ANALYZE === 'true') { // 通过环境变量控制
|
if (process.env.ANALYZE === 'true') { // 通过环境变量控制
|
||||||
config.plugins.push(
|
config.plugins.push(
|
||||||
new BundleAnalyzerPlugin({
|
new BundleAnalyzerPlugin({
|
||||||
@@ -128,13 +168,13 @@ module.exports = defineConfig({
|
|||||||
config: [__filename] // 每次配置文件修改时缓存失效
|
config: [__filename] // 每次配置文件修改时缓存失效
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
config.resolve.alias = {
|
|
||||||
'@': path.resolve(__dirname, 'src'), // 让 '@' 代表 'src' 目录
|
|
||||||
'@assets': path.resolve(__dirname, 'src/assets'), // 设置 '@assets' 为 'src/assets' 目录
|
|
||||||
'@components': path.resolve(__dirname, 'src/components'), // 设置 '@components' 为 'src/components' 目录
|
|
||||||
'@views': path.resolve(__dirname, 'src/views'), // 设置 '@views' 为 'src/views' 目录
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// 将CDN资源信息暴露给service-worker.js
|
||||||
|
pwa: {
|
||||||
|
workboxOptions: {
|
||||||
|
skipWaiting: true,
|
||||||
|
clientsClaim: true
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ selected_module:
|
|||||||
Memory: nomem
|
Memory: nomem
|
||||||
# 意图识别模块开启后,可以播放音乐、控制音量、识别退出指令。
|
# 意图识别模块开启后,可以播放音乐、控制音量、识别退出指令。
|
||||||
# 不想开通意图识别,就设置成:nointent
|
# 不想开通意图识别,就设置成:nointent
|
||||||
# 意图识别可使用intent_llm,如果你的LLM是DifyLLM或CozeLLM,建议使用这个。优点:通用性强,缺点:增加串行前置意图识别模块,会增加处理时间,这个意图识别暂时不支持控制音量大小等iot操作
|
# 意图识别可使用intent_llm。优点:通用性强,缺点:增加串行前置意图识别模块,会增加处理时间,这个意图识别暂时不支持控制音量大小等iot操作
|
||||||
# 意图识别可使用function_call,缺点:需要所选择的LLM支持function_call,优点:按需调用工具、速度快,理论上能全部操作所有iot指令
|
# 意图识别可使用function_call,缺点:需要所选择的LLM支持function_call,优点:按需调用工具、速度快,理论上能全部操作所有iot指令
|
||||||
# 默认免费的ChatGLMLLM就已经支持function_call,但是如果像追求稳定建议把LLM设置成:DoubaoLLM,使用的具体model_name是:doubao-pro-32k-functioncall-241028
|
# 默认免费的ChatGLMLLM就已经支持function_call,但是如果像追求稳定建议把LLM设置成:DoubaoLLM,使用的具体model_name是:doubao-pro-32k-functioncall-241028
|
||||||
Intent: function_call
|
Intent: function_call
|
||||||
@@ -163,7 +163,7 @@ Intent:
|
|||||||
type: intent_llm
|
type: intent_llm
|
||||||
# 配备意图识别独立的思考模型
|
# 配备意图识别独立的思考模型
|
||||||
# 如果这里不填,则会默认使用selected_module.LLM的模型作为意图识别的思考模型
|
# 如果这里不填,则会默认使用selected_module.LLM的模型作为意图识别的思考模型
|
||||||
# 如果你的selected_module.LLM选择了DifyLLM或CozeLLM,这里最好使用独立的LLM作为意图识别,例如使用免费的ChatGLMLLM
|
# 如果你的不想使用selected_module.LLM意图识别,这里最好使用独立的LLM作为意图识别,例如使用免费的ChatGLMLLM
|
||||||
llm: ChatGLMLLM
|
llm: ChatGLMLLM
|
||||||
function_call:
|
function_call:
|
||||||
# 不需要动type
|
# 不需要动type
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,8 +1,8 @@
|
|||||||
import os
|
import os
|
||||||
import argparse
|
import argparse
|
||||||
import requests
|
|
||||||
import yaml
|
import yaml
|
||||||
import time
|
from config.manage_api_client import init_service, get_server_config, get_agent_models
|
||||||
|
|
||||||
|
|
||||||
# 添加全局配置缓存
|
# 添加全局配置缓存
|
||||||
_config_cache = None
|
_config_cache = None
|
||||||
@@ -54,93 +54,27 @@ def get_config_file():
|
|||||||
return config_file
|
return config_file
|
||||||
|
|
||||||
|
|
||||||
def _make_api_request(api_url, secret, endpoint, json_data=None):
|
|
||||||
"""执行API请求的通用函数
|
|
||||||
|
|
||||||
Args:
|
|
||||||
api_url: API的基础URL
|
|
||||||
secret: API密钥
|
|
||||||
endpoint: API端点
|
|
||||||
json_data: 请求的JSON数据
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: API返回的数据
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
Exception: 当请求失败时抛出异常
|
|
||||||
"""
|
|
||||||
if not api_url or not secret:
|
|
||||||
raise Exception("manager-api的url或secret配置错误")
|
|
||||||
|
|
||||||
if "你" in secret:
|
|
||||||
raise Exception("请先配置manager-api的secret")
|
|
||||||
|
|
||||||
max_retries = 10
|
|
||||||
retry_delay = 2 # 秒
|
|
||||||
|
|
||||||
for attempt in range(max_retries):
|
|
||||||
try:
|
|
||||||
response = requests.post(f"{api_url}{endpoint}", json=json_data)
|
|
||||||
if response.status_code == 200:
|
|
||||||
result = response.json()
|
|
||||||
if result.get("code") != 0:
|
|
||||||
raise Exception(f"API返回错误: {result.get('msg', '未知错误')}")
|
|
||||||
return result.get("data")
|
|
||||||
|
|
||||||
error_msg = f"manager-api请求失败,状态码: {response.status_code}"
|
|
||||||
try:
|
|
||||||
error_data = response.json()
|
|
||||||
if "msg" in error_data:
|
|
||||||
error_msg = f"{error_msg}, 错误信息: {error_data['msg']}"
|
|
||||||
except:
|
|
||||||
error_msg = f"{error_msg}, 响应内容: {response.text}"
|
|
||||||
|
|
||||||
if attempt < max_retries - 1:
|
|
||||||
print(f"请求manager-api失败,正在重试 ({attempt + 1}/{max_retries})...")
|
|
||||||
time.sleep(retry_delay)
|
|
||||||
else:
|
|
||||||
raise Exception(error_msg)
|
|
||||||
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
if attempt < max_retries - 1:
|
|
||||||
print(f"请求manager-api异常,正在重试 ({attempt + 1}/{max_retries})...")
|
|
||||||
time.sleep(retry_delay)
|
|
||||||
else:
|
|
||||||
raise Exception(f"manager-api请求异常: {str(e)}")
|
|
||||||
|
|
||||||
|
|
||||||
def get_config_from_api(config):
|
def get_config_from_api(config):
|
||||||
"""从Java API获取配置"""
|
"""从Java API获取配置"""
|
||||||
api_url = config["manager-api"].get("url", "")
|
# 初始化API客户端
|
||||||
secret = config["manager-api"].get("secret", "")
|
init_service(config)
|
||||||
|
|
||||||
|
# 获取服务器配置
|
||||||
|
config_data = get_server_config()
|
||||||
|
if config_data is None:
|
||||||
|
raise Exception("Failed to fetch server config from API")
|
||||||
|
|
||||||
config_data = _make_api_request(
|
|
||||||
api_url, secret, "/config/server-base", {"secret": secret}
|
|
||||||
)
|
|
||||||
config_data["read_config_from_api"] = True
|
config_data["read_config_from_api"] = True
|
||||||
config_data["manager-api"] = {
|
config_data["manager-api"] = {
|
||||||
"url": api_url,
|
"url": config["manager-api"].get("url", ""),
|
||||||
"secret": secret,
|
"secret": config["manager-api"].get("secret", ""),
|
||||||
}
|
}
|
||||||
return config_data
|
return config_data
|
||||||
|
|
||||||
|
|
||||||
def get_private_config_from_api(config, device_id, client_id):
|
def get_private_config_from_api(config, device_id, client_id):
|
||||||
"""从Java API获取私有配置"""
|
"""从Java API获取私有配置"""
|
||||||
api_url = config["manager-api"].get("url", "")
|
return get_agent_models(device_id, client_id, config["selected_module"])
|
||||||
secret = config["manager-api"].get("secret", "")
|
|
||||||
|
|
||||||
return _make_api_request(
|
|
||||||
api_url,
|
|
||||||
secret,
|
|
||||||
"/config/agent-models",
|
|
||||||
{
|
|
||||||
"secret": secret,
|
|
||||||
"macAddress": device_id,
|
|
||||||
"clientId": client_id,
|
|
||||||
"selectedModule": config["selected_module"],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_directories(config):
|
def ensure_directories(config):
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import sys
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
from config.config_loader import load_config
|
from config.config_loader import load_config
|
||||||
|
|
||||||
SERVER_VERSION = "0.3.3"
|
SERVER_VERSION = "0.3.7"
|
||||||
|
|
||||||
|
|
||||||
def get_module_abbreviation(module_name, module_dict):
|
def get_module_abbreviation(module_name, module_dict):
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
import os
|
||||||
|
import time
|
||||||
|
from typing import Optional, Dict
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
TAG = __name__
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceNotFoundException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceBindException(Exception):
|
||||||
|
def __init__(self, bind_code):
|
||||||
|
self.bind_code = bind_code
|
||||||
|
super().__init__(f"设备绑定异常,绑定码: {bind_code}")
|
||||||
|
|
||||||
|
|
||||||
|
class ManageApiClient:
|
||||||
|
_instance = None
|
||||||
|
_client = None
|
||||||
|
_secret = None
|
||||||
|
|
||||||
|
def __new__(cls, config):
|
||||||
|
"""单例模式确保全局唯一实例,并支持传入配置参数"""
|
||||||
|
if cls._instance is None:
|
||||||
|
cls._instance = super().__new__(cls)
|
||||||
|
cls._init_client(config)
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _init_client(cls, config):
|
||||||
|
"""初始化持久化连接池"""
|
||||||
|
cls.config = config.get("manager-api")
|
||||||
|
|
||||||
|
if not cls.config:
|
||||||
|
raise Exception("manager-api配置错误")
|
||||||
|
|
||||||
|
if not cls.config.get("url") or not cls.config.get("secret"):
|
||||||
|
raise Exception("manager-api的url或secret配置错误")
|
||||||
|
|
||||||
|
if "你" in cls.config.get("secret"):
|
||||||
|
raise Exception("请先配置manager-api的secret")
|
||||||
|
|
||||||
|
cls._secret = cls.config.get("secret")
|
||||||
|
cls.max_retries = cls.config.get("max_retries", 6) # 最大重试次数
|
||||||
|
cls.retry_delay = cls.config.get("retry_delay", 10) # 初始重试延迟(秒)
|
||||||
|
# NOTE(goody): 2025/4/16 http相关资源统一管理,后续可以增加线程池或者超时
|
||||||
|
# 后续也可以统一配置apiToken之类的走通用的Auth
|
||||||
|
cls._client = httpx.Client(
|
||||||
|
base_url=cls.config.get("url"),
|
||||||
|
headers={
|
||||||
|
"User-Agent": f"PythonClient/2.0 (PID:{os.getpid()})",
|
||||||
|
"Accept": "application/json",
|
||||||
|
},
|
||||||
|
timeout=cls.config.get("timeout", 30), # 默认超时时间30秒
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _request(cls, method: str, endpoint: str, **kwargs) -> Dict:
|
||||||
|
"""发送单次HTTP请求并处理响应"""
|
||||||
|
endpoint = endpoint.lstrip("/")
|
||||||
|
response = cls._client.request(method, endpoint, **kwargs)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
result = response.json()
|
||||||
|
|
||||||
|
# 处理API返回的业务错误
|
||||||
|
if result.get("code") == 10041:
|
||||||
|
raise DeviceNotFoundException(result.get("msg"))
|
||||||
|
elif result.get("code") == 10042:
|
||||||
|
raise DeviceBindException(result.get("msg"))
|
||||||
|
elif result.get("code") != 0:
|
||||||
|
raise Exception(f"API返回错误: {result.get('msg', '未知错误')}")
|
||||||
|
|
||||||
|
# 返回成功数据
|
||||||
|
return result.get("data") if result.get("code") == 0 else None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _should_retry(cls, exception: Exception) -> bool:
|
||||||
|
"""判断异常是否应该重试"""
|
||||||
|
# 网络连接相关错误
|
||||||
|
if isinstance(
|
||||||
|
exception, (httpx.ConnectError, httpx.TimeoutException, httpx.NetworkError)
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# HTTP状态码错误
|
||||||
|
if isinstance(exception, httpx.HTTPStatusError):
|
||||||
|
status_code = exception.response.status_code
|
||||||
|
return status_code in [408, 429, 500, 502, 503, 504]
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _execute_request(cls, method: str, endpoint: str, **kwargs) -> Dict:
|
||||||
|
"""带重试机制的请求执行器"""
|
||||||
|
retry_count = 0
|
||||||
|
|
||||||
|
while retry_count <= cls.max_retries:
|
||||||
|
try:
|
||||||
|
# 执行请求
|
||||||
|
return cls._request(method, endpoint, **kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
# 判断是否应该重试
|
||||||
|
if retry_count < cls.max_retries and cls._should_retry(e):
|
||||||
|
retry_count += 1
|
||||||
|
print(
|
||||||
|
f"{method} {endpoint} 请求失败,将在 {cls.retry_delay:.1f} 秒后进行第 {retry_count} 次重试"
|
||||||
|
)
|
||||||
|
time.sleep(cls.retry_delay)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
# 不重试,直接抛出异常
|
||||||
|
raise
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def safe_close(cls):
|
||||||
|
"""安全关闭连接池"""
|
||||||
|
if cls._client:
|
||||||
|
cls._client.close()
|
||||||
|
cls._instance = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_server_config() -> Optional[Dict]:
|
||||||
|
"""获取服务器基础配置"""
|
||||||
|
return ManageApiClient._instance._execute_request(
|
||||||
|
"POST", "/config/server-base", json={"secret": ManageApiClient._secret}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_agent_models(
|
||||||
|
mac_address: str, client_id: str, selected_module: Dict
|
||||||
|
) -> Optional[Dict]:
|
||||||
|
"""获取代理模型配置"""
|
||||||
|
return ManageApiClient._instance._execute_request(
|
||||||
|
"POST",
|
||||||
|
"/config/agent-models",
|
||||||
|
json={
|
||||||
|
"secret": ManageApiClient._secret,
|
||||||
|
"macAddress": mac_address,
|
||||||
|
"clientId": client_id,
|
||||||
|
"selectedModule": selected_module,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def init_service(config):
|
||||||
|
ManageApiClient(config)
|
||||||
|
|
||||||
|
|
||||||
|
def manage_api_http_safe_close():
|
||||||
|
ManageApiClient.safe_close()
|
||||||
@@ -28,6 +28,8 @@ from plugins_func.register import Action, ActionResponse
|
|||||||
from core.auth import AuthMiddleware, AuthenticationError
|
from core.auth import AuthMiddleware, AuthenticationError
|
||||||
from core.mcp.manager import MCPManager
|
from core.mcp.manager import MCPManager
|
||||||
from config.config_loader import get_private_config_from_api
|
from config.config_loader import get_private_config_from_api
|
||||||
|
from config.manage_api_client import DeviceNotFoundException, DeviceBindException
|
||||||
|
from core.utils.output_counter import add_device_output
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
|
|
||||||
@@ -46,6 +48,9 @@ class ConnectionHandler:
|
|||||||
self.logger = setup_logging()
|
self.logger = setup_logging()
|
||||||
self.auth = AuthMiddleware(config)
|
self.auth = AuthMiddleware(config)
|
||||||
|
|
||||||
|
self.need_bind = False
|
||||||
|
self.bind_code = None
|
||||||
|
|
||||||
self.websocket = None
|
self.websocket = None
|
||||||
self.headers = None
|
self.headers = None
|
||||||
self.client_ip = None
|
self.client_ip = None
|
||||||
@@ -53,6 +58,7 @@ class ConnectionHandler:
|
|||||||
self.session_id = None
|
self.session_id = None
|
||||||
self.prompt = None
|
self.prompt = None
|
||||||
self.welcome_msg = None
|
self.welcome_msg = None
|
||||||
|
self.max_output_size = 0
|
||||||
|
|
||||||
# 客户端状态相关
|
# 客户端状态相关
|
||||||
self.client_abort = False
|
self.client_abort = False
|
||||||
@@ -105,10 +111,36 @@ class ConnectionHandler:
|
|||||||
self.close_after_chat = False # 是否在聊天结束后关闭连接
|
self.close_after_chat = False # 是否在聊天结束后关闭连接
|
||||||
self.use_function_call_mode = False
|
self.use_function_call_mode = False
|
||||||
|
|
||||||
|
self.timeout_task = None
|
||||||
|
self.timeout_seconds = (
|
||||||
|
int(self.config.get("close_connection_no_voice_time", 120)) + 60
|
||||||
|
) # 在原来第一道关闭的基础上加60秒,进行二道关闭
|
||||||
|
|
||||||
async def handle_connection(self, ws):
|
async def handle_connection(self, ws):
|
||||||
try:
|
try:
|
||||||
# 获取并验证headers
|
# 获取并验证headers
|
||||||
self.headers = dict(ws.request.headers)
|
self.headers = dict(ws.request.headers)
|
||||||
|
|
||||||
|
if self.headers.get("device-id", None) is None:
|
||||||
|
# 尝试从 URL 的查询参数中获取 device-id
|
||||||
|
from urllib.parse import parse_qs, urlparse
|
||||||
|
|
||||||
|
# 从 WebSocket 请求中获取路径
|
||||||
|
request_path = ws.request.path
|
||||||
|
if not request_path:
|
||||||
|
self.logger.bind(tag=TAG).error("无法获取请求路径")
|
||||||
|
return
|
||||||
|
parsed_url = urlparse(request_path)
|
||||||
|
query_params = parse_qs(parsed_url.query)
|
||||||
|
if "device-id" in query_params:
|
||||||
|
self.headers["device-id"] = query_params["device-id"][0]
|
||||||
|
self.headers["client-id"] = query_params["client-id"][0]
|
||||||
|
else:
|
||||||
|
self.logger.bind(tag=TAG).error(
|
||||||
|
"无法从请求头和URL查询参数中获取device-id"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
# 获取客户端ip地址
|
# 获取客户端ip地址
|
||||||
self.client_ip = ws.remote_address[0]
|
self.client_ip = ws.remote_address[0]
|
||||||
self.logger.bind(tag=TAG).info(
|
self.logger.bind(tag=TAG).info(
|
||||||
@@ -122,12 +154,17 @@ class ConnectionHandler:
|
|||||||
self.websocket = ws
|
self.websocket = ws
|
||||||
self.session_id = str(uuid.uuid4())
|
self.session_id = str(uuid.uuid4())
|
||||||
|
|
||||||
|
# 启动超时检查任务
|
||||||
|
self.timeout_task = asyncio.create_task(self._check_timeout())
|
||||||
|
|
||||||
self.welcome_msg = self.config["xiaozhi"]
|
self.welcome_msg = self.config["xiaozhi"]
|
||||||
self.welcome_msg["session_id"] = self.session_id
|
self.welcome_msg["session_id"] = self.session_id
|
||||||
await self.websocket.send(json.dumps(self.welcome_msg))
|
await self.websocket.send(json.dumps(self.welcome_msg))
|
||||||
|
|
||||||
|
# 获取差异化配置
|
||||||
|
private_config = self._initialize_private_config()
|
||||||
# 异步初始化
|
# 异步初始化
|
||||||
self.executor.submit(self._initialize_components)
|
self.executor.submit(self._initialize_components, private_config)
|
||||||
# tts 消化线程
|
# tts 消化线程
|
||||||
self.tts_priority_thread = threading.Thread(
|
self.tts_priority_thread = threading.Thread(
|
||||||
target=self._tts_priority_thread, daemon=True
|
target=self._tts_priority_thread, daemon=True
|
||||||
@@ -167,19 +204,23 @@ class ConnectionHandler:
|
|||||||
|
|
||||||
async def _route_message(self, message):
|
async def _route_message(self, message):
|
||||||
"""消息路由"""
|
"""消息路由"""
|
||||||
|
# 重置超时计时器
|
||||||
|
if self.timeout_task:
|
||||||
|
self.timeout_task.cancel()
|
||||||
|
self.timeout_task = asyncio.create_task(self._check_timeout())
|
||||||
|
|
||||||
if isinstance(message, str):
|
if isinstance(message, str):
|
||||||
await handleTextMessage(self, message)
|
await handleTextMessage(self, message)
|
||||||
elif isinstance(message, bytes):
|
elif isinstance(message, bytes):
|
||||||
await handleAudioMessage(self, message)
|
await handleAudioMessage(self, message)
|
||||||
|
|
||||||
def _initialize_components(self):
|
def _initialize_components(self, private_config):
|
||||||
"""初始化组件"""
|
"""初始化组件"""
|
||||||
self._initialize_models()
|
if private_config is not None:
|
||||||
|
self._initialize_models(private_config)
|
||||||
"""加载提示词"""
|
else:
|
||||||
self.prompt = self.config["prompt"]
|
self.prompt = self.config["prompt"]
|
||||||
self.dialogue.put(Message(role="system", content=self.prompt))
|
self.change_system_prompt(self.prompt)
|
||||||
|
|
||||||
"""加载记忆"""
|
"""加载记忆"""
|
||||||
self._initialize_memory()
|
self._initialize_memory()
|
||||||
"""加载意图识别"""
|
"""加载意图识别"""
|
||||||
@@ -192,26 +233,66 @@ class ConnectionHandler:
|
|||||||
|
|
||||||
self.dialogue.update_system_message(self.prompt)
|
self.dialogue.update_system_message(self.prompt)
|
||||||
|
|
||||||
def _initialize_models(self):
|
def _initialize_private_config(self):
|
||||||
read_config_from_api = self.config.get("read_config_from_api", False)
|
read_config_from_api = self.config.get("read_config_from_api", False)
|
||||||
"""如果是从配置文件获取,则进行二次实例化"""
|
"""如果是从配置文件获取,则进行二次实例化"""
|
||||||
if not read_config_from_api:
|
if not read_config_from_api:
|
||||||
return
|
return
|
||||||
"""从接口获取差异化的配置进行二次实例化,非全量重新实例化"""
|
"""从接口获取差异化的配置进行二次实例化,非全量重新实例化"""
|
||||||
try:
|
try:
|
||||||
|
begin_time = time.time()
|
||||||
private_config = get_private_config_from_api(
|
private_config = get_private_config_from_api(
|
||||||
self.config,
|
self.config,
|
||||||
self.headers.get("device-id", None),
|
self.headers.get("device-id", None),
|
||||||
self.headers.get("client-id", None),
|
self.headers.get("client-id", None),
|
||||||
)
|
)
|
||||||
private_config["delete_audio"] = bool(self.config.get("delete_audio", True))
|
private_config["delete_audio"] = bool(self.config.get("delete_audio", True))
|
||||||
self.logger.bind(tag=TAG).info(f"获取差异化配置成功: {private_config}")
|
self.logger.bind(tag=TAG).info(
|
||||||
|
f"{time.time() - begin_time} 秒,获取差异化配置成功: {private_config}"
|
||||||
|
)
|
||||||
|
except DeviceNotFoundException as e:
|
||||||
|
self.need_bind = True
|
||||||
|
private_config = {}
|
||||||
|
except DeviceBindException as e:
|
||||||
|
self.need_bind = True
|
||||||
|
self.bind_code = e.bind_code
|
||||||
|
private_config = {}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
self.need_bind = True
|
||||||
self.logger.bind(tag=TAG).error(f"获取差异化配置失败: {e}")
|
self.logger.bind(tag=TAG).error(f"获取差异化配置失败: {e}")
|
||||||
private_config = {}
|
private_config = {}
|
||||||
|
|
||||||
init_vad, init_asr, init_llm, init_tts, init_memory, init_intent = (
|
init_tts = False
|
||||||
False,
|
if private_config.get("TTS", None) is not None:
|
||||||
|
init_tts = True
|
||||||
|
self.config["TTS"] = private_config["TTS"]
|
||||||
|
self.config["selected_module"]["TTS"] = private_config["selected_module"][
|
||||||
|
"TTS"
|
||||||
|
]
|
||||||
|
|
||||||
|
try:
|
||||||
|
modules = initialize_modules(
|
||||||
|
self.logger,
|
||||||
|
private_config,
|
||||||
|
False,
|
||||||
|
False,
|
||||||
|
False,
|
||||||
|
init_tts,
|
||||||
|
False,
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.bind(tag=TAG).error(f"初始化组件失败: {e}")
|
||||||
|
modules = {}
|
||||||
|
if modules.get("tts", None) is not None:
|
||||||
|
self.tts = modules["tts"]
|
||||||
|
if modules.get("prompt", None) is not None:
|
||||||
|
self.change_system_prompt(modules["prompt"])
|
||||||
|
private_config["prompt"] = None
|
||||||
|
return private_config
|
||||||
|
|
||||||
|
def _initialize_models(self, private_config):
|
||||||
|
init_vad, init_asr, init_llm, init_memory, init_intent = (
|
||||||
False,
|
False,
|
||||||
False,
|
False,
|
||||||
False,
|
False,
|
||||||
@@ -236,12 +317,6 @@ class ConnectionHandler:
|
|||||||
self.config["selected_module"]["LLM"] = private_config["selected_module"][
|
self.config["selected_module"]["LLM"] = private_config["selected_module"][
|
||||||
"LLM"
|
"LLM"
|
||||||
]
|
]
|
||||||
if private_config.get("TTS", None) is not None:
|
|
||||||
init_tts = True
|
|
||||||
self.config["TTS"] = private_config["TTS"]
|
|
||||||
self.config["selected_module"]["TTS"] = private_config["selected_module"][
|
|
||||||
"TTS"
|
|
||||||
]
|
|
||||||
if private_config.get("Memory", None) is not None:
|
if private_config.get("Memory", None) is not None:
|
||||||
init_memory = True
|
init_memory = True
|
||||||
self.config["Memory"] = private_config["Memory"]
|
self.config["Memory"] = private_config["Memory"]
|
||||||
@@ -254,8 +329,8 @@ class ConnectionHandler:
|
|||||||
self.config["selected_module"]["Intent"] = private_config[
|
self.config["selected_module"]["Intent"] = private_config[
|
||||||
"selected_module"
|
"selected_module"
|
||||||
]["Intent"]
|
]["Intent"]
|
||||||
if private_config.get("prompt", None) is not None:
|
if private_config.get("device_max_output_size", None) is not None:
|
||||||
self.config["prompt"] = private_config["prompt"]
|
self.max_output_size = int(private_config["device_max_output_size"])
|
||||||
try:
|
try:
|
||||||
modules = initialize_modules(
|
modules = initialize_modules(
|
||||||
self.logger,
|
self.logger,
|
||||||
@@ -263,15 +338,17 @@ class ConnectionHandler:
|
|||||||
init_vad,
|
init_vad,
|
||||||
init_asr,
|
init_asr,
|
||||||
init_llm,
|
init_llm,
|
||||||
init_tts,
|
False,
|
||||||
init_memory,
|
init_memory,
|
||||||
init_intent,
|
init_intent,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.bind(tag=TAG).error(f"初始化组件失败: {e}")
|
self.logger.bind(tag=TAG).error(f"初始化组件失败: {e}")
|
||||||
modules = {}
|
modules = {}
|
||||||
if modules.get("tts", None) is not None:
|
if modules.get("vad", None) is not None:
|
||||||
self.tts = modules["tts"]
|
self.vad = modules["vad"]
|
||||||
|
if modules.get("asr", None) is not None:
|
||||||
|
self.asr = modules["asr"]
|
||||||
if modules.get("llm", None) is not None:
|
if modules.get("llm", None) is not None:
|
||||||
self.llm = modules["llm"]
|
self.llm = modules["llm"]
|
||||||
if modules.get("intent", None) is not None:
|
if modules.get("intent", None) is not None:
|
||||||
@@ -345,7 +422,6 @@ class ConnectionHandler:
|
|||||||
response_message = []
|
response_message = []
|
||||||
processed_chars = 0 # 跟踪已处理的字符位置
|
processed_chars = 0 # 跟踪已处理的字符位置
|
||||||
try:
|
try:
|
||||||
start_time = time.time()
|
|
||||||
# 使用带记忆的对话
|
# 使用带记忆的对话
|
||||||
future = asyncio.run_coroutine_threadsafe(
|
future = asyncio.run_coroutine_threadsafe(
|
||||||
self.memory.query_memory(query), self.loop
|
self.memory.query_memory(query), self.loop
|
||||||
@@ -367,9 +443,6 @@ class ConnectionHandler:
|
|||||||
if self.client_abort:
|
if self.client_abort:
|
||||||
break
|
break
|
||||||
|
|
||||||
end_time = time.time()
|
|
||||||
# self.logger.bind(tag=TAG).debug(f"大模型返回时间: {end_time - start_time} 秒, 生成token={content}")
|
|
||||||
|
|
||||||
# 合并当前全部文本并处理未分割部分
|
# 合并当前全部文本并处理未分割部分
|
||||||
full_text = "".join(response_message)
|
full_text = "".join(response_message)
|
||||||
current_text = full_text[processed_chars:] # 从未处理的位置开始
|
current_text = full_text[processed_chars:] # 从未处理的位置开始
|
||||||
@@ -462,16 +535,19 @@ class ConnectionHandler:
|
|||||||
function_id = None
|
function_id = None
|
||||||
function_arguments = ""
|
function_arguments = ""
|
||||||
content_arguments = ""
|
content_arguments = ""
|
||||||
|
|
||||||
for response in llm_responses:
|
for response in llm_responses:
|
||||||
content, tools_call = response
|
content, tools_call = response
|
||||||
|
|
||||||
if "content" in response:
|
if "content" in response:
|
||||||
content = response["content"]
|
content = response["content"]
|
||||||
tools_call = None
|
tools_call = None
|
||||||
if content is not None and len(content) > 0:
|
if content is not None and len(content) > 0:
|
||||||
if len(response_message) <= 0 and (
|
content_arguments += content
|
||||||
content == "```" or "<tool_call>" in content
|
|
||||||
):
|
if not tool_call_flag and content_arguments.startswith("<tool_call>"):
|
||||||
tool_call_flag = True
|
# print("content_arguments", content_arguments)
|
||||||
|
tool_call_flag = True
|
||||||
|
|
||||||
if tools_call is not None:
|
if tools_call is not None:
|
||||||
tool_call_flag = True
|
tool_call_flag = True
|
||||||
@@ -483,9 +559,7 @@ class ConnectionHandler:
|
|||||||
function_arguments += tools_call[0].function.arguments
|
function_arguments += tools_call[0].function.arguments
|
||||||
|
|
||||||
if content is not None and len(content) > 0:
|
if content is not None and len(content) > 0:
|
||||||
if tool_call_flag:
|
if not tool_call_flag:
|
||||||
content_arguments += content
|
|
||||||
else:
|
|
||||||
response_message.append(content)
|
response_message.append(content)
|
||||||
|
|
||||||
if self.client_abort:
|
if self.client_abort:
|
||||||
@@ -546,9 +620,8 @@ class ConnectionHandler:
|
|||||||
self.logger.bind(tag=TAG).error(
|
self.logger.bind(tag=TAG).error(
|
||||||
f"function call error: {content_arguments}"
|
f"function call error: {content_arguments}"
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
function_arguments = json.loads(function_arguments)
|
|
||||||
if not bHasError:
|
if not bHasError:
|
||||||
|
response_message.clear()
|
||||||
self.logger.bind(tag=TAG).info(
|
self.logger.bind(tag=TAG).info(
|
||||||
f"function_name={function_name}, function_id={function_id}, function_arguments={function_arguments}"
|
f"function_name={function_name}, function_id={function_id}, function_arguments={function_arguments}"
|
||||||
)
|
)
|
||||||
@@ -644,7 +717,6 @@ class ConnectionHandler:
|
|||||||
self.tts_queue.put(future)
|
self.tts_queue.put(future)
|
||||||
self.dialogue.put(Message(role="assistant", content=text))
|
self.dialogue.put(Message(role="assistant", content=text))
|
||||||
elif result.action == Action.REQLLM: # 调用函数后再请求llm生成回复
|
elif result.action == Action.REQLLM: # 调用函数后再请求llm生成回复
|
||||||
|
|
||||||
text = result.result
|
text = result.result
|
||||||
if text is not None and len(text) > 0:
|
if text is not None and len(text) > 0:
|
||||||
function_id = function_call_data["id"]
|
function_id = function_call_data["id"]
|
||||||
@@ -671,18 +743,14 @@ class ConnectionHandler:
|
|||||||
Message(role="tool", tool_call_id=function_id, content=text)
|
Message(role="tool", tool_call_id=function_id, content=text)
|
||||||
)
|
)
|
||||||
self.chat_with_function_calling(text, tool_call=True)
|
self.chat_with_function_calling(text, tool_call=True)
|
||||||
elif result.action == Action.NOTFOUND:
|
elif result.action == Action.NOTFOUND or result.action == Action.ERROR:
|
||||||
text = result.result
|
text = result.result
|
||||||
self.recode_first_last_text(text, text_index)
|
self.recode_first_last_text(text, text_index)
|
||||||
future = self.executor.submit(self.speak_and_play, text, text_index)
|
future = self.executor.submit(self.speak_and_play, text, text_index)
|
||||||
self.tts_queue.put(future)
|
self.tts_queue.put(future)
|
||||||
self.dialogue.put(Message(role="assistant", content=text))
|
self.dialogue.put(Message(role="assistant", content=text))
|
||||||
else:
|
else:
|
||||||
text = result.result
|
pass
|
||||||
self.recode_first_last_text(text, text_index)
|
|
||||||
future = self.executor.submit(self.speak_and_play, text, text_index)
|
|
||||||
self.tts_queue.put(future)
|
|
||||||
self.dialogue.put(Message(role="assistant", content=text))
|
|
||||||
|
|
||||||
def _tts_priority_thread(self):
|
def _tts_priority_thread(self):
|
||||||
while not self.stop_event.is_set():
|
while not self.stop_event.is_set():
|
||||||
@@ -780,6 +848,8 @@ class ConnectionHandler:
|
|||||||
self.logger.bind(tag=TAG).error(f"tts转换失败,{text}")
|
self.logger.bind(tag=TAG).error(f"tts转换失败,{text}")
|
||||||
return None, text, text_index
|
return None, text, text_index
|
||||||
self.logger.bind(tag=TAG).debug(f"TTS 文件生成完毕: {tts_file}")
|
self.logger.bind(tag=TAG).debug(f"TTS 文件生成完毕: {tts_file}")
|
||||||
|
if self.max_output_size > 0:
|
||||||
|
add_device_output(self.headers.get("device-id"), len(text))
|
||||||
return tts_file, text, text_index
|
return tts_file, text, text_index
|
||||||
|
|
||||||
def clearSpeakStatus(self):
|
def clearSpeakStatus(self):
|
||||||
@@ -796,6 +866,11 @@ class ConnectionHandler:
|
|||||||
|
|
||||||
async def close(self, ws=None):
|
async def close(self, ws=None):
|
||||||
"""资源清理方法"""
|
"""资源清理方法"""
|
||||||
|
# 取消超时任务
|
||||||
|
if self.timeout_task:
|
||||||
|
self.timeout_task.cancel()
|
||||||
|
self.timeout_task = None
|
||||||
|
|
||||||
# 清理MCP资源
|
# 清理MCP资源
|
||||||
if hasattr(self, "mcp_manager") and self.mcp_manager:
|
if hasattr(self, "mcp_manager") and self.mcp_manager:
|
||||||
await self.mcp_manager.cleanup_all()
|
await self.mcp_manager.cleanup_all()
|
||||||
@@ -849,3 +924,15 @@ class ConnectionHandler:
|
|||||||
self.close_after_chat = True
|
self.close_after_chat = True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.bind(tag=TAG).error(f"Chat and close error: {str(e)}")
|
self.logger.bind(tag=TAG).error(f"Chat and close error: {str(e)}")
|
||||||
|
|
||||||
|
async def _check_timeout(self):
|
||||||
|
"""检查连接超时"""
|
||||||
|
try:
|
||||||
|
while not self.stop_event.is_set():
|
||||||
|
await asyncio.sleep(self.timeout_seconds)
|
||||||
|
if not self.stop_event.is_set():
|
||||||
|
self.logger.bind(tag=TAG).info("连接超时,准备关闭")
|
||||||
|
await self.close(self.websocket)
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.bind(tag=TAG).error(f"超时检查任务出错: {e}")
|
||||||
|
|||||||
@@ -81,6 +81,8 @@ async def wakeupWordsResponse(conn):
|
|||||||
"""唤醒词响应"""
|
"""唤醒词响应"""
|
||||||
wakeup_word = random.choice(WAKEUP_CONFIG["words"])
|
wakeup_word = random.choice(WAKEUP_CONFIG["words"])
|
||||||
result = conn.llm.response_no_stream(conn.config["prompt"], wakeup_word)
|
result = conn.llm.response_no_stream(conn.config["prompt"], wakeup_word)
|
||||||
|
if result is None or result == "":
|
||||||
|
return
|
||||||
tts_file = await asyncio.to_thread(conn.tts.to_tts, result)
|
tts_file = await asyncio.to_thread(conn.tts.to_tts, result)
|
||||||
|
|
||||||
if tts_file is not None and os.path.exists(tts_file):
|
if tts_file is not None and os.path.exists(tts_file):
|
||||||
|
|||||||
@@ -144,33 +144,35 @@ class IotDescriptor:
|
|||||||
self.methods = []
|
self.methods = []
|
||||||
|
|
||||||
# 根据描述创建属性
|
# 根据描述创建属性
|
||||||
for key, value in properties.items():
|
if properties is not None:
|
||||||
property_item = globals()[key] = {}
|
for key, value in properties.items():
|
||||||
property_item["name"] = key
|
property_item = globals()[key] = {}
|
||||||
property_item["description"] = value["description"]
|
property_item["name"] = key
|
||||||
if value["type"] == "number":
|
property_item["description"] = value["description"]
|
||||||
property_item["value"] = 0
|
if value["type"] == "number":
|
||||||
elif value["type"] == "boolean":
|
property_item["value"] = 0
|
||||||
property_item["value"] = False
|
elif value["type"] == "boolean":
|
||||||
else:
|
property_item["value"] = False
|
||||||
property_item["value"] = ""
|
else:
|
||||||
self.properties.append(property_item)
|
property_item["value"] = ""
|
||||||
|
self.properties.append(property_item)
|
||||||
|
|
||||||
# 根据描述创建方法
|
# 根据描述创建方法
|
||||||
for key, value in methods.items():
|
if methods is not None:
|
||||||
method = globals()[key] = {}
|
for key, value in methods.items():
|
||||||
method["description"] = value["description"]
|
method = globals()[key] = {}
|
||||||
method["name"] = key
|
method["description"] = value["description"]
|
||||||
for k, v in value["parameters"].items():
|
method["name"] = key
|
||||||
method[k] = {}
|
for k, v in value["parameters"].items():
|
||||||
method[k]["description"] = v["description"]
|
method[k] = {}
|
||||||
if v["type"] == "number":
|
method[k]["description"] = v["description"]
|
||||||
method[k]["value"] = 0
|
if v["type"] == "number":
|
||||||
elif v["type"] == "boolean":
|
method[k]["value"] = 0
|
||||||
method[k]["value"] = False
|
elif v["type"] == "boolean":
|
||||||
else:
|
method[k]["value"] = False
|
||||||
method[k]["value"] = ""
|
else:
|
||||||
self.methods.append(method)
|
method[k]["value"] = ""
|
||||||
|
self.methods.append(method)
|
||||||
|
|
||||||
|
|
||||||
def register_device_type(descriptor):
|
def register_device_type(descriptor):
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import time
|
|||||||
from core.utils.util import remove_punctuation_and_length
|
from core.utils.util import remove_punctuation_and_length
|
||||||
from core.handle.sendAudioHandle import send_stt_message
|
from core.handle.sendAudioHandle import send_stt_message
|
||||||
from core.handle.intentHandler import handle_user_intent
|
from core.handle.intentHandler import handle_user_intent
|
||||||
|
from core.utils.output_counter import check_device_output_limit
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
logger = setup_logging()
|
logger = setup_logging()
|
||||||
@@ -35,9 +36,7 @@ async def handleAudioMessage(conn, audio):
|
|||||||
if len(conn.asr_audio) < 15:
|
if len(conn.asr_audio) < 15:
|
||||||
conn.asr_server_receive = True
|
conn.asr_server_receive = True
|
||||||
else:
|
else:
|
||||||
text, file_path = await conn.asr.speech_to_text(
|
text, _ = await conn.asr.speech_to_text(conn.asr_audio, conn.session_id)
|
||||||
conn.asr_audio, conn.session_id
|
|
||||||
)
|
|
||||||
logger.bind(tag=TAG).info(f"识别文本: {text}")
|
logger.bind(tag=TAG).info(f"识别文本: {text}")
|
||||||
text_len, _ = remove_punctuation_and_length(text)
|
text_len, _ = remove_punctuation_and_length(text)
|
||||||
if text_len > 0:
|
if text_len > 0:
|
||||||
@@ -49,6 +48,18 @@ async def handleAudioMessage(conn, audio):
|
|||||||
|
|
||||||
|
|
||||||
async def startToChat(conn, text):
|
async def startToChat(conn, text):
|
||||||
|
if conn.need_bind:
|
||||||
|
await check_bind_device(conn)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 如果当日的输出字数大于限定的字数
|
||||||
|
if conn.max_output_size > 0:
|
||||||
|
if check_device_output_limit(
|
||||||
|
conn.headers.get("device-id"), conn.max_output_size
|
||||||
|
):
|
||||||
|
await max_out_size(conn)
|
||||||
|
return
|
||||||
|
|
||||||
# 首先进行意图分析
|
# 首先进行意图分析
|
||||||
intent_handled = await handle_user_intent(conn, text)
|
intent_handled = await handle_user_intent(conn, text)
|
||||||
|
|
||||||
@@ -85,3 +96,56 @@ async def no_voice_close_connect(conn):
|
|||||||
"请你以“时间过得真快”未来头,用富有感情、依依不舍的话来结束这场对话吧。"
|
"请你以“时间过得真快”未来头,用富有感情、依依不舍的话来结束这场对话吧。"
|
||||||
)
|
)
|
||||||
await startToChat(conn, prompt)
|
await startToChat(conn, prompt)
|
||||||
|
|
||||||
|
|
||||||
|
async def max_out_size(conn):
|
||||||
|
text = "不好意思,我现在有点事情要忙,明天这个时候我们再聊,约好了哦!明天不见不散,拜拜!"
|
||||||
|
await send_stt_message(conn, text)
|
||||||
|
conn.tts_first_text_index = 0
|
||||||
|
conn.tts_last_text_index = 0
|
||||||
|
conn.llm_finish_task = True
|
||||||
|
file_path = "config/assets/max_output_size.wav"
|
||||||
|
opus_packets, _ = conn.tts.audio_to_opus_data(file_path)
|
||||||
|
conn.audio_play_queue.put((opus_packets, text, 0))
|
||||||
|
conn.close_after_chat = True
|
||||||
|
|
||||||
|
|
||||||
|
async def check_bind_device(conn):
|
||||||
|
if conn.bind_code:
|
||||||
|
# 确保bind_code是6位数字
|
||||||
|
if len(conn.bind_code) != 6:
|
||||||
|
logger.bind(tag=TAG).error(f"无效的绑定码格式: {conn.bind_code}")
|
||||||
|
text = "绑定码格式错误,请检查配置。"
|
||||||
|
await send_stt_message(conn, text)
|
||||||
|
return
|
||||||
|
|
||||||
|
text = f"请登录控制面板,输入{conn.bind_code},绑定设备。"
|
||||||
|
await send_stt_message(conn, text)
|
||||||
|
conn.tts_first_text_index = 0
|
||||||
|
conn.tts_last_text_index = 6
|
||||||
|
conn.llm_finish_task = True
|
||||||
|
|
||||||
|
# 播放提示音
|
||||||
|
music_path = "config/assets/bind_code.wav"
|
||||||
|
opus_packets, _ = conn.tts.audio_to_opus_data(music_path)
|
||||||
|
conn.audio_play_queue.put((opus_packets, text, 0))
|
||||||
|
|
||||||
|
# 逐个播放数字
|
||||||
|
for i in range(6): # 确保只播放6位数字
|
||||||
|
try:
|
||||||
|
digit = conn.bind_code[i]
|
||||||
|
num_path = f"config/assets/bind_code/{digit}.wav"
|
||||||
|
num_packets, _ = conn.tts.audio_to_opus_data(num_path)
|
||||||
|
conn.audio_play_queue.put((num_packets, None, i + 1))
|
||||||
|
except Exception as e:
|
||||||
|
logger.bind(tag=TAG).error(f"播放数字音频失败: {e}")
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
text = f"没有找到该设备的版本信息,请正确配置 OTA地址,然后重新编译固件。"
|
||||||
|
await send_stt_message(conn, text)
|
||||||
|
conn.tts_first_text_index = 0
|
||||||
|
conn.tts_last_text_index = 0
|
||||||
|
conn.llm_finish_task = True
|
||||||
|
music_path = "config/assets/bind_not_found.wav"
|
||||||
|
opus_packets, _ = conn.tts.audio_to_opus_data(music_path)
|
||||||
|
conn.audio_play_queue.put((opus_packets, text, 0))
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import json
|
|||||||
import asyncio
|
import asyncio
|
||||||
import time
|
import time
|
||||||
from core.utils.util import (
|
from core.utils.util import (
|
||||||
remove_punctuation_and_length,
|
|
||||||
get_string_no_punctuation_or_emoji,
|
get_string_no_punctuation_or_emoji,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -35,4 +35,5 @@ class LLMProviderBase(ABC):
|
|||||||
"""
|
"""
|
||||||
# For providers that don't support functions, just return regular response
|
# For providers that don't support functions, just return regular response
|
||||||
for token in self.response(session_id, dialogue):
|
for token in self.response(session_id, dialogue):
|
||||||
yield {"type": "content", "content": token}
|
yield token, None
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import json
|
|||||||
import re
|
import re
|
||||||
from core.providers.llm.base import LLMProviderBase
|
from core.providers.llm.base import LLMProviderBase
|
||||||
import os
|
import os
|
||||||
|
|
||||||
# official coze sdk for Python [cozepy](https://github.com/coze-dev/coze-py)
|
# official coze sdk for Python [cozepy](https://github.com/coze-dev/coze-py)
|
||||||
from cozepy import COZE_CN_BASE_URL
|
from cozepy import COZE_CN_BASE_URL
|
||||||
from cozepy import Coze, TokenAuth, Message, ChatStatus, MessageContentType, ChatEventType # noqa
|
from cozepy import Coze, TokenAuth, Message, ChatStatus, MessageContentType, ChatEventType # noqa
|
||||||
|
from core.providers.llm.system_prompt import get_system_prompt_for_function
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
logger = setup_logging()
|
logger = setup_logging()
|
||||||
@@ -15,8 +17,8 @@ logger = setup_logging()
|
|||||||
class LLMProvider(LLMProviderBase):
|
class LLMProvider(LLMProviderBase):
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
self.personal_access_token = config.get("personal_access_token")
|
self.personal_access_token = config.get("personal_access_token")
|
||||||
self.bot_id = config.get("bot_id")
|
self.bot_id = str(config.get("bot_id"))
|
||||||
self.user_id = config.get("user_id")
|
self.user_id = str(config.get("user_id"))
|
||||||
self.session_conversation_map = {} # 存储session_id和conversation_id的映射
|
self.session_conversation_map = {} # 存储session_id和conversation_id的映射
|
||||||
|
|
||||||
def response(self, session_id, dialogue):
|
def response(self, session_id, dialogue):
|
||||||
@@ -30,10 +32,7 @@ class LLMProvider(LLMProviderBase):
|
|||||||
|
|
||||||
# 如果没有找到conversation_id,则创建新的对话
|
# 如果没有找到conversation_id,则创建新的对话
|
||||||
if not conversation_id:
|
if not conversation_id:
|
||||||
conversation = coze.conversations.create(
|
conversation = coze.conversations.create(messages=[])
|
||||||
messages=[
|
|
||||||
]
|
|
||||||
)
|
|
||||||
conversation_id = conversation.id
|
conversation_id = conversation.id
|
||||||
self.session_conversation_map[session_id] = conversation_id # 更新映射
|
self.session_conversation_map[session_id] = conversation_id # 更新映射
|
||||||
|
|
||||||
@@ -48,3 +47,23 @@ class LLMProvider(LLMProviderBase):
|
|||||||
if event.event == ChatEventType.CONVERSATION_MESSAGE_DELTA:
|
if event.event == ChatEventType.CONVERSATION_MESSAGE_DELTA:
|
||||||
print(event.message.content, end="", flush=True)
|
print(event.message.content, end="", flush=True)
|
||||||
yield event.message.content
|
yield event.message.content
|
||||||
|
|
||||||
|
def response_with_functions(self, session_id, dialogue, functions=None):
|
||||||
|
if len(dialogue) == 2 and functions is not None and len(functions) > 0:
|
||||||
|
# 第一次调用llm, 取最后一条用户消息,附加tool提示词
|
||||||
|
last_msg = dialogue[-1]["content"]
|
||||||
|
function_str = json.dumps(functions, ensure_ascii=False)
|
||||||
|
modify_msg = get_system_prompt_for_function(function_str) + last_msg
|
||||||
|
dialogue[-1]["content"] = modify_msg
|
||||||
|
|
||||||
|
# 如果最后一个是 role="tool",附加到user上
|
||||||
|
if len(dialogue) > 1 and dialogue[-1]["role"] == "tool":
|
||||||
|
assistant_msg = "\ntool call result: " + dialogue[-1]["content"] + "\n\n"
|
||||||
|
while len(dialogue) > 1 :
|
||||||
|
if dialogue[-1]["role"] == "user":
|
||||||
|
dialogue[-1]["content"] = assistant_msg + dialogue[-1]["content"]
|
||||||
|
break
|
||||||
|
dialogue.pop()
|
||||||
|
|
||||||
|
for token in self.response(session_id, dialogue):
|
||||||
|
yield token, None
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import json
|
|||||||
from config.logger import setup_logging
|
from config.logger import setup_logging
|
||||||
import requests
|
import requests
|
||||||
from core.providers.llm.base import LLMProviderBase
|
from core.providers.llm.base import LLMProviderBase
|
||||||
|
from core.providers.llm.system_prompt import get_system_prompt_for_function
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
logger = setup_logging()
|
logger = setup_logging()
|
||||||
@@ -81,3 +82,23 @@ class LLMProvider(LLMProviderBase):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.bind(tag=TAG).error(f"Error in response generation: {e}")
|
logger.bind(tag=TAG).error(f"Error in response generation: {e}")
|
||||||
yield "【服务响应异常】"
|
yield "【服务响应异常】"
|
||||||
|
|
||||||
|
def response_with_functions(self, session_id, dialogue, functions=None):
|
||||||
|
if len(dialogue) == 2 and functions is not None and len(functions) > 0:
|
||||||
|
# 第一次调用llm, 取最后一条用户消息,附加tool提示词
|
||||||
|
last_msg = dialogue[-1]["content"]
|
||||||
|
function_str = json.dumps(functions, ensure_ascii=False)
|
||||||
|
modify_msg = get_system_prompt_for_function(function_str) + last_msg
|
||||||
|
dialogue[-1]["content"] = modify_msg
|
||||||
|
|
||||||
|
# 如果最后一个是 role="tool",附加到user上
|
||||||
|
if len(dialogue) > 1 and dialogue[-1]["role"] == "tool":
|
||||||
|
assistant_msg = "\ntool call result: " + dialogue[-1]["content"] + "\n\n"
|
||||||
|
while len(dialogue) > 1 :
|
||||||
|
if dialogue[-1]["role"] == "user":
|
||||||
|
dialogue[-1]["content"] = assistant_msg + dialogue[-1]["content"]
|
||||||
|
break
|
||||||
|
dialogue.pop()
|
||||||
|
|
||||||
|
for token in self.response(session_id, dialogue):
|
||||||
|
yield token, None
|
||||||
@@ -63,4 +63,4 @@ class LLMProvider(LLMProviderBase):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.bind(tag=TAG).error(f"Error in Ollama function call: {e}")
|
logger.bind(tag=TAG).error(f"Error in Ollama function call: {e}")
|
||||||
yield {"type": "content", "content": f"【Ollama服务响应异常: {str(e)}】"}
|
yield f"【Ollama服务响应异常: {str(e)}】", None
|
||||||
|
|||||||
@@ -74,4 +74,4 @@ class LLMProvider(LLMProviderBase):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.bind(tag=TAG).error(f"Error in function call streaming: {e}")
|
logger.bind(tag=TAG).error(f"Error in function call streaming: {e}")
|
||||||
yield {"type": "content", "content": f"【OpenAI服务响应异常: {e}】"}
|
yield f"【OpenAI服务响应异常: {e}】", None
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
def get_system_prompt_for_function(functions: str) -> str:
|
||||||
|
"""
|
||||||
|
生成系统提示信息
|
||||||
|
:param functions: 可用的函数列表
|
||||||
|
:return: 系统提示信息
|
||||||
|
"""
|
||||||
|
|
||||||
|
SYSTEM_PROMPT = f"""
|
||||||
|
====
|
||||||
|
|
||||||
|
TOOL USE
|
||||||
|
|
||||||
|
You have access to a set of tools that are executed upon the user's approval. You can use one tool per message, and will receive the result of that tool use in the user's response.
|
||||||
|
You use tools step-by-step to accomplish a given task, with each tool use informed by the result of the previous tool use.
|
||||||
|
|
||||||
|
# Tool Use Formatting
|
||||||
|
|
||||||
|
Tool use is formatted using JSON-style tags. The tool name is enclosed in opening and closing tags, and each parameter is similarly enclosed within its own set of tags.
|
||||||
|
Here's the structure:
|
||||||
|
|
||||||
|
<tool_call>
|
||||||
|
{{
|
||||||
|
"name": "function name",
|
||||||
|
"arguments": {{
|
||||||
|
"param1": "value1",
|
||||||
|
"param2": "value2",
|
||||||
|
// Add more parameters as needed, if parameters are required, you must provide them
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
<tool_call>
|
||||||
|
|
||||||
|
For example:
|
||||||
|
if you got tool as follow
|
||||||
|
|
||||||
|
{{
|
||||||
|
"type": "function",
|
||||||
|
"function": {{
|
||||||
|
"name": "handle_exit_intent",
|
||||||
|
"description": "当用户想结束对话或需要退出系统时调用",
|
||||||
|
"parameters": {{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {{
|
||||||
|
"say_goodbye": {{
|
||||||
|
"type": "string",
|
||||||
|
"description": "和用户友好结束对话的告别语",
|
||||||
|
}}
|
||||||
|
}},
|
||||||
|
"required": ["say_goodbye"],
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
}}
|
||||||
|
|
||||||
|
you should respond with the following format:
|
||||||
|
|
||||||
|
<tool_call>
|
||||||
|
{{
|
||||||
|
"name": "handle_exit_intent",
|
||||||
|
"arguments": {{
|
||||||
|
"say_goodbye": "再见,祝您生活愉快!"
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
</tool_call>
|
||||||
|
|
||||||
|
|
||||||
|
Always adhere to this format for the tool use to ensure proper parsing and execution.
|
||||||
|
|
||||||
|
# Tools
|
||||||
|
|
||||||
|
{functions}
|
||||||
|
|
||||||
|
# Tool Use Guidelines
|
||||||
|
|
||||||
|
1. Tools must be called in a separate message, Do not add thoughts when calling tools. The message must start with <tool_call> and end with </tool_call>, with the tool invocation JSON data in between. No additional response content is needed.
|
||||||
|
2. Choose the most appropriate tool based on the task and the tool descriptions provided. Assess if you need additional information to proceed, and which of the available tools would be most effective for gathering this information.
|
||||||
|
For example using the list_files tool is more effective than running a command like \`ls\` in the terminal. It's critical that you think about each available tool and use the one that best fits the current step in the task.
|
||||||
|
3. If multiple actions are needed, use one tool at a time per message to accomplish the task iteratively, with each tool use being informed by the result of the previous tool use. Do not assume the outcome of any tool use.
|
||||||
|
Each step must be informed by the previous step's result.
|
||||||
|
4. Formulate your tool use using the JSON format specified for each tool.
|
||||||
|
5. After each tool use, the user will respond with the result of that tool use. This result will provide you with the necessary information to continue your task or make further decisions. This response may include:
|
||||||
|
- Information about whether the tool succeeded or failed, along with any reasons for failure.
|
||||||
|
- Linter errors that may have arisen due to the changes you made, which you'll need to address.
|
||||||
|
- New terminal output in reaction to the changes, which you may need to consider or act upon.
|
||||||
|
- Any other relevant feedback or information related to the tool use.
|
||||||
|
6. ALWAYS wait for user confirmation after each tool use before proceeding. Never assume the success of a tool use without explicit confirmation of the result from the user.
|
||||||
|
7. Tool calls should contain no extra information. Only after receiving the tool's response should you integrate it into a complete reply.
|
||||||
|
|
||||||
|
It is crucial to proceed step-by-step, waiting for the user's message after each tool use before moving forward with the task. This approach allows you to:
|
||||||
|
1. Confirm the success of each step before proceeding.
|
||||||
|
2. Address any issues or errors that arise immediately.
|
||||||
|
3. Adapt your approach based on new information or unexpected results.
|
||||||
|
4. Ensure that each action builds correctly on the previous ones.
|
||||||
|
|
||||||
|
By waiting for and carefully considering the user's response after each tool use, you can react accordingly and make informed decisions about how to proceed with the task. This iterative process helps ensure the overall success and accuracy of your work.
|
||||||
|
|
||||||
|
====
|
||||||
|
|
||||||
|
USER CHAT CONTENT
|
||||||
|
|
||||||
|
The following additional message is the user's chat message, and should be followed to the best of your ability without interfering with the TOOL USE guidelines.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
return SYSTEM_PROMPT
|
||||||
@@ -8,7 +8,7 @@ from pydantic import BaseModel, Field, conint, model_validator
|
|||||||
from typing_extensions import Annotated
|
from typing_extensions import Annotated
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
from core.utils.util import check_model_key
|
from core.utils.util import check_model_key, parse_string_to_list
|
||||||
from core.providers.tts.base import TTSProviderBase
|
from core.providers.tts.base import TTSProviderBase
|
||||||
from config.logger import setup_logging
|
from config.logger import setup_logging
|
||||||
|
|
||||||
@@ -86,8 +86,8 @@ class TTSProvider(TTSProviderBase):
|
|||||||
super().__init__(config, delete_audio_file)
|
super().__init__(config, delete_audio_file)
|
||||||
|
|
||||||
self.reference_id = config.get("reference_id")
|
self.reference_id = config.get("reference_id")
|
||||||
self.reference_audio = config.get("reference_audio", [])
|
self.reference_audio = parse_string_to_list(config.get("reference_audio"))
|
||||||
self.reference_text = config.get("reference_text", [])
|
self.reference_text = parse_string_to_list(config.get("reference_text"))
|
||||||
self.format = config.get("format", "wav")
|
self.format = config.get("format", "wav")
|
||||||
self.channels = int(config.get("channels", 1))
|
self.channels = int(config.get("channels", 1))
|
||||||
self.rate = int(config.get("rate", 44100))
|
self.rate = int(config.get("rate", 44100))
|
||||||
@@ -101,9 +101,13 @@ class TTSProvider(TTSProviderBase):
|
|||||||
self.top_p = float(config.get("top_p", 0.7))
|
self.top_p = float(config.get("top_p", 0.7))
|
||||||
self.repetition_penalty = float(config.get("repetition_penalty", 1.2))
|
self.repetition_penalty = float(config.get("repetition_penalty", 1.2))
|
||||||
self.temperature = float(config.get("temperature", 0.7))
|
self.temperature = float(config.get("temperature", 0.7))
|
||||||
self.streaming = bool(config.get("streaming", False))
|
self.streaming = str(config.get("streaming", False)).lower() in (
|
||||||
|
"true",
|
||||||
|
"1",
|
||||||
|
"yes",
|
||||||
|
)
|
||||||
self.use_memory_cache = config.get("use_memory_cache", "on")
|
self.use_memory_cache = config.get("use_memory_cache", "on")
|
||||||
self.seed = config.get("seed")
|
self.seed = config.get("seed") or None
|
||||||
self.api_url = config.get("api_url", "http://127.0.0.1:8080/v1/tts")
|
self.api_url = config.get("api_url", "http://127.0.0.1:8080/v1/tts")
|
||||||
|
|
||||||
def generate_filename(self, extension=".wav"):
|
def generate_filename(self, extension=".wav"):
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import requests
|
|||||||
from config.logger import setup_logging
|
from config.logger import setup_logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from core.providers.tts.base import TTSProviderBase
|
from core.providers.tts.base import TTSProviderBase
|
||||||
|
from core.utils.util import parse_string_to_list
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
logger = setup_logging()
|
logger = setup_logging()
|
||||||
@@ -25,14 +26,33 @@ class TTSProvider(TTSProviderBase):
|
|||||||
self.text_split_method = config.get("text_split_method", "cut0")
|
self.text_split_method = config.get("text_split_method", "cut0")
|
||||||
self.batch_size = int(config.get("batch_size", 1))
|
self.batch_size = int(config.get("batch_size", 1))
|
||||||
self.batch_threshold = float(config.get("batch_threshold", 0.75))
|
self.batch_threshold = float(config.get("batch_threshold", 0.75))
|
||||||
self.split_bucket = bool(config.get("split_bucket", True))
|
|
||||||
self.return_fragment = bool(config.get("return_fragment", False))
|
self.split_bucket = str(config.get("split_bucket", True)).lower() in (
|
||||||
|
"true",
|
||||||
|
"1",
|
||||||
|
"yes",
|
||||||
|
)
|
||||||
|
self.return_fragment = str(config.get("return_fragment", False)).lower() in (
|
||||||
|
"true",
|
||||||
|
"1",
|
||||||
|
"yes",
|
||||||
|
)
|
||||||
self.speed_factor = float(config.get("speed_factor", 1.0))
|
self.speed_factor = float(config.get("speed_factor", 1.0))
|
||||||
self.streaming_mode = bool(config.get("streaming_mode", False))
|
self.streaming_mode = str(config.get("streaming_mode", False)).lower() in (
|
||||||
|
"true",
|
||||||
|
"1",
|
||||||
|
"yes",
|
||||||
|
)
|
||||||
self.seed = int(config.get("seed", -1))
|
self.seed = int(config.get("seed", -1))
|
||||||
self.parallel_infer = bool(config.get("parallel_infer", True))
|
self.parallel_infer = str(config.get("parallel_infer", True)).lower() in (
|
||||||
|
"true",
|
||||||
|
"1",
|
||||||
|
"yes",
|
||||||
|
)
|
||||||
self.repetition_penalty = float(config.get("repetition_penalty", 1.35))
|
self.repetition_penalty = float(config.get("repetition_penalty", 1.35))
|
||||||
self.aux_ref_audio_paths = config.get("aux_ref_audio_paths", [])
|
self.aux_ref_audio_paths = parse_string_to_list(
|
||||||
|
config.get("aux_ref_audio_paths")
|
||||||
|
)
|
||||||
|
|
||||||
def generate_filename(self, extension=".wav"):
|
def generate_filename(self, extension=".wav"):
|
||||||
return os.path.join(
|
return os.path.join(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import requests
|
|||||||
from config.logger import setup_logging
|
from config.logger import setup_logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from core.providers.tts.base import TTSProviderBase
|
from core.providers.tts.base import TTSProviderBase
|
||||||
|
from core.utils.util import parse_string_to_list
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
logger = setup_logging()
|
logger = setup_logging()
|
||||||
@@ -22,9 +23,9 @@ class TTSProvider(TTSProviderBase):
|
|||||||
self.temperature = float(config.get("temperature", 1.0))
|
self.temperature = float(config.get("temperature", 1.0))
|
||||||
self.cut_punc = config.get("cut_punc", "")
|
self.cut_punc = config.get("cut_punc", "")
|
||||||
self.speed = float(config.get("speed", 1.0))
|
self.speed = float(config.get("speed", 1.0))
|
||||||
self.inp_refs = config.get("inp_refs", [])
|
self.inp_refs = parse_string_to_list(config.get("inp_refs"))
|
||||||
self.sample_steps = int(config.get("sample_steps", 32))
|
self.sample_steps = int(config.get("sample_steps", 32))
|
||||||
self.if_sr = bool(config.get("if_sr", False))
|
self.if_sr = str(config.get("if_sr", False)).lower() in ("true", "1", "yes")
|
||||||
|
|
||||||
def generate_filename(self, extension=".wav"):
|
def generate_filename(self, extension=".wav"):
|
||||||
return os.path.join(
|
return os.path.join(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import json
|
|||||||
import requests
|
import requests
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from core.providers.tts.base import TTSProviderBase
|
from core.providers.tts.base import TTSProviderBase
|
||||||
|
from core.utils.util import parse_string_to_list
|
||||||
|
|
||||||
|
|
||||||
class TTSProvider(TTSProviderBase):
|
class TTSProvider(TTSProviderBase):
|
||||||
@@ -40,7 +41,7 @@ class TTSProvider(TTSProviderBase):
|
|||||||
**config.get("pronunciation_dict", {}),
|
**config.get("pronunciation_dict", {}),
|
||||||
}
|
}
|
||||||
self.audio_setting = {**defult_audio_setting, **config.get("audio_setting", {})}
|
self.audio_setting = {**defult_audio_setting, **config.get("audio_setting", {})}
|
||||||
self.timber_weights = config.get("timber_weights", [])
|
self.timber_weights = parse_string_to_list(config.get("timber_weights"))
|
||||||
|
|
||||||
if self.voice_id:
|
if self.voice_id:
|
||||||
self.voice_setting["voice_id"] = self.voice_id
|
self.voice_setting["voice_id"] = self.voice_id
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class TTSProvider(TTSProviderBase):
|
|||||||
self.voice = config.get("voice")
|
self.voice = config.get("voice")
|
||||||
self.response_format = config.get("response_format")
|
self.response_format = config.get("response_format")
|
||||||
self.sample_rate = config.get("sample_rate")
|
self.sample_rate = config.get("sample_rate")
|
||||||
self.speed = float(config.get("speed"))
|
self.speed = float(config.get("speed", 1.0))
|
||||||
self.gain = config.get("gain")
|
self.gain = config.get("gain")
|
||||||
|
|
||||||
self.host = "api.siliconflow.cn"
|
self.host = "api.siliconflow.cn"
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class TTSProvider(TTSProviderBase):
|
|||||||
self.to_lang = config.get("to_lang")
|
self.to_lang = config.get("to_lang")
|
||||||
self.volume_change_dB = int(config.get("volume_change_dB", 0))
|
self.volume_change_dB = int(config.get("volume_change_dB", 0))
|
||||||
self.speed_factor = int(config.get("speed_factor", 1))
|
self.speed_factor = int(config.get("speed_factor", 1))
|
||||||
self.stream = bool(config.get("stream", False))
|
self.stream = str(config.get("stream", False)).lower() in ("true", "1", "yes")
|
||||||
self.output_file = config.get("output_dir")
|
self.output_file = config.get("output_dir")
|
||||||
self.pitch_factor = int(config.get("pitch_factor", 0))
|
self.pitch_factor = int(config.get("pitch_factor", 0))
|
||||||
self.format = config.get("format", "mp3")
|
self.format = config.get("format", "mp3")
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import datetime
|
||||||
|
from typing import Dict, Tuple
|
||||||
|
|
||||||
|
# 全局字典,用于存储每个设备的每日输出字数
|
||||||
|
_device_daily_output: Dict[Tuple[str, datetime.date], int] = {}
|
||||||
|
# 记录最后一次检查的日期
|
||||||
|
_last_check_date: datetime.date = None
|
||||||
|
|
||||||
|
|
||||||
|
def reset_device_output():
|
||||||
|
"""
|
||||||
|
重置所有设备的每日输出字数
|
||||||
|
每天0点调用此函数
|
||||||
|
"""
|
||||||
|
_device_daily_output.clear()
|
||||||
|
|
||||||
|
|
||||||
|
def get_device_output(device_id: str) -> int:
|
||||||
|
"""
|
||||||
|
获取设备当日的输出字数
|
||||||
|
"""
|
||||||
|
current_date = datetime.datetime.now().date()
|
||||||
|
return _device_daily_output.get((device_id, current_date), 0)
|
||||||
|
|
||||||
|
|
||||||
|
def add_device_output(device_id: str, char_count: int):
|
||||||
|
"""
|
||||||
|
增加设备的输出字数
|
||||||
|
"""
|
||||||
|
current_date = datetime.datetime.now().date()
|
||||||
|
global _last_check_date
|
||||||
|
|
||||||
|
# 如果是第一次调用或者日期发生变化,清空计数器
|
||||||
|
if _last_check_date is None or _last_check_date != current_date:
|
||||||
|
_device_daily_output.clear()
|
||||||
|
_last_check_date = current_date
|
||||||
|
|
||||||
|
current_count = _device_daily_output.get((device_id, current_date), 0)
|
||||||
|
_device_daily_output[(device_id, current_date)] = current_count + char_count
|
||||||
|
|
||||||
|
|
||||||
|
def check_device_output_limit(device_id: str, max_output_size: int) -> bool:
|
||||||
|
"""
|
||||||
|
检查设备是否超过输出限制
|
||||||
|
:return: True 如果超过限制,False 如果未超过
|
||||||
|
"""
|
||||||
|
if not device_id:
|
||||||
|
return False
|
||||||
|
current_output = get_device_output(device_id)
|
||||||
|
return current_output >= max_output_size
|
||||||
@@ -163,6 +163,24 @@ def check_model_key(modelType, modelKey):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def parse_string_to_list(value, separator=";"):
|
||||||
|
"""
|
||||||
|
将输入值转换为列表
|
||||||
|
Args:
|
||||||
|
value: 输入值,可以是 None、字符串或列表
|
||||||
|
separator: 分隔符,默认为分号
|
||||||
|
Returns:
|
||||||
|
list: 处理后的列表
|
||||||
|
"""
|
||||||
|
if value is None or value == "":
|
||||||
|
return []
|
||||||
|
elif isinstance(value, str):
|
||||||
|
return [item.strip() for item in value.split(separator) if item.strip()]
|
||||||
|
elif isinstance(value, list):
|
||||||
|
return value
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
def check_ffmpeg_installed():
|
def check_ffmpeg_installed():
|
||||||
ffmpeg_installed = False
|
ffmpeg_installed = False
|
||||||
try:
|
try:
|
||||||
@@ -193,7 +211,7 @@ def check_ffmpeg_installed():
|
|||||||
def extract_json_from_string(input_string):
|
def extract_json_from_string(input_string):
|
||||||
"""提取字符串中的 JSON 部分"""
|
"""提取字符串中的 JSON 部分"""
|
||||||
pattern = r"(\{.*\})"
|
pattern = r"(\{.*\})"
|
||||||
match = re.search(pattern, input_string)
|
match = re.search(pattern, input_string, re.DOTALL) #添加 re.DOTALL
|
||||||
if match:
|
if match:
|
||||||
return match.group(1) # 返回提取的 JSON 字符串
|
return match.group(1) # 返回提取的 JSON 字符串
|
||||||
return None
|
return None
|
||||||
@@ -222,80 +240,93 @@ def initialize_modules(
|
|||||||
|
|
||||||
# 初始化TTS模块
|
# 初始化TTS模块
|
||||||
if init_tts:
|
if init_tts:
|
||||||
|
select_tts_module = config["selected_module"]["TTS"]
|
||||||
tts_type = (
|
tts_type = (
|
||||||
config["selected_module"]["TTS"]
|
select_tts_module
|
||||||
if "type" not in config["TTS"][config["selected_module"]["TTS"]]
|
if "type" not in config["TTS"][select_tts_module]
|
||||||
else config["TTS"][config["selected_module"]["TTS"]]["type"]
|
else config["TTS"][select_tts_module]["type"]
|
||||||
)
|
)
|
||||||
modules["tts"] = tts.create_instance(
|
modules["tts"] = tts.create_instance(
|
||||||
tts_type,
|
tts_type,
|
||||||
config["TTS"][config["selected_module"]["TTS"]],
|
config["TTS"][select_tts_module],
|
||||||
bool(config.get("delete_audio", True)),
|
str(config.get("delete_audio", True)).lower() in ("true", "1", "yes"),
|
||||||
)
|
)
|
||||||
logger.bind(tag=TAG).info(f"初始化组件: tts成功")
|
logger.bind(tag=TAG).info(f"初始化组件: tts成功 {select_tts_module}")
|
||||||
|
|
||||||
# 初始化LLM模块
|
# 初始化LLM模块
|
||||||
if init_llm:
|
if init_llm:
|
||||||
|
select_llm_module = config["selected_module"]["LLM"]
|
||||||
llm_type = (
|
llm_type = (
|
||||||
config["selected_module"]["LLM"]
|
select_llm_module
|
||||||
if "type" not in config["LLM"][config["selected_module"]["LLM"]]
|
if "type" not in config["LLM"][select_llm_module]
|
||||||
else config["LLM"][config["selected_module"]["LLM"]]["type"]
|
else config["LLM"][select_llm_module]["type"]
|
||||||
)
|
)
|
||||||
modules["llm"] = llm.create_instance(
|
modules["llm"] = llm.create_instance(
|
||||||
llm_type,
|
llm_type,
|
||||||
config["LLM"][config["selected_module"]["LLM"]],
|
config["LLM"][select_llm_module],
|
||||||
)
|
)
|
||||||
logger.bind(tag=TAG).info(f"初始化组件: llm成功")
|
logger.bind(tag=TAG).info(f"初始化组件: llm成功 {select_llm_module}")
|
||||||
|
|
||||||
# 初始化Intent模块
|
# 初始化Intent模块
|
||||||
if init_intent:
|
if init_intent:
|
||||||
|
select_intent_module = config["selected_module"]["Intent"]
|
||||||
intent_type = (
|
intent_type = (
|
||||||
config["selected_module"]["Intent"]
|
select_intent_module
|
||||||
if "type" not in config["Intent"][config["selected_module"]["Intent"]]
|
if "type" not in config["Intent"][select_intent_module]
|
||||||
else config["Intent"][config["selected_module"]["Intent"]]["type"]
|
else config["Intent"][select_intent_module]["type"]
|
||||||
)
|
)
|
||||||
modules["intent"] = intent.create_instance(
|
modules["intent"] = intent.create_instance(
|
||||||
intent_type,
|
intent_type,
|
||||||
config["Intent"][config["selected_module"]["Intent"]],
|
config["Intent"][select_intent_module],
|
||||||
)
|
)
|
||||||
logger.bind(tag=TAG).info(f"初始化组件: intent成功")
|
logger.bind(tag=TAG).info(f"初始化组件: intent成功 {select_intent_module}")
|
||||||
|
|
||||||
# 初始化Memory模块
|
# 初始化Memory模块
|
||||||
if init_memory:
|
if init_memory:
|
||||||
|
select_memory_module = config["selected_module"]["Memory"]
|
||||||
memory_type = (
|
memory_type = (
|
||||||
config["selected_module"]["Memory"]
|
select_memory_module
|
||||||
if "type" not in config["Memory"][config["selected_module"]["Memory"]]
|
if "type" not in config["Memory"][select_memory_module]
|
||||||
else config["Memory"][config["selected_module"]["Memory"]]["type"]
|
else config["Memory"][select_memory_module]["type"]
|
||||||
)
|
)
|
||||||
modules["memory"] = memory.create_instance(
|
modules["memory"] = memory.create_instance(
|
||||||
memory_type,
|
memory_type,
|
||||||
config["Memory"][config["selected_module"]["Memory"]],
|
config["Memory"][select_memory_module],
|
||||||
)
|
)
|
||||||
logger.bind(tag=TAG).info(f"初始化组件: memory成功")
|
logger.bind(tag=TAG).info(f"初始化组件: memory成功 {select_memory_module}")
|
||||||
|
|
||||||
# 初始化VAD模块
|
# 初始化VAD模块
|
||||||
if init_vad:
|
if init_vad:
|
||||||
|
select_vad_module = config["selected_module"]["VAD"]
|
||||||
vad_type = (
|
vad_type = (
|
||||||
config["selected_module"]["VAD"]
|
select_vad_module
|
||||||
if "type" not in config["VAD"][config["selected_module"]["VAD"]]
|
if "type" not in config["VAD"][select_vad_module]
|
||||||
else config["VAD"][config["selected_module"]["VAD"]]["type"]
|
else config["VAD"][select_vad_module]["type"]
|
||||||
)
|
)
|
||||||
modules["vad"] = vad.create_instance(
|
modules["vad"] = vad.create_instance(
|
||||||
vad_type,
|
vad_type,
|
||||||
config["VAD"][config["selected_module"]["VAD"]],
|
config["VAD"][select_vad_module],
|
||||||
)
|
)
|
||||||
logger.bind(tag=TAG).info(f"初始化组件: vad成功")
|
logger.bind(tag=TAG).info(f"初始化组件: vad成功 {select_vad_module}")
|
||||||
|
|
||||||
# 初始化ASR模块
|
# 初始化ASR模块
|
||||||
if init_asr:
|
if init_asr:
|
||||||
|
select_asr_module = config["selected_module"]["ASR"]
|
||||||
asr_type = (
|
asr_type = (
|
||||||
config["selected_module"]["ASR"]
|
select_asr_module
|
||||||
if "type" not in config["ASR"][config["selected_module"]["ASR"]]
|
if "type" not in config["ASR"][select_asr_module]
|
||||||
else config["ASR"][config["selected_module"]["ASR"]]["type"]
|
else config["ASR"][select_asr_module]["type"]
|
||||||
)
|
)
|
||||||
modules["asr"] = asr.create_instance(
|
modules["asr"] = asr.create_instance(
|
||||||
asr_type,
|
asr_type,
|
||||||
config["ASR"][config["selected_module"]["ASR"]],
|
config["ASR"][select_asr_module],
|
||||||
bool(config.get("delete_audio", True)),
|
str(config.get("delete_audio", True)).lower() in ("true", "1", "yes"),
|
||||||
)
|
)
|
||||||
logger.bind(tag=TAG).info(f"初始化组件: asr成功")
|
logger.bind(tag=TAG).info(f"初始化组件: asr成功 {select_asr_module}")
|
||||||
|
|
||||||
|
# 初始化自定义prompt
|
||||||
|
if config.get("prompt", None) is not None:
|
||||||
|
modules["prompt"] = config["prompt"]
|
||||||
|
logger.bind(tag=TAG).info(f"初始化组件: prompt成功 {modules['prompt'][:50]}...")
|
||||||
|
|
||||||
return modules
|
return modules
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ services:
|
|||||||
- "8002:8002"
|
- "8002:8002"
|
||||||
environment:
|
environment:
|
||||||
- TZ=Asia/Shanghai
|
- TZ=Asia/Shanghai
|
||||||
##记得改mysql和redis IP 密码
|
|
||||||
- SPRING_DATASOURCE_DRUID_URL=jdbc:mysql://xiaozhi-esp32-server-db:3306/xiaozhi_esp32_server?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true&connectTimeout=30000&socketTimeout=30000&autoReconnect=true&failOverReadOnly=false&maxReconnects=10
|
- SPRING_DATASOURCE_DRUID_URL=jdbc:mysql://xiaozhi-esp32-server-db:3306/xiaozhi_esp32_server?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true&connectTimeout=30000&socketTimeout=30000&autoReconnect=true&failOverReadOnly=false&maxReconnects=10
|
||||||
- SPRING_DATASOURCE_DRUID_USERNAME=root
|
- SPRING_DATASOURCE_DRUID_USERNAME=root
|
||||||
- SPRING_DATASOURCE_DRUID_PASSWORD=123456
|
- SPRING_DATASOURCE_DRUID_PASSWORD=123456
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import traceback
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from core.utils import p3
|
from core.utils import p3
|
||||||
from core.handle.sendAudioHandle import send_stt_message
|
from core.handle.sendAudioHandle import send_stt_message
|
||||||
from plugins_func.register import register_function,ToolType, ActionResponse, Action
|
from plugins_func.register import register_function, ToolType, ActionResponse, Action
|
||||||
|
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
@@ -18,38 +18,41 @@ logger = setup_logging()
|
|||||||
MUSIC_CACHE = {}
|
MUSIC_CACHE = {}
|
||||||
|
|
||||||
play_music_function_desc = {
|
play_music_function_desc = {
|
||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
"name": "play_music",
|
"name": "play_music",
|
||||||
"description": "唱歌、听歌、播放音乐的方法。",
|
"description": "唱歌、听歌、播放音乐的方法。",
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"song_name": {
|
"song_name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "歌曲名称,如果用户没有指定具体歌名则为'random', 明确指定的时返回音乐的名字 示例: ```用户:播放两只老虎\n参数:两只老虎``` ```用户:播放音乐 \n参数:random ```"
|
"description": "歌曲名称,如果用户没有指定具体歌名则为'random', 明确指定的时返回音乐的名字 示例: ```用户:播放两只老虎\n参数:两只老虎``` ```用户:播放音乐 \n参数:random ```",
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["song_name"]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"required": ["song_name"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@register_function('play_music', play_music_function_desc, ToolType.SYSTEM_CTL)
|
@register_function("play_music", play_music_function_desc, ToolType.SYSTEM_CTL)
|
||||||
def play_music(conn, song_name: str):
|
def play_music(conn, song_name: str):
|
||||||
try:
|
try:
|
||||||
music_intent = f"播放音乐 {song_name}" if song_name != "random" else "随机播放音乐"
|
music_intent = (
|
||||||
|
f"播放音乐 {song_name}" if song_name != "random" else "随机播放音乐"
|
||||||
|
)
|
||||||
|
|
||||||
# 检查事件循环状态
|
# 检查事件循环状态
|
||||||
if not conn.loop.is_running():
|
if not conn.loop.is_running():
|
||||||
logger.bind(tag=TAG).error("事件循环未运行,无法提交任务")
|
logger.bind(tag=TAG).error("事件循环未运行,无法提交任务")
|
||||||
return ActionResponse(action=Action.RESPONSE, result="系统繁忙", response="请稍后再试")
|
return ActionResponse(
|
||||||
|
action=Action.RESPONSE, result="系统繁忙", response="请稍后再试"
|
||||||
|
)
|
||||||
|
|
||||||
# 提交异步任务
|
# 提交异步任务
|
||||||
future = asyncio.run_coroutine_threadsafe(
|
future = asyncio.run_coroutine_threadsafe(
|
||||||
handle_music_command(conn, music_intent),
|
handle_music_command(conn, music_intent), conn.loop
|
||||||
conn.loop
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# 非阻塞回调处理
|
# 非阻塞回调处理
|
||||||
@@ -62,10 +65,14 @@ def play_music(conn, song_name: str):
|
|||||||
|
|
||||||
future.add_done_callback(handle_done)
|
future.add_done_callback(handle_done)
|
||||||
|
|
||||||
return ActionResponse(action=Action.RESPONSE, result="指令已接收", response="正在为您播放音乐")
|
return ActionResponse(
|
||||||
|
action=Action.NONE, result="指令已接收", response="正在为您播放音乐"
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.bind(tag=TAG).error(f"处理音乐意图错误: {e}")
|
logger.bind(tag=TAG).error(f"处理音乐意图错误: {e}")
|
||||||
return ActionResponse(action=Action.RESPONSE, result=str(e), response="播放音乐时出错了")
|
return ActionResponse(
|
||||||
|
action=Action.RESPONSE, result=str(e), response="播放音乐时出错了"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _extract_song_name(text):
|
def _extract_song_name(text):
|
||||||
@@ -105,7 +112,9 @@ def get_music_files(music_dir, music_ext):
|
|||||||
if ext in music_ext:
|
if ext in music_ext:
|
||||||
# 添加相对路径
|
# 添加相对路径
|
||||||
music_files.append(str(file.relative_to(music_dir)))
|
music_files.append(str(file.relative_to(music_dir)))
|
||||||
music_file_names.append(os.path.splitext(str(file.relative_to(music_dir)))[0])
|
music_file_names.append(
|
||||||
|
os.path.splitext(str(file.relative_to(music_dir)))[0]
|
||||||
|
)
|
||||||
return music_files, music_file_names
|
return music_files, music_file_names
|
||||||
|
|
||||||
|
|
||||||
@@ -117,15 +126,20 @@ def initialize_music_handler(conn):
|
|||||||
MUSIC_CACHE["music_dir"] = os.path.abspath(
|
MUSIC_CACHE["music_dir"] = os.path.abspath(
|
||||||
MUSIC_CACHE["music_config"].get("music_dir", "./music") # 默认路径修改
|
MUSIC_CACHE["music_config"].get("music_dir", "./music") # 默认路径修改
|
||||||
)
|
)
|
||||||
MUSIC_CACHE["music_ext"] = MUSIC_CACHE["music_config"].get("music_ext", (".mp3", ".wav", ".p3"))
|
MUSIC_CACHE["music_ext"] = MUSIC_CACHE["music_config"].get(
|
||||||
MUSIC_CACHE["refresh_time"] = MUSIC_CACHE["music_config"].get("refresh_time", 60)
|
"music_ext", (".mp3", ".wav", ".p3")
|
||||||
|
)
|
||||||
|
MUSIC_CACHE["refresh_time"] = MUSIC_CACHE["music_config"].get(
|
||||||
|
"refresh_time", 60
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
MUSIC_CACHE["music_dir"] = os.path.abspath("./music")
|
MUSIC_CACHE["music_dir"] = os.path.abspath("./music")
|
||||||
MUSIC_CACHE["music_ext"] = (".mp3", ".wav", ".p3")
|
MUSIC_CACHE["music_ext"] = (".mp3", ".wav", ".p3")
|
||||||
MUSIC_CACHE["refresh_time"] = 60
|
MUSIC_CACHE["refresh_time"] = 60
|
||||||
# 获取音乐文件列表
|
# 获取音乐文件列表
|
||||||
MUSIC_CACHE["music_files"], MUSIC_CACHE["music_file_names"] = get_music_files(MUSIC_CACHE["music_dir"],
|
MUSIC_CACHE["music_files"], MUSIC_CACHE["music_file_names"] = get_music_files(
|
||||||
MUSIC_CACHE["music_ext"])
|
MUSIC_CACHE["music_dir"], MUSIC_CACHE["music_ext"]
|
||||||
|
)
|
||||||
MUSIC_CACHE["scan_time"] = time.time()
|
MUSIC_CACHE["scan_time"] = time.time()
|
||||||
return MUSIC_CACHE
|
return MUSIC_CACHE
|
||||||
|
|
||||||
@@ -135,15 +149,16 @@ async def handle_music_command(conn, text):
|
|||||||
global MUSIC_CACHE
|
global MUSIC_CACHE
|
||||||
|
|
||||||
"""处理音乐播放指令"""
|
"""处理音乐播放指令"""
|
||||||
clean_text = re.sub(r'[^\w\s]', '', text).strip()
|
clean_text = re.sub(r"[^\w\s]", "", text).strip()
|
||||||
logger.bind(tag=TAG).debug(f"检查是否是音乐命令: {clean_text}")
|
logger.bind(tag=TAG).debug(f"检查是否是音乐命令: {clean_text}")
|
||||||
|
|
||||||
# 尝试匹配具体歌名
|
# 尝试匹配具体歌名
|
||||||
if os.path.exists(MUSIC_CACHE["music_dir"]):
|
if os.path.exists(MUSIC_CACHE["music_dir"]):
|
||||||
if time.time() - MUSIC_CACHE["scan_time"] > MUSIC_CACHE["refresh_time"]:
|
if time.time() - MUSIC_CACHE["scan_time"] > MUSIC_CACHE["refresh_time"]:
|
||||||
# 刷新音乐文件列表
|
# 刷新音乐文件列表
|
||||||
MUSIC_CACHE["music_files"], MUSIC_CACHE["music_file_names"] = get_music_files(MUSIC_CACHE["music_dir"],
|
MUSIC_CACHE["music_files"], MUSIC_CACHE["music_file_names"] = (
|
||||||
MUSIC_CACHE["music_ext"])
|
get_music_files(MUSIC_CACHE["music_dir"], MUSIC_CACHE["music_ext"])
|
||||||
|
)
|
||||||
MUSIC_CACHE["scan_time"] = time.time()
|
MUSIC_CACHE["scan_time"] = time.time()
|
||||||
|
|
||||||
potential_song = _extract_song_name(clean_text)
|
potential_song = _extract_song_name(clean_text)
|
||||||
@@ -158,6 +173,23 @@ async def handle_music_command(conn, text):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _get_random_play_prompt(song_name):
|
||||||
|
"""生成随机播放引导语"""
|
||||||
|
# 移除文件扩展名
|
||||||
|
clean_name = os.path.splitext(song_name)[0]
|
||||||
|
prompts = [
|
||||||
|
f"正在为您播放,{clean_name}",
|
||||||
|
f"请欣赏歌曲,{clean_name}",
|
||||||
|
f"即将为您播放,{clean_name}",
|
||||||
|
f"为您带来,{clean_name}",
|
||||||
|
f"让我们聆听,{clean_name}",
|
||||||
|
f"接下来请欣赏,{clean_name}",
|
||||||
|
f"为您献上,{clean_name}",
|
||||||
|
]
|
||||||
|
# 直接使用random.choice,不设置seed
|
||||||
|
return random.choice(prompts)
|
||||||
|
|
||||||
|
|
||||||
async def play_local_music(conn, specific_file=None):
|
async def play_local_music(conn, specific_file=None):
|
||||||
global MUSIC_CACHE
|
global MUSIC_CACHE
|
||||||
"""播放本地音乐文件"""
|
"""播放本地音乐文件"""
|
||||||
@@ -180,16 +212,25 @@ async def play_local_music(conn, specific_file=None):
|
|||||||
if not os.path.exists(music_path):
|
if not os.path.exists(music_path):
|
||||||
logger.bind(tag=TAG).error(f"选定的音乐文件不存在: {music_path}")
|
logger.bind(tag=TAG).error(f"选定的音乐文件不存在: {music_path}")
|
||||||
return
|
return
|
||||||
text = f"正在播放{selected_music}"
|
text = _get_random_play_prompt(selected_music)
|
||||||
await send_stt_message(conn, text)
|
await send_stt_message(conn, text)
|
||||||
conn.tts_first_text_index = 0
|
conn.tts_first_text_index = 0
|
||||||
conn.tts_last_text_index = 0
|
conn.tts_last_text_index = 0
|
||||||
|
|
||||||
|
tts_file = await asyncio.to_thread(conn.tts.to_tts, text)
|
||||||
|
if tts_file is not None and os.path.exists(tts_file):
|
||||||
|
conn.tts_last_text_index = 1
|
||||||
|
opus_packets, _ = conn.tts.audio_to_opus_data(tts_file)
|
||||||
|
conn.audio_play_queue.put((opus_packets, None, 0))
|
||||||
|
os.remove(tts_file)
|
||||||
|
|
||||||
conn.llm_finish_task = True
|
conn.llm_finish_task = True
|
||||||
|
|
||||||
if music_path.endswith(".p3"):
|
if music_path.endswith(".p3"):
|
||||||
opus_packets, duration = p3.decode_opus_from_file(music_path)
|
opus_packets, _ = p3.decode_opus_from_file(music_path)
|
||||||
else:
|
else:
|
||||||
opus_packets, duration = conn.tts.audio_to_opus_data(music_path)
|
opus_packets, _ = conn.tts.audio_to_opus_data(music_path)
|
||||||
conn.audio_play_queue.put((opus_packets, selected_music, 0))
|
conn.audio_play_queue.put((opus_packets, None, conn.tts_last_text_index))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.bind(tag=TAG).error(f"播放音乐失败: {str(e)}")
|
logger.bind(tag=TAG).error(f"播放音乐失败: {str(e)}")
|
||||||
|
|||||||
@@ -992,7 +992,9 @@ function connectToServer() {
|
|||||||
|
|
||||||
if(connectButton.id === "connectButton") {
|
if(connectButton.id === "connectButton") {
|
||||||
connectButton.textContent = '断开';
|
connectButton.textContent = '断开';
|
||||||
connectButton.onclick = disconnectFromServer;
|
// connectButton.onclick = disconnectFromServer;
|
||||||
|
connectButton.removeEventListener("click", connectToServer);
|
||||||
|
connectButton.addEventListener("click", disconnectFromServer);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(messageInput.id === "messageInput") {
|
if(messageInput.id === "messageInput") {
|
||||||
@@ -1011,7 +1013,9 @@ function connectToServer() {
|
|||||||
|
|
||||||
if(connectButton.id === "connectButton") {
|
if(connectButton.id === "connectButton") {
|
||||||
connectButton.textContent = '连接';
|
connectButton.textContent = '连接';
|
||||||
connectButton.onclick = connectToServer;
|
// connectButton.onclick = connectToServer;
|
||||||
|
connectButton.removeEventListener("click", disconnectFromServer);
|
||||||
|
connectButton.addEventListener("click", connectToServer);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(messageInput.id === "messageInput") {
|
if(messageInput.id === "messageInput") {
|
||||||
@@ -1175,7 +1179,7 @@ function updateStatus(message, type = 'info') {
|
|||||||
// 处理文本消息
|
// 处理文本消息
|
||||||
function handleTextMessage(message) {
|
function handleTextMessage(message) {
|
||||||
if (message.type === 'hello') {
|
if (message.type === 'hello') {
|
||||||
console.log(`服务器回应:${message.message}`);
|
console.log(`服务器回应:${JSON.stringify(message, null, 2)}`);
|
||||||
} else if (message.type === 'tts') {
|
} else if (message.type === 'tts') {
|
||||||
// TTS状态消息
|
// TTS状态消息
|
||||||
if (message.state === 'start') {
|
if (message.state === 'start') {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user