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 | |
|---|---|---|---|
|
|
d721270bed | ||
|
|
fe2e178b19 | ||
|
|
21337e5046 | ||
|
|
1c8b271e20 | ||
|
|
3d1160b6bb | ||
|
|
086c4a55e6 | ||
|
|
889f2f9a75 | ||
|
|
07b686be14 | ||
|
|
4d6b453485 | ||
|
|
9909045f9d | ||
|
|
b0dc4d88db | ||
|
|
bbf3858735 | ||
|
|
73573e2f12 | ||
|
|
99e136786e | ||
|
|
205be0e4ec | ||
|
|
2e64f45a31 | ||
|
|
818b4f2f6d | ||
|
|
5d72f9f79d | ||
|
|
0ee349df2d | ||
|
|
4dde66339b | ||
|
|
0064f975c2 | ||
|
|
43234f4798 | ||
|
|
48034fc000 | ||
|
|
3fc33e9095 | ||
|
|
da71dce860 | ||
|
|
b7e4408a0f | ||
|
|
4f4f8ca54e | ||
|
|
99c12a0fed | ||
|
|
dcd0ef3f40 | ||
|
|
2f53b921fe | ||
|
|
3051dc8327 | ||
|
|
c313997a61 | ||
|
|
07c0c764d3 | ||
|
|
e339a501aa | ||
|
|
f2c541b4ab | ||
|
|
206af1c074 | ||
|
|
344729c9a6 | ||
|
|
3b1c2d3b1b | ||
|
|
3660af9619 | ||
|
|
4905ba836c | ||
|
|
ce00c862fe | ||
|
|
a50229431b | ||
|
|
275102f5b7 | ||
|
|
ff6286ffcc | ||
|
|
974fe6c3b7 | ||
|
|
b78292b270 | ||
|
|
3559a7018c | ||
|
|
3222d6175d | ||
|
|
28f9f23449 | ||
|
|
5e7d192ebc | ||
|
|
d6a32416d7 | ||
|
|
6a2c01763e | ||
|
|
e1dd272708 | ||
|
|
3e7bc0606a | ||
|
|
9d9ca1475d | ||
|
|
43ddc0135c | ||
|
|
98f907a97b | ||
|
|
b2ddf060c9 | ||
|
|
5e612276db | ||
|
|
cbb293e7fa | ||
|
|
fdb050f6ed | ||
|
|
f83ddf79fa | ||
|
|
95b340a7a3 | ||
|
|
d46ac89e58 | ||
|
|
80b0912f84 | ||
|
|
e0b4431064 | ||
|
|
e3f16f91cf | ||
|
|
5f39e5c308 | ||
|
|
83304c9ff4 | ||
|
|
863cfb575c | ||
|
|
2944040648 | ||
|
|
5f19b9b0d0 | ||
|
|
2948ed091c | ||
|
|
97ee397b54 | ||
|
|
f7fbe7efbe | ||
|
|
ce6b0bd099 | ||
|
|
8643960104 | ||
|
|
b4549ebc59 | ||
|
|
a602cddd05 | ||
|
|
c4278864d1 | ||
|
|
7094548ecf | ||
|
|
363fffada8 | ||
|
|
c2c3eb7106 | ||
|
|
8e19a64f15 | ||
|
|
9042b7e6d7 | ||
|
|
ad1855cfb5 | ||
|
|
efa0d1095d | ||
|
|
a8c13d740b | ||
|
|
7475e9829f | ||
|
|
b7dde1dae2 | ||
|
|
778c4560fd | ||
|
|
fa31096cd8 | ||
|
|
fd1dd39463 | ||
|
|
ecd3e12a41 | ||
|
|
0a8121e849 | ||
|
|
e93f9d7111 | ||
|
|
fdf98985a1 | ||
|
|
6c3609e79e | ||
|
|
a9aa709502 | ||
|
|
1f6a89ada5 | ||
|
|
31a1180b79 | ||
|
|
77b4a416a9 | ||
|
|
da7a6933f5 | ||
|
|
54cac0d2fd | ||
|
|
7fe718c887 | ||
|
|
36eaefb8b0 | ||
|
|
3a61ec84b3 | ||
|
|
c1bf92d243 | ||
|
|
c1959e0795 | ||
|
|
40a6b7434b | ||
|
|
24519a2732 | ||
|
|
c190abe4b1 | ||
|
|
07d5b3cd2a | ||
|
|
81e923b4f7 | ||
|
|
bcb51729a5 | ||
|
|
a731c1d01f | ||
|
|
0bb5a37ace | ||
|
|
a711827996 | ||
|
|
8f854e6f74 | ||
|
|
f1b5afbf5c | ||
|
|
84f6930389 | ||
|
|
f415dd4482 | ||
|
|
927f65e106 | ||
|
|
4b6b3082c7 | ||
|
|
4c550245db | ||
|
|
341ed22370 | ||
|
|
6851ccacf3 | ||
|
|
815af687ea | ||
|
|
e9373b59c8 | ||
|
|
3f3f3fcd57 | ||
|
|
d003e66310 | ||
|
|
e6108ccbbd | ||
|
|
a01f01198c | ||
|
|
cc96eaeb8f | ||
|
|
e25ca3d1d3 | ||
|
|
9c111b2d5e | ||
|
|
436a01fb6e | ||
|
|
9b6620dd4a | ||
|
|
85919e800e | ||
|
|
a2023a65c0 | ||
|
|
619370f36c | ||
|
|
ea6c0e691f | ||
|
|
de0d82f1e8 | ||
|
|
c8a26d9c5e | ||
|
|
d89b842586 | ||
|
|
6dd3ceea7e | ||
|
|
5fe68030b0 | ||
|
|
5215105440 | ||
|
|
fb69eb751a | ||
|
|
b903d51592 | ||
|
|
d1f6774e58 | ||
|
|
9e55df4141 | ||
|
|
b169e9413f | ||
|
|
c511766e9d | ||
|
|
e6f9ceb7fa | ||
|
|
eab8b48c9c | ||
|
|
46abb1009a | ||
|
|
2a42d86db0 | ||
|
|
68b0d9654c | ||
|
|
8f96cf13ac | ||
|
|
dbd179e507 | ||
|
|
9541e85600 | ||
|
|
9ecd829bfa | ||
|
|
fd4cff73ae | ||
|
|
1d6c4751de | ||
|
|
e066c1d6a1 | ||
|
|
85f5404b3b | ||
|
|
33c5893d32 | ||
|
|
b597cb0686 | ||
|
|
c5e288ca9c | ||
|
|
38d984f225 | ||
|
|
628c642040 | ||
|
|
d217cc4dd0 | ||
|
|
4f0e54b390 | ||
|
|
d7f89be31c | ||
|
|
53bcee7032 | ||
|
|
96991ae5ef | ||
|
|
068bcde451 | ||
|
|
0dda4f5646 | ||
|
|
6c57ce9dd2 | ||
|
|
6ac67a7e41 | ||
|
|
a23a585ad8 | ||
|
|
eba9562e81 | ||
|
|
ce49b409ac | ||
|
|
7222f68d4d | ||
|
|
f3f0d62f12 | ||
|
|
6e7c86e159 | ||
|
|
f5565f6700 | ||
|
|
e8d0bb0c54 | ||
|
|
33d70ccc96 | ||
|
|
53313a26af | ||
|
|
7ba180752a | ||
|
|
19736e66ad | ||
|
|
a00f8e4b76 | ||
|
|
7d9895cf5b | ||
|
|
d5f804bbb3 | ||
|
|
33a385cfa8 | ||
|
|
109d924591 | ||
|
|
a825eb3d4c | ||
|
|
3fb40677a4 | ||
|
|
833971cd28 | ||
|
|
d14b14bce9 | ||
|
|
53e26821ad | ||
|
|
1a7c06eb81 | ||
|
|
6cdbe45313 | ||
|
|
42a5b4892d | ||
|
|
9f4508b0c7 | ||
|
|
b2123ff01a | ||
|
|
43ead841a4 | ||
|
|
33b4794e83 | ||
|
|
fb91e6b1dd | ||
|
|
4a4dbf123e | ||
|
|
0decedd6a1 | ||
|
|
d2e3a63418 | ||
|
|
15b3f0a8f5 | ||
|
|
5c261528d0 | ||
|
|
8b2bbec0b9 | ||
|
|
8b3a4ad163 | ||
|
|
b3e272281b | ||
|
|
06a90d6266 | ||
|
|
6eb7acaf73 | ||
|
|
2e092a7880 | ||
|
|
515f669546 | ||
|
|
7699596597 | ||
|
|
34dd325a1d | ||
|
|
d33cdd978d | ||
|
|
0da9b4a69c | ||
|
|
7fc2eeaaa5 | ||
|
|
508529f66e | ||
|
|
dc170edbc1 | ||
|
|
85e65cf9e8 | ||
|
|
41887ef431 | ||
|
|
5dbf796fa9 | ||
|
|
20f601e607 | ||
|
|
c8d2d2255d | ||
|
|
5eaaf9f01d | ||
|
|
d6697948c2 | ||
|
|
f88abd7638 | ||
|
|
fbfc408e94 | ||
|
|
5ac1a1d6a5 | ||
|
|
3a74b30a0e | ||
|
|
fe0a6852e9 | ||
|
|
bfa04743e2 | ||
|
|
48094f7e37 | ||
|
|
60521b0a7e | ||
|
|
401deff2c3 | ||
|
|
6ffa325b73 | ||
|
|
eeedfc8ac9 | ||
|
|
aba7172a03 | ||
|
|
192f6f198f | ||
|
|
43c9d85b8f | ||
|
|
6d5935fb27 | ||
|
|
efbef4ff5b | ||
|
|
e4c99b9351 | ||
|
|
11e328ea6a | ||
|
|
8879907e38 | ||
|
|
885d72c4f7 | ||
|
|
4538542c25 | ||
|
|
3d509d75dc | ||
|
|
0ef73d7c3e | ||
|
|
6da90c6a97 | ||
|
|
bacdb794da | ||
|
|
2ece3f399b | ||
|
|
68b539db15 | ||
|
|
7a7bfa26f6 | ||
|
|
5f229351c8 | ||
|
|
82125c4933 | ||
|
|
33f75d26c1 | ||
|
|
3c4d702bc1 | ||
|
|
db8d100edb | ||
|
|
6f7e8978ca | ||
|
|
856bf2c574 | ||
|
|
b10cfa6b24 | ||
|
|
d3d329bd43 | ||
|
|
f358011900 | ||
|
|
cd6c3e4f79 | ||
|
|
ee7342de9d | ||
|
|
6120d49a35 | ||
|
|
381f8ea578 | ||
|
|
a3e2073dc6 | ||
|
|
f7bd858fea | ||
|
|
80d10cf4b5 | ||
|
|
ba1fb16f2d | ||
|
|
5f574667d3 | ||
|
|
c470bb1db1 | ||
|
|
26ed8ca33f | ||
|
|
313f5b1982 | ||
|
|
f3f0c24e16 | ||
|
|
fe2539ac97 | ||
|
|
dd1a430c21 | ||
|
|
621634697e | ||
|
|
fb5c35e6c9 | ||
|
|
228596dbe3 | ||
|
|
74baa85340 | ||
|
|
8f6e9ca54d | ||
|
|
19420d0bb3 | ||
|
|
770c8c8a9f | ||
|
|
252a34090a | ||
|
|
bde5ef4594 | ||
|
|
6eddf081de | ||
|
|
4645c50f6d | ||
|
|
3f6e66b1ce | ||
|
|
106889b0dd | ||
|
|
785b43464b | ||
|
|
03da60dd12 | ||
|
|
ec372f65ec | ||
|
|
cfba4c45b6 | ||
|
|
657d03f49d | ||
|
|
b4dd78b2ed | ||
|
|
1ebe7b8f71 | ||
|
|
30e7a71497 | ||
|
|
ff1de6c7e3 | ||
|
|
9e39883276 | ||
|
|
0f249ae1db | ||
|
|
31ada101b0 | ||
|
|
1b52a7168b | ||
|
|
83a3d0eabe | ||
|
|
080c6b4dfb | ||
|
|
ea50288c5b | ||
|
|
5d439a4168 | ||
|
|
3c46e16494 | ||
|
|
39cd0fe1ac | ||
|
|
16c0be74c9 | ||
|
|
732828612e | ||
|
|
7344b81c98 | ||
|
|
1e5b03edb5 | ||
|
|
b15b5af349 | ||
|
|
5242430b6d | ||
|
|
c39968a5bf | ||
|
|
8b4f9ab20e | ||
|
|
527e6050ce | ||
|
|
1d162cb874 | ||
|
|
c197f5c942 | ||
|
|
3a4c0bb888 | ||
|
|
38d50f4275 | ||
|
|
25a3b648fc | ||
|
|
d3638164fe | ||
|
|
1c2a61fd34 | ||
|
|
f81c4aa970 | ||
|
|
3caa4ec6e8 | ||
|
|
5e48a2274a | ||
|
|
7f7c980522 | ||
|
|
bbab4ebc86 | ||
|
|
8bae0c8ac5 | ||
|
|
a5e4aa1300 | ||
|
|
48313bc302 | ||
|
|
f0261a8236 | ||
|
|
84c194dd34 | ||
|
|
920e24b4ff | ||
|
|
43d82343a3 | ||
|
|
320d246a83 | ||
|
|
faea50f99d | ||
|
|
ef59798d70 | ||
|
|
6781eca56a | ||
|
|
c1f0853271 | ||
|
|
d4cda5c104 | ||
|
|
f9bd5bb995 | ||
|
|
f1c2b5cb3f | ||
|
|
579af29a73 | ||
|
|
0b0fd142a3 | ||
|
|
9e20a53ca0 | ||
|
|
8768240efe | ||
|
|
b429760bd5 | ||
|
|
218cd35c3d | ||
|
|
146a039c19 | ||
|
|
9a3e1fa9c8 | ||
|
|
fa194af9bb | ||
|
|
46b3757c13 | ||
|
|
5050ea7ac1 | ||
|
|
d121b4d0e1 | ||
|
|
d45bf5f60a | ||
|
|
94a43432fc |
@@ -37,7 +37,7 @@ jobs:
|
||||
file: Dockerfile-server-base
|
||||
push: true
|
||||
tags: ghcr.io/${{ github.repository }}:server-base
|
||||
platforms: linux/amd64
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache-from: type=gha,scope=server-base
|
||||
cache-to: type=gha,mode=max,scope=server-base
|
||||
build-args: |
|
||||
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.IS_VERSION == 'true' && format('ghcr.io/{0}:server_{1},ghcr.io/{0}:server_latest', github.repository, env.VERSION) || format('ghcr.io/{0}:server_latest', github.repository) }}
|
||||
platforms: linux/amd64
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: |
|
||||
@@ -81,7 +81,7 @@ jobs:
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.IS_VERSION == 'true' && format('ghcr.io/{0}:web_{1},ghcr.io/{0}:web_latest', github.repository, env.VERSION) || format('ghcr.io/{0}:web_latest', github.repository) }}
|
||||
platforms: linux/amd64
|
||||
platforms: linux/amd64,linux/arm64
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: |
|
||||
|
||||
+7
-7
@@ -1,5 +1,5 @@
|
||||
# 第一阶段:构建Vue前端
|
||||
FROM node:18 as web-builder
|
||||
FROM node:18 AS web-builder
|
||||
WORKDIR /app
|
||||
COPY main/manager-web/package*.json ./
|
||||
RUN npm install
|
||||
@@ -7,7 +7,7 @@ COPY main/manager-web .
|
||||
RUN npm run build
|
||||
|
||||
# 第二阶段:构建Java后端
|
||||
FROM maven:3.9.4-eclipse-temurin-21 as api-builder
|
||||
FROM maven:3.9.4-eclipse-temurin-21 AS api-builder
|
||||
WORKDIR /app
|
||||
COPY main/manager-api/pom.xml .
|
||||
COPY main/manager-api/src ./src
|
||||
@@ -18,18 +18,18 @@ FROM bellsoft/liberica-runtime-container:jre-21-glibc
|
||||
|
||||
# 安装Nginx和字体库
|
||||
RUN apk update && \
|
||||
apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing/ \
|
||||
apk add --no-cache --no-scripts \
|
||||
nginx \
|
||||
bash \
|
||||
fontconfig \
|
||||
ttf-dejavu \
|
||||
msttcorefonts-installer \
|
||||
&& ACCEPT_EULA=Y apk add --no-cache msttcorefonts-installer \
|
||||
&& fc-cache -f -v \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
|
||||
# 复制项目自带的中文字体
|
||||
COPY main/manager-web/public/generator/static/fonts/*.ttf /usr/share/fonts/
|
||||
|
||||
# 更新字体缓存
|
||||
RUN (printf 'YES\n' | update-ms-fonts || true) && fc-cache -f -v
|
||||
RUN fc-cache -f -v
|
||||
|
||||
# 配置Nginx
|
||||
COPY docs/docker/nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
@@ -183,8 +183,8 @@ Spearheaded by Professor Siyuan Liu's Team (South China University of Technology
|
||||
#### 🚀 部署方式选择
|
||||
| 部署方式 | 特点 | 适用场景 | 部署文档 | 配置要求 | 视频教程 |
|
||||
|---------|------|---------|---------|---------|---------|
|
||||
| **最简化安装** | 智能对话、IOT、MCP、视觉感知 | 低配置环境,数据存储在配置文件,无需数据库 | [①Docker版](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②源码部署](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| 如果使用`FunASR`要2核4G,如果全API,要2核2G | - |
|
||||
| **全模块安装** | 智能对话、IOT、MCP接入点、声纹识别、视觉感知、OTA、智控台 | 完整功能体验,数据存储在数据库 |[①Docker版](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [②源码部署](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [③源码部署自动更新教程](./docs/dev-ops-integration.md) | 如果使用`FunASR`要4核8G,如果全API,要2核4G| [本地源码启动视频教程](https://www.bilibili.com/video/BV1wBJhz4Ewe) |
|
||||
| **最简化安装** | 智能对话、单智能体管理 | 低配置环境,数据存储在配置文件,无需数据库 | [①Docker版](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②源码部署](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| 如果使用`FunASR`要2核4G,如果全API,要2核2G | - |
|
||||
| **全模块安装** | 智能对话、多用户管理、多智能体管理、智控台界面操作 | 完整功能体验,数据存储在数据库 |[①Docker版](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [②源码部署](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [③源码部署自动更新教程](./docs/dev-ops-integration.md) | 如果使用`FunASR`要4核8G,如果全API,要2核4G| [本地源码启动视频教程](https://www.bilibili.com/video/BV1wBJhz4Ewe) |
|
||||
|
||||
常见问题及相关教程,可参考[这个链接](./docs/FAQ.md)
|
||||
|
||||
@@ -211,10 +211,10 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
||||
|
||||
| 模块名称 | 入门全免费设置 | 流式配置 |
|
||||
|:---:|:---:|:---:|
|
||||
| ASR(语音识别) | FunASR(本地) | 👍FunASR(本地GPU模式) |
|
||||
| LLM(大模型) | ChatGLMLLM(智谱glm-4-flash) | 👍AliLLM(qwen3-235b-a22b-instruct-2507) 或 👍DoubaoLLM(doubao-1-5-pro-32k-250115) |
|
||||
| VLLM(视觉大模型) | ChatGLMVLLM(智谱glm-4v-flash) | 👍QwenVLVLLM(千问qwen2.5-vl-3b-instructh) |
|
||||
| TTS(语音合成) | ✅LinkeraiTTS(灵犀流式) | 👍HuoshanDoubleStreamTTS(火山双流式语音合成) 或 👍AliyunStreamTTS(阿里云流式语音合成) |
|
||||
| ASR(语音识别) | FunASR(本地) | 👍XunfeiStreamASR(讯飞流式) |
|
||||
| LLM(大模型) | glm-4-flash(智谱) | 👍qwen-flash(阿里百炼) |
|
||||
| VLLM(视觉大模型) | glm-4v-flash(智谱) | 👍qwen2.5-vl-3b-instructh(阿里百炼) |
|
||||
| TTS(语音合成) | ✅LinkeraiTTS(灵犀流式) | 👍HuoshanDoubleStreamTTS(火山流式) |
|
||||
| Intent(意图识别) | function_call(函数调用) | function_call(函数调用) |
|
||||
| Memory(记忆功能) | mem_local_short(本地短期记忆) | mem_local_short(本地短期记忆) |
|
||||
|
||||
@@ -260,7 +260,7 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
||||
---
|
||||
|
||||
## 产品生态 👬
|
||||
小智是一个生态,当你使用这个产品时,也可以看看其他在这个生态圈的[优秀项目](https://github.com/78/xiaozhi-esp32?tab=readme-ov-file#%E7%9B%B8%E5%85%B3%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE)
|
||||
小智是一个生态,当你使用这个产品时,也可以看看其他在这个生态圈的[优秀项目](https://github.com/78/xiaozhi-esp32/blob/main/README_zh.md#%E7%9B%B8%E5%85%B3%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE)
|
||||
|
||||
---
|
||||
|
||||
|
||||
+7
-7
@@ -181,8 +181,8 @@ Dieses Projekt bietet zwei Bereitstellungsmethoden. Bitte wählen Sie basierend
|
||||
#### 🚀 Auswahl der Bereitstellungsmethode
|
||||
| Bereitstellungsmethode | Funktionen | Anwendungsszenarien | Deployment-Dokumente | Konfigurationsanforderungen | Video-Tutorials |
|
||||
|---------|------|---------|---------|---------|---------|
|
||||
| **Vereinfachte Installation** | Intelligenter Dialog, IOT, MCP, visuelle Wahrnehmung | Umgebungen mit geringer Konfiguration, Daten in Konfigurationsdateien gespeichert, keine Datenbank erforderlich | [①Docker-Version](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②Quellcode-Deployment](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| 2 Kerne 4GB bei Verwendung von `FunASR`, 2 Kerne 2GB bei allen APIs | - |
|
||||
| **Vollständige Modulinstallation** | Intelligenter Dialog, IOT, MCP-Endpunkte, Stimmabdruckerkennung, visuelle Wahrnehmung, OTA, intelligente Steuerkonsole | Vollständige Funktionserfahrung, Daten in Datenbank gespeichert |[①Docker-Version](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [②Quellcode-Deployment](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [③Quellcode-Deployment Auto-Update-Tutorial](./docs/dev-ops-integration.md) | 4 Kerne 8GB bei Verwendung von `FunASR`, 2 Kerne 4GB bei allen APIs| [Video-Tutorial für lokalen Quellcode-Start](https://www.bilibili.com/video/BV1wBJhz4Ewe) |
|
||||
| **Vereinfachte Installation** | Intelligenter Dialog, Einzel-Agenten-Verwaltung | Umgebungen mit geringer Konfiguration, Daten in Konfigurationsdateien gespeichert, keine Datenbank erforderlich | [①Docker-Version](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②Quellcode-Deployment](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| 2 Kerne 4GB bei Verwendung von `FunASR`, 2 Kerne 2GB bei allen APIs | - |
|
||||
| **Vollständige Modulinstallation** | Intelligenter Dialog, Mehrbenutzerverwaltung, Mehr-Agenten-Verwaltung, Intelligente Steuerkonsole-Bedienung | Vollständige Funktionserfahrung, Daten in Datenbank gespeichert |[①Docker-Version](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [②Quellcode-Deployment](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [③Quellcode-Deployment Auto-Update-Tutorial](./docs/dev-ops-integration.md) | 4 Kerne 8GB bei Verwendung von `FunASR`, 2 Kerne 4GB bei allen APIs| [Video-Tutorial für lokalen Quellcode-Start](https://www.bilibili.com/video/BV1wBJhz4Ewe) |
|
||||
|
||||
Häufige Fragen und entsprechende Tutorials finden Sie unter [diesem Link](./docs/FAQ.md)
|
||||
|
||||
@@ -209,10 +209,10 @@ Websocket-Schnittstellenadresse: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
||||
|
||||
| Modulname | Einstiegslevel Kostenlose Einstellungen | Streaming-Konfiguration |
|
||||
|:---:|:---:|:---:|
|
||||
| ASR (Spracherkennung) | FunASR (Lokal) | 👍FunASR (Lokaler GPU-Modus) |
|
||||
| LLM (Großes Modell) | ChatGLMLLM (Zhipu glm-4-flash) | 👍AliLLM (qwen3-235b-a22b-instruct-2507) oder 👍DoubaoLLM (doubao-1-5-pro-32k-250115) |
|
||||
| VLLM (Vision Large Model) | ChatGLMVLLM (Zhipu glm-4v-flash) | 👍QwenVLVLLM (Qwen qwen2.5-vl-3b-instructh) |
|
||||
| TTS (Sprachsynthese) | ✅LinkeraiTTS (Lingxi-Streaming) | 👍HuoshanDoubleStreamTTS (Volcano Dual-Stream-Sprachsynthese) oder 👍AliyunStreamTTS (Alibaba Cloud Streaming-Sprachsynthese) |
|
||||
| ASR (Spracherkennung) | FunASR (Lokal) | 👍XunfeiStreamASR (Xunfei-Streaming) |
|
||||
| LLM (Großes Modell) | glm-4-flash (Zhipu) | 👍qwen-flash (Alibaba Bailian) |
|
||||
| VLLM (Vision Large Model) | glm-4v-flash (Zhipu) | 👍qwen2.5-vl-3b-instructh (Alibaba Bailian) |
|
||||
| TTS (Sprachsynthese) | ✅LinkeraiTTS (Lingxi-Streaming) | 👍HuoshanDoubleStreamTTS (Volcano-Streaming) |
|
||||
| Intent (Absichtserkennung) | function_call (Funktionsaufruf) | function_call (Funktionsaufruf) |
|
||||
| Memory (Gedächtnisfunktion) | mem_local_short (Lokales Kurzzeitgedächtnis) | mem_local_short (Lokales Kurzzeitgedächtnis) |
|
||||
|
||||
@@ -258,7 +258,7 @@ Wenn Sie ein Softwareentwickler sind, finden Sie hier einen [Offenen Brief an En
|
||||
---
|
||||
|
||||
## Produktökosystem 👬
|
||||
Xiaozhi ist ein Ökosystem. Wenn Sie dieses Produkt verwenden, können Sie sich auch andere [hervorragende Projekte](https://github.com/78/xiaozhi-esp32?tab=readme-ov-file#%E7%9B%B8%E5%85%B3%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE) in diesem Ökosystem ansehen
|
||||
Xiaozhi ist ein Ökosystem. Wenn Sie dieses Produkt verwenden, können Sie sich auch andere [hervorragende Projekte](https://github.com/78/xiaozhi-esp32?tab=readme-ov-file#related-open-source-projects) in diesem Ökosystem ansehen
|
||||
|
||||
---
|
||||
|
||||
|
||||
+7
-7
@@ -181,8 +181,8 @@ This project provides two deployment methods. Please choose based on your specif
|
||||
#### 🚀 Deployment Method Selection
|
||||
| Deployment Method | Features | Applicable Scenarios | Deployment Docs | Configuration Requirements | Video Tutorials |
|
||||
|---------|------|---------|---------|---------|---------|
|
||||
| **Simplified Installation** | Intelligent dialogue, IOT, MCP, visual perception | Low-configuration environments, data stored in config files, no database required | [①Docker Version](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②Source Code Deployment](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| 2 cores 4GB if using `FunASR`, 2 cores 2GB if all APIs | - |
|
||||
| **Full Module Installation** | Intelligent dialogue, IOT, MCP endpoints, voiceprint recognition, visual perception, OTA, intelligent control console | Complete functionality experience, data stored in database |[①Docker Version](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [②Source Code Deployment](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [③Source Code Deployment Auto-Update Tutorial](./docs/dev-ops-integration.md) | 4 cores 8GB if using `FunASR`, 2 cores 4GB if all APIs| [Local Source Code Startup Video Tutorial](https://www.bilibili.com/video/BV1wBJhz4Ewe) |
|
||||
| **Simplified Installation** | Intelligent dialogue, single agent management | Low-configuration environments, data stored in config files, no database required | [①Docker Version](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②Source Code Deployment](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| 2 cores 4GB if using `FunASR`, 2 cores 2GB if all APIs | - |
|
||||
| **Full Module Installation** | Intelligent dialogue, multi-user management, multi-agent management, intelligent console interface operation | Complete functionality experience, data stored in database |[①Docker Version](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [②Source Code Deployment](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [③Source Code Deployment Auto-Update Tutorial](./docs/dev-ops-integration.md) | 4 cores 8GB if using `FunASR`, 2 cores 4GB if all APIs| [Local Source Code Startup Video Tutorial](https://www.bilibili.com/video/BV1wBJhz4Ewe) |
|
||||
|
||||
|
||||
> 💡 Note: Below is a test platform deployed with the latest code. You can burn and test if needed. Concurrent users: 6, data will be cleared daily.
|
||||
@@ -208,10 +208,10 @@ Websocket Interface Address: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
||||
|
||||
| Module Name | Entry Level Free Settings | Streaming Configuration |
|
||||
|:---:|:---:|:---:|
|
||||
| ASR(Speech Recognition) | FunASR(Local) | 👍FunASRServer or 👍DoubaoStreamASR |
|
||||
| LLM(Large Model) | ChatGLMLLM(Zhipu glm-4-flash) | 👍DoubaoLLM(Volcano doubao-1-5-pro-32k-250115) |
|
||||
| VLLM(Vision Large Model) | ChatGLMVLLM(Zhipu glm-4v-flash) | 👍QwenVLVLLM(Qwen qwen2.5-vl-3b-instructh) |
|
||||
| TTS(Speech Synthesis) | ✅LinkeraiTTS(Lingxi streaming) | 👍HuoshanDoubleStreamTTS(Volcano dual-stream speech synthesis) |
|
||||
| ASR(Speech Recognition) | FunASR(Local) | 👍XunfeiStreamASR(Xunfei Streaming) |
|
||||
| LLM(Large Model) | glm-4-flash(Zhipu) | 👍qwen-flash(Alibaba Bailian) |
|
||||
| VLLM(Vision Large Model) | glm-4v-flash(Zhipu) | 👍qwen2.5-vl-3b-instructh(Alibaba Bailian) |
|
||||
| TTS(Speech Synthesis) | ✅LinkeraiTTS(Lingxi streaming) | 👍HuoshanDoubleStreamTTS(Volcano Streaming) |
|
||||
| Intent(Intent Recognition) | function_call(Function calling) | function_call(Function calling) |
|
||||
| Memory(Memory function) | mem_local_short(Local short-term memory) | mem_local_short(Local short-term memory) |
|
||||
|
||||
@@ -256,7 +256,7 @@ If you are a software developer, here is an [Open Letter to Developers](docs/con
|
||||
---
|
||||
|
||||
## Product Ecosystem 👬
|
||||
Xiaozhi is an ecosystem. When using this product, you can also check out other [excellent projects](https://github.com/78/xiaozhi-esp32?tab=readme-ov-file#%E7%9B%B8%E5%85%B3%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE) in this ecosystem
|
||||
Xiaozhi is an ecosystem. When using this product, you can also check out other [excellent projects](https://github.com/78/xiaozhi-esp32?tab=readme-ov-file#related-open-source-projects) in this ecosystem
|
||||
|
||||
| Project Name | Project Address | Project Description |
|
||||
|:---------------------|:--------|:--------|
|
||||
|
||||
+7
-7
@@ -182,8 +182,8 @@ Dự án này cung cấp hai phương pháp triển khai, vui lòng chọn theo
|
||||
#### 🚀 Lựa chọn phương pháp triển khai
|
||||
| Phương pháp triển khai | Đặc điểm | Tình huống áp dụng | Tài liệu triển khai | Yêu cầu cấu hình | Video hướng dẫn |
|
||||
|---------|------|---------|---------|---------|---------|
|
||||
| **Cài đặt tối giản** | Đối thoại thông minh, IOT, MCP, cảm nhận thị giác | Môi trường cấu hình thấp, dữ liệu lưu trong tệp cấu hình, không cần cơ sở dữ liệu | [①Phiên bản Docker](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②Triển khai mã nguồn](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| 2 nhân 4GB nếu dùng `FunASR`, 2 nhân 2GB nếu toàn API | - |
|
||||
| **Cài đặt toàn bộ module** | Đối thoại thông minh, IOT, điểm truy cập MCP, nhận dạng giọng nói, cảm nhận thị giác, OTA, bảng điều khiển thông minh | Trải nghiệm đầy đủ tính năng, dữ liệu lưu trong cơ sở dữ liệu |[①Phiên bản Docker](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [②Triển khai mã nguồn](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [③Hướng dẫn tự động cập nhật triển khai mã nguồn](./docs/dev-ops-integration.md) | 4 nhân 8GB nếu dùng `FunASR`, 2 nhân 4GB nếu toàn API| [Video hướng dẫn khởi động mã nguồn cục bộ](https://www.bilibili.com/video/BV1wBJhz4Ewe) |
|
||||
| **Cài đặt tối giản** | Đối thoại thông minh, quản lý đơn tác nhân | Môi trường cấu hình thấp, dữ liệu lưu trong tệp cấu hình, không cần cơ sở dữ liệu | [①Phiên bản Docker](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) / [②Triển khai mã nguồn](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)| 2 nhân 4GB nếu dùng `FunASR`, 2 nhân 2GB nếu toàn API | - |
|
||||
| **Cài đặt toàn bộ module** | Đối thoại thông minh, quản lý đa người dùng, quản lý đa tác nhân, bảng điều khiển thông minh | Trải nghiệm đầy đủ tính năng, dữ liệu lưu trong cơ sở dữ liệu |[①Phiên bản Docker](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [②Triển khai mã nguồn](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) / [③Hướng dẫn tự động cập nhật triển khai mã nguồn](./docs/dev-ops-integration.md) | 4 nhân 8GB nếu dùng `FunASR`, 2 nhân 4GB nếu toàn API| [Video hướng dẫn khởi động mã nguồn cục bộ](https://www.bilibili.com/video/BV1wBJhz4Ewe) |
|
||||
|
||||
Câu hỏi thường gặp và hướng dẫn liên quan, vui lòng tham khảo [liên kết này](./docs/FAQ.md)
|
||||
|
||||
@@ -210,10 +210,10 @@ Công cụ kiểm tra dịch vụ: https://2662r3426b.vicp.fun/test/
|
||||
|
||||
| Tên module | Cài đặt miễn phí cho người mới | Cấu hình streaming |
|
||||
|:---:|:---:|:---:|
|
||||
| ASR(Nhận dạng giọng nói) | FunASR(Local) | 👍FunASR(Chế độ GPU cục bộ) |
|
||||
| LLM(Mô hình lớn) | ChatGLMLLM(Zhipu glm-4-flash) | 👍AliLLM(qwen3-235b-a22b-instruct-2507) hoặc 👍DoubaoLLM(doubao-1-5-pro-32k-250115) |
|
||||
| VLLM(Mô hình lớn thị giác) | ChatGLMVLLM(Zhipu glm-4v-flash) | 👍QwenVLVLLM(Qwen qwen2.5-vl-3b-instructh) |
|
||||
| TTS(Tổng hợp giọng nói) | ✅LinkeraiTTS(Lingxi streaming) | 👍HuoshanDoubleStreamTTS(Tổng hợp giọng nói streaming kép Volcano) hoặc 👍AliyunStreamTTS(Tổng hợp giọng nói streaming Alibaba Cloud) |
|
||||
| ASR(Nhận dạng giọng nói) | FunASR(Local) | 👍XunfeiStreamASR(Xunfei Streaming) |
|
||||
| LLM(Mô hình lớn) | glm-4-flash(Zhipu) | 👍qwen-flash(Alibaba Bailian) |
|
||||
| VLLM(Mô hình lớn thị giác) | glm-4v-flash(Zhipu) | 👍qwen2.5-vl-3b-instructh(Alibaba Bailian) |
|
||||
| TTS(Tổng hợp giọng nói) | ✅LinkeraiTTS(Lingxi streaming) | 👍HuoshanDoubleStreamTTS(Volcano Streaming) |
|
||||
| Intent(Nhận dạng ý định) | function_call(Gọi hàm) | function_call(Gọi hàm) |
|
||||
| Memory(Chức năng bộ nhớ) | mem_local_short(Bộ nhớ ngắn hạn cục bộ) | mem_local_short(Bộ nhớ ngắn hạn cục bộ) |
|
||||
|
||||
@@ -259,7 +259,7 @@ Nếu bạn là một nhà phát triển phần mềm, đây có một [Lá thư
|
||||
---
|
||||
|
||||
## Hệ sinh thái sản phẩm 👬
|
||||
Xiaozhi là một hệ sinh thái, khi bạn sử dụng sản phẩm này, bạn cũng có thể xem các [dự án xuất sắc](https://github.com/78/xiaozhi-esp32?tab=readme-ov-file#%E7%9B%B8%E5%85%B3%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE) khác trong hệ sinh thái này
|
||||
Xiaozhi là một hệ sinh thái, khi bạn sử dụng sản phẩm này, bạn cũng có thể xem các [dự án xuất sắc](https://github.com/78/xiaozhi-esp32?tab=readme-ov-file#related-open-source-projects) khác trong hệ sinh thái này
|
||||
|
||||
---
|
||||
|
||||
|
||||
+6
-4
@@ -38,10 +38,10 @@ conda install conda-forge::ffmpeg
|
||||
|
||||
| 模块名称 | 入门全免费设置 | 流式配置 |
|
||||
|:---:|:---:|:---:|
|
||||
| ASR(语音识别) | FunASR(本地) | 👍FunASR(本地GPU模式) |
|
||||
| LLM(大模型) | ChatGLMLLM(智谱glm-4-flash) | 👍AliLLM(qwen3-235b-a22b-instruct-2507) 或 👍DoubaoLLM(doubao-1-5-pro-32k-250115) |
|
||||
| VLLM(视觉大模型) | ChatGLMVLLM(智谱glm-4v-flash) | 👍QwenVLVLLM(千问qwen2.5-vl-3b-instructh) |
|
||||
| TTS(语音合成) | ✅LinkeraiTTS(灵犀流式) | 👍HuoshanDoubleStreamTTS(火山双流式语音合成) 或 👍AliyunStreamTTS(阿里云流式语音合成) |
|
||||
| ASR(语音识别) | FunASR(本地) | 👍XunfeiStreamASR(讯飞流式) |
|
||||
| LLM(大模型) | glm-4-flash(智谱) | 👍qwen-flash(阿里百炼) |
|
||||
| VLLM(视觉大模型) | glm-4v-flash(智谱) | 👍qwen2.5-vl-3b-instructh(阿里百炼) |
|
||||
| TTS(语音合成) | ✅LinkeraiTTS(灵犀流式) | 👍HuoshanDoubleStreamTTS(火山流式) |
|
||||
| Intent(意图识别) | function_call(函数调用) | function_call(函数调用) |
|
||||
| Memory(记忆功能) | mem_local_short(本地短期记忆) | mem_local_short(本地短期记忆) |
|
||||
|
||||
@@ -69,6 +69,7 @@ VAD:
|
||||
### 9、编译固件相关教程
|
||||
1、[如何自己编译小智固件](./firmware-build.md)<br/>
|
||||
2、[如何基于虾哥编译好的固件修改OTA地址](./firmware-setting.md)<br/>
|
||||
3、[单模块部署如何配置固件OTA自动升级](./ota-upgrade-guide.md)<br/>
|
||||
|
||||
### 10、拓展相关教程
|
||||
1、[如何开启手机号码注册智控台](./ali-sms-integration.md)<br/>
|
||||
@@ -80,6 +81,7 @@ VAD:
|
||||
7、[如何开启声纹识别](./voiceprint-integration.md)<br/>
|
||||
8、[新闻插件源配置指南](./newsnow_plugin_config.md)<br/>
|
||||
9、[知识库ragflow集成指南](./ragflow-integration.md)<br/>
|
||||
10、[如何部署上下文源](./context-provider-integration.md)<br/>
|
||||
|
||||
### 11、语音克隆、本地语音部署相关教程
|
||||
1、[如何在智控台克隆音色](./huoshan-streamTTS-voice-cloning.md)<br/>
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
# 上下文源使用教程
|
||||
|
||||
## 概述
|
||||
|
||||
`上下文源`,就是为小智系统提示词的上下文添加【数据源】。
|
||||
|
||||
`上下文源` 在小智在唤醒那一刻,获取外部系统的数据,并将其动态注入到大模型的系统提示词(System Prompt)中。
|
||||
让其做到唤醒时感知世界某个事物的状态。
|
||||
|
||||
它和MCP、记忆有本质的区别:`上下文源`是强制让小智感知世界的数据;`记忆(Mem)`是让他知道之前聊了什么内容;`MCP(functionc all)`是当需要调用某项能力/知识的时候使用调用。
|
||||
|
||||
通过这个功能,在小智唤醒的一刹那,“感知”到:
|
||||
- 人体健康传感器状态(体温、血压、血氧状态等)
|
||||
- 业务系统的实时数据(服务器负载、待办数据、股票信息等)
|
||||
- 任何可以通过 HTTP API 获取的文本信息
|
||||
|
||||
**注意**:该功能只是方便小智在唤醒的时候感知事物的状态,而如果想要小智唤醒后实时获取事物的状态,建议在此功能上再结合MCP工具的调用。
|
||||
|
||||
## 工作原理
|
||||
|
||||
1. **配置源**:用户配置一个或多个 HTTP API 地址。
|
||||
2. **触发请求**:当系统构建 Prompt 时,如果发现模板中包含 `{{ dynamic_context }}` 占位符,会请求所有配置的 API。
|
||||
3. **自动注入**:系统会自动将 API 返回的数据格式化为 Markdown 列表,替换 `{{ dynamic_context }}` 占位符。
|
||||
|
||||
## 接口规范
|
||||
|
||||
为了让小智正确解析数据,您的 API 需要满足以下规范:
|
||||
|
||||
- **请求方式**:`GET`
|
||||
- **请求头**:系统会自动添加 `device-id` 字段到 Request Header。
|
||||
- **响应格式**:必须返回 JSON 格式,且包含 `code` 和 `data` 字段。
|
||||
|
||||
### 响应示例
|
||||
|
||||
**情况 1:返回键值对**
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"客厅温度": "26℃",
|
||||
"客厅湿度": "45%",
|
||||
"大门状态": "已关闭"
|
||||
}
|
||||
}
|
||||
```
|
||||
*注入效果:*
|
||||
```markdown
|
||||
<context>
|
||||
- **客厅温度:** 26℃
|
||||
- **客厅湿度:** 45%
|
||||
- **大门状态:** 已关闭
|
||||
</context>
|
||||
```
|
||||
|
||||
**情况 2:返回列表**
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": [
|
||||
"您有10个待办事项",
|
||||
"当前汽车的行驶速度是100km每小时"
|
||||
]
|
||||
}
|
||||
```
|
||||
*注入效果:*
|
||||
```markdown
|
||||
<context>
|
||||
- 您有10个待办事项
|
||||
- 当前汽车的行驶速度是100km每小时
|
||||
</context>
|
||||
```
|
||||
|
||||
## 配置指南
|
||||
|
||||
### 方式 1:智控台配置(全模块部署)
|
||||
|
||||
1. 登录智控台,进入**角色配置**页面。
|
||||
2. 找到**上下文源**配置项(点击“编辑源”按钮)。
|
||||
3. 点击**添加**,输入您的 API 地址。
|
||||
4. 如果 API 需要鉴权,可以在**请求头**部分添加 `Authorization` 或其他 Header。
|
||||
5. 保存配置。
|
||||
|
||||
### 方式 2:配置文件配置(单模块部署)
|
||||
|
||||
编辑 `xiaozhi-server/data/.config.yaml` 文件,添加 `context_providers` 配置段:
|
||||
|
||||
```yaml
|
||||
# 上下文源配置
|
||||
context_providers:
|
||||
- url: "http://api.example.com/data"
|
||||
headers:
|
||||
Authorization: "Bearer your-token"
|
||||
- url: "http://another-api.com/data"
|
||||
```
|
||||
|
||||
## 启用功能
|
||||
|
||||
默认情况下,系统的提示词模板文件(`data/.agent-base-prompt.txt`)中已经预置了 `{{ dynamic_context }}` 占位符,您无需手动添加。
|
||||
|
||||
**示例:**
|
||||
|
||||
```markdown
|
||||
<context>
|
||||
【重要!以下信息已实时提供,无需调用工具查询,请直接使用:】
|
||||
- **设备ID:** {{device_id}}
|
||||
- **当前时间:** {{current_time}}
|
||||
...
|
||||
{{ dynamic_context }}
|
||||
</context>
|
||||
```
|
||||
|
||||
**注意**:如果您不需要使用此功能,可以选择**不配置任何上下文源**,也可以从提示词模板文件中**删除** `{{ dynamic_context }}` 占位符。
|
||||
|
||||
## 附录:Mock 测试服务示例
|
||||
|
||||
为了方便您测试和开发,我们提供了一个简单的 Python Mock Server 脚本。您可以运行此脚本在本地模拟 API 接口。
|
||||
|
||||
**mock_api_server.py**
|
||||
|
||||
```python
|
||||
import http.server
|
||||
import socketserver
|
||||
import json
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
|
||||
# 设置端口号
|
||||
PORT = 8081
|
||||
|
||||
class MockRequestHandler(http.server.SimpleHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
# 解析路径和参数
|
||||
parsed_path = urlparse(self.path)
|
||||
path = parsed_path.path
|
||||
query = parse_qs(parsed_path.query)
|
||||
|
||||
response_data = {}
|
||||
status_code = 200
|
||||
|
||||
print(f"收到请求: {path}, 参数: {query}")
|
||||
|
||||
# Case 1: 模拟健康数据 (返回字典 Dict)
|
||||
# 路径参数风格: /health
|
||||
# device_id 从 Header 获取
|
||||
if path == "/health":
|
||||
device_id = self.headers.get("device-id", "unknown_device")
|
||||
print(f"device_id: {device_id}")
|
||||
response_data = {
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"测试设备ID": device_id,
|
||||
"心率": "80 bpm",
|
||||
"血压": "120/80 mmHg",
|
||||
"状态": "良好"
|
||||
}
|
||||
}
|
||||
|
||||
# Case 2: 模拟新闻列表 (返回列表 List)
|
||||
# 无参数: /news/list
|
||||
elif path == "/news/list":
|
||||
response_data = {
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"data": [
|
||||
"今日头条:Python 3.14 发布",
|
||||
"科技新闻:AI 助手改变生活",
|
||||
"本地新闻:明日有大雨,记得带伞"
|
||||
]
|
||||
}
|
||||
|
||||
# Case 3: 模拟天气简报 (返回字符串 String)
|
||||
# 无参数: /weather/simple
|
||||
elif path == "/weather/simple":
|
||||
response_data = {
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"data": "今日晴转多云,气温 20-25 度,空气质量优,适合出行。"
|
||||
}
|
||||
|
||||
# Case 4: 模拟设备详情 (Query参数风格)
|
||||
# 参数风格: /device/info
|
||||
# device_id 从 Header 获取
|
||||
elif path == "/device/info":
|
||||
device_id = self.headers.get("device-id", "unknown_device")
|
||||
response_data = {
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"查询方式": "Header参数",
|
||||
"设备ID": device_id,
|
||||
"电量": "85%",
|
||||
"固件": "v2.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
# Case 5: 404 Not Found
|
||||
else:
|
||||
status_code = 404
|
||||
response_data = {"error": "接口不存在"}
|
||||
|
||||
# 发送响应
|
||||
self.send_response(status_code)
|
||||
self.send_header('Content-type', 'application/json; charset=utf-8')
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps(response_data, ensure_ascii=False).encode('utf-8'))
|
||||
|
||||
# 启动服务
|
||||
# 允许地址重用,防止快速重启报错
|
||||
socketserver.TCPServer.allow_reuse_address = True
|
||||
with socketserver.TCPServer(("", PORT), MockRequestHandler) as httpd:
|
||||
print(f"==================================================")
|
||||
print(f"Mock API Server 已启动: http://localhost:{PORT}")
|
||||
print(f"可用接口列表:")
|
||||
print(f"1. [字典] http://localhost:{PORT}/health")
|
||||
print(f"2. [列表] http://localhost:{PORT}/news/list")
|
||||
print(f"3. [文本] http://localhost:{PORT}/weather/simple")
|
||||
print(f"4. [参数] http://localhost:{PORT}/device/info")
|
||||
print(f"==================================================")
|
||||
try:
|
||||
httpd.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
print("\n服务已停止")
|
||||
```
|
||||
@@ -23,6 +23,8 @@
|
||||
|
||||
### 2.将音色资源ID分配给系统账号
|
||||
|
||||
使用超级管理员账号登录智控台,点击顶部`参数字典`,在下拉菜单中,点击`系统功能配置`页面。在页面上勾选`音色克隆`,点击保存配置。即可在顶部菜单看到`音色克隆`按钮。
|
||||
|
||||
使用超级管理员账号登录智控台,点击顶部【音色克隆】、【音色资源】。
|
||||
|
||||
点击新增按钮,在【平台名称】选择“火山双流式语音合成”;
|
||||
|
||||
@@ -71,6 +71,7 @@ docker logs -f mcp-endpoint-server
|
||||
请你保留好上面两个`接口地址`,下一步要用到。
|
||||
|
||||
# 2、全模块部署时,怎么配置MCP接入点
|
||||
首先,你要开启MCP接入点功能。在智控台,点击顶部`参数字典`,在下拉菜单中,点击`系统功能配置`页面。在页面上勾选`MCP接入点`,点击`保存配置`。在`角色配置`页面,点击`编辑功能`按钮,即可看到`mcp接入点`功能。
|
||||
|
||||
如果你是全模块部署,使用管理员账号,登录智控台,点击顶部`参数字典`,选择`参数管理`功能。
|
||||
|
||||
|
||||
@@ -76,6 +76,7 @@ MQTT_PORT=1883 # MQTT服务器端口
|
||||
UDP_PORT=8884 # UDP服务器端口
|
||||
API_PORT=8007 # 管理API端口
|
||||
MQTT_SIGNATURE_KEY=test # MQTT签名密钥
|
||||
SERVER_SECRET=Te1st12134 # 服务器密钥,请保持和智控台(server.secret)一致或者和xiaozhi-server里(server.auth_key)保持一致
|
||||
```
|
||||
请注意`PUBLIC_IP`配置,确保其与实际公网IP一致,如果有域名就填域名。
|
||||
|
||||
@@ -85,6 +86,13 @@ MQTT_SIGNATURE_KEY=test # MQTT签名密钥
|
||||
- 注意不要用简单的密码,比如`123456`、`test`等。
|
||||
- 注意不要用简单的密码,比如`123456`、`test`等。
|
||||
|
||||
`SERVER_SECRET` 是用生成websocket连接的认证信息。
|
||||
|
||||
1、如果你是全模块部署,且你的智控台的参数管理里`server.auth.enabled`设置成了`true`,那么,`SERVER_SECRET`需要和智控台(`server.secret`)保持一致。
|
||||
|
||||
2、如果你是单模块部署,且你在配置文件里把`server.auth.enabled`设置成了`true`,那么,`SERVER_SECRET`需要和配置文件里(`server.auth_key`)保持一致。
|
||||
|
||||
|
||||
6. 启动MQTT网关
|
||||
```
|
||||
# 启动服务
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
# 单模块部署固件OTA自动升级配置指南
|
||||
|
||||
本教程将指导你如何在**单模块部署**场景下配置固件OTA自动升级功能,实现设备固件的自动更新。
|
||||
|
||||
如果你已经使用**全模块部署**,请忽略本教程。
|
||||
|
||||
## 功能介绍
|
||||
|
||||
在单模块部署中,xiaozhi-server内置了OTA固件管理功能,可以自动检测设备版本并下发升级固件。系统会根据设备型号和当前版本,自动匹配并推送最新的固件版本。
|
||||
|
||||
## 前提条件
|
||||
|
||||
- 你已经成功进行**单模块部署**并运行xiaozhi-server
|
||||
- 设备能够正常连接到服务器
|
||||
|
||||
## 第一步 准备固件文件
|
||||
|
||||
### 1. 创建固件存放目录
|
||||
|
||||
固件文件需要放在`data/bin/`目录下。如果该目录不存在,请手动创建:
|
||||
|
||||
```bash
|
||||
mkdir -p data/bin
|
||||
```
|
||||
|
||||
### 2. 固件文件命名规则
|
||||
|
||||
固件文件必须遵循以下命名格式:
|
||||
|
||||
```
|
||||
{设备型号}_{版本号}.bin
|
||||
```
|
||||
|
||||
**命名规则说明:**
|
||||
- `设备型号`:设备的型号名称,例如 `lichuang-dev`、`bread-compact-wifi` 等
|
||||
- `版本号`:固件版本号,必须以数字开头,支持数字、字母、点号、下划线和短横线,例如 `1.6.6`、`2.0.0` 等
|
||||
- 文件扩展名必须是 `.bin`
|
||||
|
||||
**命名示例:**
|
||||
```
|
||||
bread-compact-wifi_1.6.6.bin
|
||||
lichuang-dev_2.0.0.bin
|
||||
```
|
||||
|
||||
### 3. 放置固件文件
|
||||
|
||||
将准备好的固件文件(.bin文件)复制到`data/bin/`目录下:
|
||||
|
||||
重要的事情说三遍:升级的bin文件是`xiaozhi.bin`,不是全量固件文件`merged-binary.bin`!
|
||||
|
||||
重要的事情说三遍:升级的bin文件是`xiaozhi.bin`,不是全量固件文件`merged-binary.bin`!
|
||||
|
||||
重要的事情说三遍:升级的bin文件是`xiaozhi.bin`,不是全量固件文件`merged-binary.bin`!
|
||||
|
||||
```bash
|
||||
cp xiaozhi.bin data/bin/设备型号_版本号.bin
|
||||
```
|
||||
|
||||
例如:
|
||||
```bash
|
||||
cp xiaozhi.bin data/bin/bread-compact-wifi_1.6.6.bin
|
||||
```
|
||||
|
||||
## 第二步 配置公网访问地址(仅公网部署需要)
|
||||
|
||||
**注意:此步骤仅适用于单模块公网部署的场景。**
|
||||
|
||||
如果你的xiaozhi-server是公网部署(使用公网IP或域名),**必须**配置`server.vision_explain`参数,因为OTA固件下载地址会使用该配置的域名和端口。
|
||||
|
||||
如果你是局域网部署,可以跳过此步骤。
|
||||
|
||||
### 为什么要配置这个参数?
|
||||
|
||||
在单模块部署中,系统生成固件下载地址时,会使用`vision_explain`配置的域名和端口作为基础地址。如果不配置或配置错误,设备将无法访问固件下载地址。
|
||||
|
||||
### 配置方法
|
||||
|
||||
打开`data/.config.yaml`文件,找到`server`配置段,设置`vision_explain`参数:
|
||||
|
||||
```yaml
|
||||
server:
|
||||
vision_explain: http://你的域名或IP:端口号/mcp/vision/explain
|
||||
```
|
||||
|
||||
**配置示例:**
|
||||
|
||||
局域网部署(默认):
|
||||
```yaml
|
||||
server:
|
||||
vision_explain: http://192.168.1.100:8003/mcp/vision/explain
|
||||
```
|
||||
|
||||
公网域名部署:
|
||||
```yaml
|
||||
server:
|
||||
vision_explain: http://yourdomain.com:8003/mcp/vision/explain
|
||||
```
|
||||
|
||||
### 注意事项
|
||||
|
||||
- 域名或IP必须是设备能够访问的地址
|
||||
- 如果使用Docker部署,不能使用Docker内部地址(如127.0.0.1或localhost)
|
||||
- 如果你使用了nginx反向代理,请填写对外的地址和端口号,不是本项目运行的端口号
|
||||
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 1. 设备收不到固件更新
|
||||
|
||||
**可能原因和解决方法:**
|
||||
|
||||
- 检查固件文件命名是否符合规则:`{型号}_{版本号}.bin`
|
||||
- 检查固件文件是否正确放置在`data/bin/`目录
|
||||
- 检查设备型号是否与固件文件名中的型号匹配
|
||||
- 检查固件版本号是否高于设备当前版本
|
||||
- 查看服务器日志,确认OTA请求是否正常处理
|
||||
|
||||
### 2. 设备报告下载地址无法访问
|
||||
|
||||
**可能原因和解决方法:**
|
||||
|
||||
- 检查`server.vision_explain`配置的域名或IP是否正确
|
||||
- 确认端口号配置正确(默认8003)
|
||||
- 如果是公网部署,确保设备能够访问该公网地址
|
||||
- 如果是Docker部署,确保不是使用了内部地址(127.0.0.1)
|
||||
- 检查防火墙是否开放了对应端口
|
||||
- 如果你使用了nginx反向代理,请填写对外的地址和端口号,不是本项目运行的端口号
|
||||
|
||||
### 3. 如何确认设备当前版本
|
||||
|
||||
查看OTA请求日志,日志中会显示设备上报的版本号:
|
||||
|
||||
```
|
||||
[ota_handler] - 设备 AA:BB:CC:DD:EE:FF 固件已是最新: 1.6.6
|
||||
```
|
||||
|
||||
### 4. 固件文件放置后没有生效
|
||||
|
||||
系统有30秒的缓存时间(默认),可以:
|
||||
- 等待30秒后再让设备发起OTA请求
|
||||
- 重启xiaozhi-server服务
|
||||
- 调整`firmware_cache_ttl`配置为更短的时间
|
||||
@@ -156,6 +156,14 @@ services:
|
||||
|
||||
编辑`ragflow/docker`文件夹下的`.env`文件,找到以下配置,逐个搜索,逐个修改!逐个搜索,逐个修改!
|
||||
|
||||
下面对于`.env`文件的修改,60%的人会忽略`MYSQL_USER`配置导致ragflow启动不成功,因此,需要强调三次:
|
||||
|
||||
强调第一次:如果你的`.env`文件如果没有`MYSQL_USER`配置,请在配置文件增加这项!
|
||||
|
||||
强调第二次:如果你的`.env`文件如果没有`MYSQL_USER`配置,请在配置文件增加这项!
|
||||
|
||||
强调第三次:如果你的`.env`文件如果没有`MYSQL_USER`配置,请在配置文件增加这项!
|
||||
|
||||
``` env
|
||||
# 端口设置
|
||||
SVR_WEB_HTTP_PORT=8008 # HTTP端口
|
||||
@@ -230,9 +238,11 @@ docker-compose -f docker-compose.yml up -d
|
||||
在弹框中,点击"Create new Key"按钮,生成一个API Key。复制这个`API Key`,你稍后会用到。
|
||||
|
||||
# 第二步 配置到智控台
|
||||
确保你的智控台版本是`0.8.7`或以上。使用超级管理员账号登录到智控台。在顶部导航栏中,点击`模型配置`,在左侧导航栏中,点击`知识库`。
|
||||
确保你的智控台版本是`0.8.7`或以上。使用超级管理员账号登录到智控台。
|
||||
|
||||
在列表中找到`RAG_RAGFlow`,点击`编辑`按钮。
|
||||
首先,你要先开启知识库功能。在顶部导航栏中,点击`参数字典`,在下拉菜单中,点击`系统功能配置`页面。在页面上勾选`知识库`,点击`保存配置`。即可在导航栏看到`知识库`功能。
|
||||
|
||||
在顶部导航栏中,点击`模型配置`,在左侧导航栏中,点击`知识库`。在列表中找到`RAG_RAGFlow`,点击`编辑`按钮。
|
||||
|
||||
在`服务地址`中,填写`http://你的ragflow服务的局域网IP:8008`,例如我的ragflow服务的局域网IP是`192.168.1.100`,那么我就填写`http://192.168.1.100:8008`。
|
||||
|
||||
|
||||
@@ -164,6 +164,8 @@ http://192.168.1.25:8005/voiceprint/health?key=abcd
|
||||
# 2、全模块部署时,怎么配置声纹识别
|
||||
|
||||
## 第一步 配置接口
|
||||
首先,你要开启声纹识别功能。在智控台,点击顶部`参数字典`,在下拉菜单中,点击`系统功能配置`页面。在页面上勾选`声纹识别`,点击`保存配置`。即可在新建智能体的卡片上看到`声纹识别`按钮。
|
||||
|
||||
如果你是全模块部署,使用管理员账号,登录智控台,点击顶部`参数字典`,选择`参数管理`功能。
|
||||
|
||||
然后搜索参数`server.voice_print`,此时,它的值应该是`null`值。
|
||||
|
||||
@@ -43,7 +43,7 @@ public class SwaggerConfig {
|
||||
public GroupedOpenApi oatApi() {
|
||||
return GroupedOpenApi.builder()
|
||||
.group("ota")
|
||||
.pathsToMatch("/ota/**")
|
||||
.pathsToMatch("/ota/**", "/otaMag/**")
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@@ -141,6 +141,11 @@ public interface Constant {
|
||||
*/
|
||||
String SERVER_MQTT_SECRET = "server.mqtt_signature_key";
|
||||
|
||||
/**
|
||||
* WebSocket认证开关
|
||||
*/
|
||||
String SERVER_AUTH_ENABLED = "server.auth.enabled";
|
||||
|
||||
/**
|
||||
* 无记忆
|
||||
*/
|
||||
@@ -299,7 +304,7 @@ public interface Constant {
|
||||
/**
|
||||
* 版本号
|
||||
*/
|
||||
public static final String VERSION = "0.8.8";
|
||||
public static final String VERSION = "0.9.1";
|
||||
|
||||
/**
|
||||
* 无效固件URL
|
||||
|
||||
@@ -228,4 +228,21 @@ public interface ErrorCode {
|
||||
|
||||
// 智能体模板相关错误码(补充)
|
||||
int AGENT_TEMPLATE_NOT_FOUND = 10183; // 默认智能体未找到
|
||||
|
||||
// 知识库适配器相关错误码
|
||||
int RAG_ADAPTER_TYPE_NOT_SUPPORTED = 10184; // 不支持的适配器类型
|
||||
int RAG_CONFIG_VALIDATION_FAILED = 10185; // RAG配置验证失败
|
||||
int RAG_ADAPTER_CREATION_FAILED = 10186; // 适配器创建失败
|
||||
int RAG_ADAPTER_INIT_FAILED = 10187; // 适配器初始化失败
|
||||
int RAG_ADAPTER_CONNECTION_FAILED = 10188; // 适配器连接测试失败
|
||||
int RAG_ADAPTER_OPERATION_FAILED = 10189; // 适配器操作失败
|
||||
int RAG_ADAPTER_NOT_FOUND = 10190; // 适配器未找到
|
||||
int RAG_ADAPTER_CACHE_ERROR = 10191; // 适配器缓存错误
|
||||
int RAG_ADAPTER_TYPE_NOT_FOUND = 10192; // 适配器类型未找到
|
||||
|
||||
// 设备工具相关错误码
|
||||
int DEVICE_ID_NOT_NULL = 10193; // 设备ID不能为空
|
||||
int DEVICE_NOT_EXIST = 10194; // 设备不存在
|
||||
int OTA_UPLOAD_COUNT_EXCEED = 10195; // OTA上传次数超过限制
|
||||
|
||||
}
|
||||
|
||||
@@ -159,4 +159,32 @@ public class RedisKeys {
|
||||
public static String getKnowledgeBaseCacheKey(String datasetId) {
|
||||
return "knowledge:base:" + datasetId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取临时注册设备标记key
|
||||
*/
|
||||
public static String getTmpRegisterMacKey(String deviceId) {
|
||||
return "tmp_register_mac:" + deviceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* OTA绑定设备
|
||||
*/
|
||||
public static String getOtaActivationCode(String activationCode) {
|
||||
return "ota:activation:code:" + activationCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* OTA获取设备mac相关信息
|
||||
*/
|
||||
public static String getOtaDeviceActivationInfo(String deviceId) {
|
||||
return "ota:activation:data:" + deviceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* OTA上传次数
|
||||
*/
|
||||
public static String getOtaUploadCountKey(Long username) {
|
||||
return "ota:upload:count:" + username;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package xiaozhi.common.utils;
|
||||
|
||||
import cn.hutool.core.util.ReUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 通用工具类
|
||||
*/
|
||||
public class ToolUtil {
|
||||
private static final Logger logger = LoggerFactory.getLogger(ToolUtil.class);
|
||||
|
||||
/**
|
||||
* 对象是否不为空(新增)
|
||||
*/
|
||||
public static boolean isNotEmpty(Object o) {
|
||||
return !isEmpty(o);
|
||||
}
|
||||
|
||||
/**
|
||||
* 对象是否为空
|
||||
*/
|
||||
public static boolean isEmpty(Object o) {
|
||||
if (o == null) {
|
||||
return true;
|
||||
}
|
||||
if (o instanceof String) {
|
||||
if (o.toString().trim().equals("")) {
|
||||
return true;
|
||||
}
|
||||
} else if (o instanceof List) {
|
||||
if (((List) o).size() == 0) {
|
||||
return true;
|
||||
}
|
||||
} else if (o instanceof Map) {
|
||||
if (((Map) o).size() == 0) {
|
||||
return true;
|
||||
}
|
||||
} else if (o instanceof Set) {
|
||||
if (((Set) o).size() == 0) {
|
||||
return true;
|
||||
}
|
||||
} else if (o instanceof Object[]) {
|
||||
if (((Object[]) o).length == 0) {
|
||||
return true;
|
||||
}
|
||||
} else if (o instanceof int[]) {
|
||||
if (((int[]) o).length == 0) {
|
||||
return true;
|
||||
}
|
||||
} else if (o instanceof long[]) {
|
||||
if (((long[]) o).length == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对象组中是否存在空对象
|
||||
*/
|
||||
public static boolean isOneEmpty(Object... os) {
|
||||
for (Object o : os) {
|
||||
if (isEmpty(o)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对象组中是否全是空对象
|
||||
*/
|
||||
public static boolean isAllEmpty(Object... os) {
|
||||
for (Object o : os) {
|
||||
if (!isEmpty(o)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+36
-2
@@ -44,6 +44,8 @@ import xiaozhi.modules.agent.entity.AgentEntity;
|
||||
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
||||
import xiaozhi.modules.agent.service.AgentChatAudioService;
|
||||
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||
import xiaozhi.modules.agent.service.AgentChatSummaryService;
|
||||
import xiaozhi.modules.agent.service.AgentContextProviderService;
|
||||
import xiaozhi.modules.agent.service.AgentPluginMappingService;
|
||||
import xiaozhi.modules.agent.service.AgentService;
|
||||
import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||
@@ -64,14 +66,20 @@ public class AgentController {
|
||||
private final AgentChatHistoryService agentChatHistoryService;
|
||||
private final AgentChatAudioService agentChatAudioService;
|
||||
private final AgentPluginMappingService agentPluginMappingService;
|
||||
private final AgentContextProviderService agentContextProviderService;
|
||||
private final AgentChatSummaryService agentChatSummaryService;
|
||||
private final RedisUtils redisUtils;
|
||||
|
||||
@GetMapping("/list")
|
||||
@Operation(summary = "获取用户智能体列表")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<List<AgentDTO>> getUserAgents() {
|
||||
public Result<List<AgentDTO>> getUserAgents(
|
||||
@RequestParam(value = "keyword", required = false) String keyword,
|
||||
@RequestParam(value = "searchType", defaultValue = "name") String searchType) {
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
List<AgentDTO> agents = agentService.getUserAgents(user.getId());
|
||||
|
||||
// 直接调用整合后的getUserAgents方法,无需再区分搜索和普通查询
|
||||
List<AgentDTO> agents = agentService.getUserAgents(user.getId(), keyword, searchType);
|
||||
return new Result<List<AgentDTO>>().ok(agents);
|
||||
}
|
||||
|
||||
@@ -117,6 +125,27 @@ public class AgentController {
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
@PostMapping("/chat-summary/{sessionId}/save")
|
||||
@Operation(summary = "根据会话ID生成聊天记录总结并保存(异步执行)")
|
||||
public Result<Void> generateAndSaveChatSummary(@PathVariable String sessionId) {
|
||||
try {
|
||||
// 异步执行总结生成任务,立即返回成功响应
|
||||
new Thread(() -> {
|
||||
try {
|
||||
agentChatSummaryService.generateAndSaveChatSummary(sessionId);
|
||||
System.out.println("异步执行会话 " + sessionId + " 的聊天记录总结完成");
|
||||
} catch (Exception e) {
|
||||
System.err.println("异步执行会话 " + sessionId + " 的聊天记录总结失败: " + e.getMessage());
|
||||
}
|
||||
}).start();
|
||||
|
||||
// 立即返回成功响应,不等待总结生成完成
|
||||
return new Result<Void>().ok(null);
|
||||
} catch (Exception e) {
|
||||
return new Result<Void>().error("启动异步总结生成任务失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
@Operation(summary = "更新智能体")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
@@ -135,6 +164,8 @@ public class AgentController {
|
||||
agentChatHistoryService.deleteByAgentId(id, true, true);
|
||||
// 删除关联的插件
|
||||
agentPluginMappingService.deleteByAgentId(id);
|
||||
// 删除关联的上下文源配置
|
||||
agentContextProviderService.deleteByAgentId(id);
|
||||
// 再删除智能体
|
||||
agentService.deleteById(id);
|
||||
return new Result<>();
|
||||
@@ -182,6 +213,7 @@ public class AgentController {
|
||||
List<AgentChatHistoryDTO> result = agentChatHistoryService.getChatHistoryBySessionId(id, sessionId);
|
||||
return new Result<List<AgentChatHistoryDTO>>().ok(result);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/chat-history/user")
|
||||
@Operation(summary = "获取智能体聊天记录(用户)")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
@@ -243,4 +275,6 @@ public class AgentController {
|
||||
.body(audioData);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
+1
-1
@@ -28,7 +28,7 @@ public class AgentMcpAccessPointController {
|
||||
/**
|
||||
* 获取智能体的Mcp接入点地址
|
||||
*
|
||||
* @param audioId 智能体id
|
||||
* @param agentId 智能体id
|
||||
* @return 返回错误提醒或者Mcp接入点地址
|
||||
*/
|
||||
@Operation(summary = "获取智能体的Mcp接入点地址")
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package xiaozhi.modules.agent.dao;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import xiaozhi.common.dao.BaseDao;
|
||||
import xiaozhi.modules.agent.entity.AgentContextProviderEntity;
|
||||
|
||||
@Mapper
|
||||
public interface AgentContextProviderDao extends BaseDao<AgentContextProviderEntity> {
|
||||
}
|
||||
+18
-6
@@ -1,6 +1,9 @@
|
||||
package xiaozhi.modules.agent.dao;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
@@ -15,12 +18,6 @@ import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
|
||||
*/
|
||||
@Mapper
|
||||
public interface AiAgentChatHistoryDao extends BaseMapper<AgentChatHistoryEntity> {
|
||||
/**
|
||||
* 根据智能体ID删除音频
|
||||
*
|
||||
* @param agentId 智能体ID
|
||||
*/
|
||||
void deleteAudioByAgentId(String agentId);
|
||||
|
||||
/**
|
||||
* 根据智能体ID删除聊天历史记录
|
||||
@@ -35,4 +32,19 @@ public interface AiAgentChatHistoryDao extends BaseMapper<AgentChatHistoryEntity
|
||||
* @param agentId 智能体ID
|
||||
*/
|
||||
void deleteAudioIdByAgentId(String agentId);
|
||||
|
||||
/**
|
||||
* 根据智能体ID获取所有音频ID列表
|
||||
*
|
||||
* @param agentId 智能体ID
|
||||
* @return 音频ID列表
|
||||
*/
|
||||
List<String> getAudioIdsByAgentId(String agentId);
|
||||
|
||||
/**
|
||||
* 批量删除音频
|
||||
*
|
||||
* @param audioIds 音频ID列表
|
||||
*/
|
||||
void deleteAudioByIds(@Param("audioIds") List<String> audioIds);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package xiaozhi.modules.agent.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 智能体聊天记录总结DTO
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "智能体聊天记录总结对象")
|
||||
public class AgentChatSummaryDTO {
|
||||
|
||||
@Schema(description = "会话ID")
|
||||
private String sessionId;
|
||||
|
||||
@Schema(description = "智能体ID")
|
||||
private String agentId;
|
||||
|
||||
@Schema(description = "总结内容")
|
||||
private String summary;
|
||||
|
||||
@Schema(description = "总结状态")
|
||||
private boolean success;
|
||||
|
||||
@Schema(description = "错误信息")
|
||||
private String errorMessage;
|
||||
|
||||
public AgentChatSummaryDTO() {
|
||||
this.success = true;
|
||||
}
|
||||
|
||||
public AgentChatSummaryDTO(String sessionId, String agentId, String summary) {
|
||||
this.sessionId = sessionId;
|
||||
this.agentId = agentId;
|
||||
this.summary = summary;
|
||||
this.success = true;
|
||||
}
|
||||
|
||||
public AgentChatSummaryDTO(String sessionId, String errorMessage) {
|
||||
this.sessionId = sessionId;
|
||||
this.errorMessage = errorMessage;
|
||||
this.success = false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package xiaozhi.modules.agent.dto;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@@ -69,6 +69,9 @@ public class AgentUpdateDTO implements Serializable {
|
||||
@Schema(description = "排序", example = "1", nullable = true)
|
||||
private Integer sort;
|
||||
|
||||
@Schema(description = "上下文源配置", nullable = true)
|
||||
private List<ContextProviderDTO> contextProviders;
|
||||
|
||||
@Data
|
||||
@Schema(description = "插件函数信息")
|
||||
public static class FunctionInfo implements Serializable {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package xiaozhi.modules.agent.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Map;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "上下文源配置DTO")
|
||||
public class ContextProviderDTO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "URL地址")
|
||||
private String url;
|
||||
|
||||
@Schema(description = "请求头")
|
||||
private Map<String, Object> headers;
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package xiaozhi.modules.agent.entity;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import xiaozhi.modules.agent.dto.ContextProviderDTO;
|
||||
|
||||
@Data
|
||||
@TableName(value = "ai_agent_context_provider", autoResultMap = true)
|
||||
@Schema(description = "智能体上下文源配置")
|
||||
public class AgentContextProviderEntity {
|
||||
|
||||
@TableId(type = IdType.ASSIGN_UUID)
|
||||
@Schema(description = "主键")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "智能体ID")
|
||||
private String agentId;
|
||||
|
||||
@Schema(description = "上下文源配置")
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private List<ContextProviderDTO> contextProviders;
|
||||
|
||||
@Schema(description = "创建者")
|
||||
private Long creator;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private Date createdAt;
|
||||
|
||||
@Schema(description = "更新者")
|
||||
private Long updater;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
private Date updatedAt;
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package xiaozhi.modules.agent.service;
|
||||
|
||||
/**
|
||||
* 智能体聊天记录总结服务接口
|
||||
*/
|
||||
public interface AgentChatSummaryService {
|
||||
|
||||
/**
|
||||
* 根据会话ID生成聊天记录总结并保存到智能体记忆
|
||||
*
|
||||
* @param sessionId 会话ID
|
||||
* @return 保存结果
|
||||
*/
|
||||
boolean generateAndSaveChatSummary(String sessionId);
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package xiaozhi.modules.agent.service;
|
||||
|
||||
import xiaozhi.common.service.BaseService;
|
||||
import xiaozhi.modules.agent.entity.AgentContextProviderEntity;
|
||||
|
||||
public interface AgentContextProviderService extends BaseService<AgentContextProviderEntity> {
|
||||
/**
|
||||
* 根据智能体ID获取上下文源配置
|
||||
* @param agentId 智能体ID
|
||||
* @return 上下文源配置实体
|
||||
*/
|
||||
AgentContextProviderEntity getByAgentId(String agentId);
|
||||
|
||||
/**
|
||||
* 保存或更新上下文源配置
|
||||
* @param entity 实体
|
||||
*/
|
||||
void saveOrUpdateByAgentId(AgentContextProviderEntity entity);
|
||||
|
||||
/**
|
||||
* 根据智能体ID删除上下文源配置
|
||||
* @param agentId 智能体ID
|
||||
*/
|
||||
void deleteByAgentId(String agentId);
|
||||
}
|
||||
@@ -54,9 +54,11 @@ public interface AgentService extends BaseService<AgentEntity> {
|
||||
* 获取用户智能体列表
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param keyword 搜索关键词
|
||||
* @param searchType 搜索类型(name - 按名称搜索,mac - 按MAC地址搜索)
|
||||
* @return 智能体列表
|
||||
*/
|
||||
List<AgentDTO> getUserAgents(Long userId);
|
||||
List<AgentDTO> getUserAgents(Long userId, String keyword, String searchType);
|
||||
|
||||
/**
|
||||
* 根据智能体ID获取设备数量
|
||||
@@ -98,4 +100,6 @@ public interface AgentService extends BaseService<AgentEntity> {
|
||||
* @return 创建的智能体ID
|
||||
*/
|
||||
String createAgent(AgentCreateDTO dto);
|
||||
|
||||
|
||||
}
|
||||
|
||||
+6
-2
@@ -17,6 +17,7 @@ import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
|
||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||
import xiaozhi.modules.agent.service.AgentChatAudioService;
|
||||
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||
import xiaozhi.modules.agent.service.AgentChatSummaryService;
|
||||
import xiaozhi.modules.agent.service.AgentService;
|
||||
import xiaozhi.modules.agent.service.biz.AgentChatHistoryBizService;
|
||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||
@@ -36,6 +37,7 @@ public class AgentChatHistoryBizServiceImpl implements AgentChatHistoryBizServic
|
||||
private final AgentService agentService;
|
||||
private final AgentChatHistoryService agentChatHistoryService;
|
||||
private final AgentChatAudioService agentChatAudioService;
|
||||
private final AgentChatSummaryService agentChatSummaryService;
|
||||
private final RedisUtils redisUtils;
|
||||
private final DeviceService deviceService;
|
||||
|
||||
@@ -50,7 +52,8 @@ public class AgentChatHistoryBizServiceImpl implements AgentChatHistoryBizServic
|
||||
public Boolean report(AgentChatHistoryReportDTO report) {
|
||||
String macAddress = report.getMacAddress();
|
||||
Byte chatType = report.getChatType();
|
||||
Long reportTimeMillis = null != report.getReportTime() ? report.getReportTime() * 1000 : System.currentTimeMillis();
|
||||
Long reportTimeMillis = null != report.getReportTime() ? report.getReportTime() * 1000
|
||||
: System.currentTimeMillis();
|
||||
log.info("小智设备聊天上报请求: macAddress={}, type={} reportTime={}", macAddress, chatType, reportTimeMillis);
|
||||
|
||||
// 根据设备MAC地址查询对应的默认智能体,判断是否需要上报
|
||||
@@ -105,7 +108,8 @@ public class AgentChatHistoryBizServiceImpl implements AgentChatHistoryBizServic
|
||||
/**
|
||||
* 组装上报数据
|
||||
*/
|
||||
private void saveChatText(AgentChatHistoryReportDTO report, String agentId, String macAddress, String audioId, Long reportTime) {
|
||||
private void saveChatText(AgentChatHistoryReportDTO report, String agentId, String macAddress, String audioId,
|
||||
Long reportTime) {
|
||||
// 构建聊天记录实体
|
||||
AgentChatHistoryEntity entity = AgentChatHistoryEntity.builder()
|
||||
.macAddress(macAddress)
|
||||
|
||||
+12
-2
@@ -5,6 +5,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import cn.hutool.core.collection.ListUtil;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -18,6 +19,7 @@ import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.common.utils.JsonUtils;
|
||||
import xiaozhi.common.utils.ToolUtil;
|
||||
import xiaozhi.modules.agent.Enums.AgentChatHistoryType;
|
||||
import xiaozhi.modules.agent.dao.AiAgentChatHistoryDao;
|
||||
import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
|
||||
@@ -84,7 +86,15 @@ public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryD
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteByAgentId(String agentId, Boolean deleteAudio, Boolean deleteText) {
|
||||
if (deleteAudio) {
|
||||
baseMapper.deleteAudioByAgentId(agentId);
|
||||
// 分批删除音频,避免超时
|
||||
List<String> audioIds = baseMapper.getAudioIdsByAgentId(agentId);
|
||||
if (ToolUtil.isNotEmpty(audioIds)) {
|
||||
// 每批删除1000条
|
||||
List<List<String>> batch = ListUtil.split(audioIds, 1000);
|
||||
batch.forEach(dataList->{
|
||||
baseMapper.deleteAudioByIds(dataList);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (deleteAudio && !deleteText) {
|
||||
baseMapper.deleteAudioIdByAgentId(agentId);
|
||||
@@ -107,7 +117,7 @@ public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryD
|
||||
// 添加此行,确保查询结果按照创建时间降序排列
|
||||
// 使用id的原因:数据形式,id越大的创建时间就越晚,所以使用id的结果和创建时间降序排列结果一样
|
||||
// id作为降序排列的优势,性能高,有主键索引,不用在排序的时候重新进行排除扫描比较
|
||||
.orderByDesc(AgentChatHistoryEntity::getId);
|
||||
.orderByDesc(AgentChatHistoryEntity::getId);
|
||||
|
||||
// 构建分页查询,查询前50页数据
|
||||
Page<AgentChatHistoryEntity> pageParam = new Page<>(0, 50);
|
||||
|
||||
+423
@@ -0,0 +1,423 @@
|
||||
package xiaozhi.modules.agent.service.impl;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentChatSummaryDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentMemoryDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
|
||||
import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
|
||||
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||
import xiaozhi.modules.agent.service.AgentChatSummaryService;
|
||||
import xiaozhi.modules.agent.service.AgentService;
|
||||
import xiaozhi.modules.agent.vo.AgentInfoVO;
|
||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||
import xiaozhi.modules.device.service.DeviceService;
|
||||
import xiaozhi.modules.llm.service.LLMService;
|
||||
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
||||
import xiaozhi.modules.model.service.ModelConfigService;
|
||||
|
||||
/**
|
||||
* 智能体聊天记录总结服务实现类
|
||||
* 实现Python端mem_local_short.py中的总结逻辑
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AgentChatSummaryServiceImpl implements AgentChatSummaryService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(AgentChatSummaryServiceImpl.class);
|
||||
|
||||
private final AgentChatHistoryService agentChatHistoryService;
|
||||
private final AgentService agentService;
|
||||
private final DeviceService deviceService;
|
||||
private final LLMService llmService;
|
||||
private final ModelConfigService modelConfigService;
|
||||
|
||||
// 总结规则常量
|
||||
private static final int MAX_SUMMARY_LENGTH = 1800; // 最大总结长度
|
||||
private static final Pattern JSON_PATTERN = Pattern.compile("\\{.*?\\}", Pattern.DOTALL);
|
||||
private static final Pattern DEVICE_CONTROL_PATTERN = Pattern.compile("设备控制|设备操作|控制设备|设备状态",
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern WEATHER_PATTERN = Pattern.compile("天气|温度|湿度|降雨|气象", Pattern.CASE_INSENSITIVE);
|
||||
private static final Pattern DATE_PATTERN = Pattern.compile("日期|时间|星期|月份|年份", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private AgentChatSummaryDTO generateChatSummary(String sessionId) {
|
||||
try {
|
||||
System.out.println("开始生成会话 " + sessionId + " 的聊天记录总结");
|
||||
|
||||
// 1. 根据sessionId获取聊天记录
|
||||
List<AgentChatHistoryDTO> chatHistory = getChatHistoryBySessionId(sessionId);
|
||||
if (chatHistory == null || chatHistory.isEmpty()) {
|
||||
return new AgentChatSummaryDTO(sessionId, "未找到该会话的聊天记录");
|
||||
}
|
||||
|
||||
// 2. 获取智能体信息
|
||||
String agentId = getAgentIdFromSession(sessionId, chatHistory);
|
||||
if (StringUtils.isBlank(agentId)) {
|
||||
return new AgentChatSummaryDTO(sessionId, "无法获取智能体信息");
|
||||
}
|
||||
|
||||
// 3. 提取关键对话内容
|
||||
List<String> meaningfulMessages = extractMeaningfulMessages(chatHistory);
|
||||
if (meaningfulMessages.isEmpty()) {
|
||||
return new AgentChatSummaryDTO(sessionId, "没有有效的对话内容可总结");
|
||||
}
|
||||
|
||||
// 4. 生成总结(generateSummaryFromMessages方法已包含长度限制逻辑)
|
||||
String summary = generateSummaryFromMessages(meaningfulMessages, agentId);
|
||||
|
||||
log.info("成功生成会话 {} 的聊天记录总结,长度: {} 字符", sessionId, summary.length());
|
||||
return new AgentChatSummaryDTO(sessionId, agentId, summary);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("生成会话 {} 的聊天记录总结时发生错误: {}", sessionId, e.getMessage());
|
||||
return new AgentChatSummaryDTO(sessionId, "生成总结时发生错误: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean generateAndSaveChatSummary(String sessionId) {
|
||||
try {
|
||||
// 1. 生成总结
|
||||
AgentChatSummaryDTO summaryDTO = generateChatSummary(sessionId);
|
||||
if (!summaryDTO.isSuccess()) {
|
||||
log.info("生成总结失败: {}", summaryDTO.getErrorMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 获取设备信息(通过会话关联的设备)
|
||||
DeviceEntity device = getDeviceBySessionId(sessionId);
|
||||
if (device == null) {
|
||||
log.info("未找到与会话 {} 关联的设备", sessionId);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. 更新智能体记忆
|
||||
AgentMemoryDTO memoryDTO = new AgentMemoryDTO();
|
||||
memoryDTO.setSummaryMemory(summaryDTO.getSummary());
|
||||
|
||||
// 调用现有接口更新记忆
|
||||
agentService.updateAgentById(device.getAgentId(),
|
||||
new AgentUpdateDTO() {
|
||||
{
|
||||
setSummaryMemory(summaryDTO.getSummary());
|
||||
}
|
||||
});
|
||||
|
||||
log.info("成功保存会话 {} 的聊天记录总结到智能体 {}", sessionId, device.getAgentId());
|
||||
return true;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("保存会话 {} 的聊天记录总结时发生错误: {}", sessionId, e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据会话ID获取聊天记录
|
||||
*/
|
||||
private List<AgentChatHistoryDTO> getChatHistoryBySessionId(String sessionId) {
|
||||
try {
|
||||
// 这里需要根据sessionId获取聊天记录
|
||||
// 由于现有接口需要agentId,我们需要先找到关联的agentId
|
||||
String agentId = findAgentIdBySessionId(sessionId);
|
||||
if (StringUtils.isBlank(agentId)) {
|
||||
return null;
|
||||
}
|
||||
return agentChatHistoryService.getChatHistoryBySessionId(agentId, sessionId);
|
||||
} catch (Exception e) {
|
||||
log.error("获取会话 {} 的聊天记录失败: {}", sessionId, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据会话ID查找关联的智能体ID
|
||||
*/
|
||||
private String findAgentIdBySessionId(String sessionId) {
|
||||
try {
|
||||
// 查询该会话的第一条记录获取agentId
|
||||
QueryWrapper<AgentChatHistoryEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.select("agent_id")
|
||||
.eq("session_id", sessionId)
|
||||
.last("LIMIT 1");
|
||||
|
||||
AgentChatHistoryEntity entity = agentChatHistoryService.getOne(wrapper);
|
||||
return entity != null ? entity.getAgentId() : null;
|
||||
} catch (Exception e) {
|
||||
log.error("根据会话ID {} 查找智能体ID失败: {}", sessionId, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从会话中获取智能体ID
|
||||
*/
|
||||
private String getAgentIdFromSession(String sessionId, List<AgentChatHistoryDTO> chatHistory) {
|
||||
// 直接从数据库查询智能体ID
|
||||
return findAgentIdBySessionId(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取有意义的对话内容(只提取用户消息,排除AI回复)
|
||||
*/
|
||||
private List<String> extractMeaningfulMessages(List<AgentChatHistoryDTO> chatHistory) {
|
||||
List<String> meaningfulMessages = new ArrayList<>();
|
||||
|
||||
for (AgentChatHistoryDTO message : chatHistory) {
|
||||
// 只处理用户消息(chatType = 1)
|
||||
if (message.getChatType() != null && message.getChatType() == 1) {
|
||||
String content = extractContentFromMessage(message);
|
||||
if (isMeaningfulMessage(content)) {
|
||||
meaningfulMessages.add(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return meaningfulMessages;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从消息中提取内容(处理JSON格式)
|
||||
*/
|
||||
private String extractContentFromMessage(AgentChatHistoryDTO message) {
|
||||
String content = message.getContent();
|
||||
if (StringUtils.isBlank(content)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// 处理JSON格式内容(与前端ChatHistoryDialog.vue逻辑一致)
|
||||
Matcher matcher = JSON_PATTERN.matcher(content);
|
||||
if (matcher.find()) {
|
||||
String jsonContent = matcher.group();
|
||||
// 简化处理:提取JSON中的文本内容
|
||||
return extractTextFromJson(jsonContent);
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从JSON中提取文本内容
|
||||
*/
|
||||
private String extractTextFromJson(String jsonContent) {
|
||||
// 简化处理:提取"content"字段的值
|
||||
Pattern contentPattern = Pattern.compile("\"content\"\s*:\s*\"([^\"]*)\"");
|
||||
Matcher matcher = contentPattern.matcher(jsonContent);
|
||||
if (matcher.find()) {
|
||||
return matcher.group(1);
|
||||
}
|
||||
return jsonContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为有意义的消息
|
||||
*/
|
||||
private boolean isMeaningfulMessage(String content) {
|
||||
if (StringUtils.isBlank(content)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 排除设备控制信息
|
||||
if (DEVICE_CONTROL_PATTERN.matcher(content).find()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 排除日期天气等无关内容
|
||||
if (WEATHER_PATTERN.matcher(content).find() || DATE_PATTERN.matcher(content).find()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 排除过短的消息
|
||||
return content.length() >= 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从消息生成总结
|
||||
*/
|
||||
private String generateSummaryFromMessages(List<String> messages, String agentId) {
|
||||
if (messages.isEmpty()) {
|
||||
return "本次对话内容较少,没有需要总结的重要信息。";
|
||||
}
|
||||
|
||||
// 构建完整的对话内容
|
||||
StringBuilder conversation = new StringBuilder();
|
||||
for (int i = 0; i < messages.size(); i++) {
|
||||
conversation.append("消息").append(i + 1).append(": ").append(messages.get(i)).append("\n");
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取当前智能体的历史记忆
|
||||
String historyMemory = getCurrentAgentMemory(agentId);
|
||||
|
||||
// 调用LLM服务进行智能总结,传递agentId以获取正确的模型配置
|
||||
String summary = callJavaLLMForSummaryWithHistory(conversation.toString(), historyMemory, agentId);
|
||||
|
||||
// 应用总结规则:限制最大长度
|
||||
if (summary.length() > MAX_SUMMARY_LENGTH) {
|
||||
summary = summary.substring(0, MAX_SUMMARY_LENGTH) + "...";
|
||||
}
|
||||
|
||||
return summary;
|
||||
} catch (Exception e) {
|
||||
log.error("调用Java端LLM服务失败: {}", e.getMessage());
|
||||
throw new RuntimeException("LLM服务不可用,无法生成聊天总结");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前智能体的历史记忆
|
||||
*/
|
||||
private String getCurrentAgentMemory(String agentId) {
|
||||
try {
|
||||
if (StringUtils.isBlank(agentId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取智能体信息
|
||||
AgentInfoVO agentInfo = agentService.getAgentById(agentId);
|
||||
if (agentInfo == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 返回智能体的当前总结记忆
|
||||
return agentInfo.getSummaryMemory();
|
||||
} catch (Exception e) {
|
||||
log.error("获取智能体历史记忆失败,agentId: {}, 错误: {}", agentId, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用Java端LLM服务进行智能总结(支持历史记忆合并)
|
||||
*/
|
||||
private String callJavaLLMForSummaryWithHistory(String conversation, String historyMemory, String agentId) {
|
||||
try {
|
||||
// 获取智能体配置,从中提取记忆总结的模型ID
|
||||
String modelId = getMemorySummaryModelId(agentId);
|
||||
|
||||
if (StringUtils.isBlank(modelId)) {
|
||||
log.info("未找到记忆总结的LLM模型配置,使用默认LLM服务");
|
||||
return llmService.generateSummaryWithHistory(conversation, historyMemory, null, null);
|
||||
}
|
||||
|
||||
// 使用指定的模型ID调用LLM服务(支持历史记忆合并)
|
||||
String summary = llmService.generateSummaryWithHistory(conversation, historyMemory, null, modelId);
|
||||
|
||||
if (StringUtils.isNotBlank(summary) && !summary.equals("服务暂不可用") && !summary.equals("总结生成失败")) {
|
||||
return summary;
|
||||
}
|
||||
|
||||
throw new RuntimeException("Java端LLM服务返回异常: " + summary);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("调用Java端LLM服务异常,agentId: {}, 错误: {}", agentId, e.getMessage());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用Java端LLM服务进行智能总结
|
||||
*/
|
||||
private String callJavaLLMForSummary(String conversation, String agentId) {
|
||||
try {
|
||||
// 获取智能体配置,从中提取记忆总结的模型ID
|
||||
String modelId = getMemorySummaryModelId(agentId);
|
||||
|
||||
if (StringUtils.isBlank(modelId)) {
|
||||
log.info("未找到记忆总结的LLM模型配置,使用默认LLM服务");
|
||||
return llmService.generateSummary(conversation);
|
||||
}
|
||||
|
||||
// 使用指定的模型ID调用LLM服务
|
||||
String summary = llmService.generateSummaryWithModel(conversation, modelId);
|
||||
|
||||
if (StringUtils.isNotBlank(summary) && !summary.equals("服务暂不可用") && !summary.equals("总结生成失败")) {
|
||||
return summary;
|
||||
}
|
||||
|
||||
throw new RuntimeException("Java端LLM服务返回异常: " + summary);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("调用Java端LLM服务异常,agentId: {}, 错误: {}", agentId, e.getMessage());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取记忆总结的LLM模型ID
|
||||
*/
|
||||
private String getMemorySummaryModelId(String agentId) {
|
||||
try {
|
||||
if (StringUtils.isBlank(agentId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取智能体信息
|
||||
AgentInfoVO agentInfo = agentService.getAgentById(agentId);
|
||||
if (agentInfo == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取智能体的记忆模型ID
|
||||
String memModelId = agentInfo.getMemModelId();
|
||||
if (StringUtils.isBlank(memModelId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取记忆模型配置
|
||||
ModelConfigEntity memModelConfig = modelConfigService.getModelByIdFromCache(memModelId);
|
||||
if (memModelConfig == null || memModelConfig.getConfigJson() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 从记忆模型配置中提取对应的LLM模型ID
|
||||
Map<String, Object> configMap = memModelConfig.getConfigJson();
|
||||
String llmModelId = (String) configMap.get("llm");
|
||||
|
||||
if (StringUtils.isBlank(llmModelId)) {
|
||||
// 如果记忆模型没有配置独立的LLM,则使用智能体的默认LLM模型
|
||||
return agentInfo.getLlmModelId();
|
||||
}
|
||||
|
||||
return llmModelId;
|
||||
} catch (Exception e) {
|
||||
log.error("获取记忆总结LLM模型ID失败,agentId: {}, 错误: {}", agentId, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据会话ID获取设备信息
|
||||
*/
|
||||
private DeviceEntity getDeviceBySessionId(String sessionId) {
|
||||
try {
|
||||
// 查询该会话的第一条记录获取macAddress
|
||||
QueryWrapper<AgentChatHistoryEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.select("mac_address")
|
||||
.eq("session_id", sessionId)
|
||||
.last("LIMIT 1");
|
||||
|
||||
AgentChatHistoryEntity entity = agentChatHistoryService.getOne(wrapper);
|
||||
if (entity != null && StringUtils.isNotBlank(entity.getMacAddress())) {
|
||||
return deviceService.getDeviceByMacAddress(entity.getMacAddress());
|
||||
}
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
log.error("根据会话ID {} 查找设备信息失败: {}", sessionId, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package xiaozhi.modules.agent.service.impl;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.modules.agent.dao.AgentContextProviderDao;
|
||||
import xiaozhi.modules.agent.entity.AgentContextProviderEntity;
|
||||
import xiaozhi.modules.agent.service.AgentContextProviderService;
|
||||
|
||||
@Service
|
||||
public class AgentContextProviderServiceImpl extends BaseServiceImpl<AgentContextProviderDao, AgentContextProviderEntity> implements AgentContextProviderService {
|
||||
|
||||
@Override
|
||||
public AgentContextProviderEntity getByAgentId(String agentId) {
|
||||
return baseDao.selectOne(new QueryWrapper<AgentContextProviderEntity>().eq("agent_id", agentId));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveOrUpdateByAgentId(AgentContextProviderEntity entity) {
|
||||
AgentContextProviderEntity exist = getByAgentId(entity.getAgentId());
|
||||
if (exist != null) {
|
||||
entity.setId(exist.getId());
|
||||
updateById(entity);
|
||||
} else {
|
||||
insert(entity);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteByAgentId(String agentId) {
|
||||
baseDao.delete(new QueryWrapper<AgentContextProviderEntity>().eq("agent_id", agentId));
|
||||
}
|
||||
}
|
||||
+1
@@ -147,6 +147,7 @@ public class AgentMcpAccessPointServiceImpl implements AgentMcpAccessPointServic
|
||||
List<String> result = toolsList.stream()
|
||||
.map(tool -> (String) tool.get("name"))
|
||||
.filter(name -> name != null)
|
||||
.sorted()
|
||||
.collect(Collectors.toList());
|
||||
log.info("成功获取MCP工具列表,智能体ID: {}, 工具数量: {}", id, result.size());
|
||||
return result;
|
||||
|
||||
+97
-30
@@ -5,6 +5,7 @@ import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -28,18 +29,22 @@ import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.common.user.UserDetail;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.common.utils.JsonUtils;
|
||||
import xiaozhi.common.utils.ToolUtil;
|
||||
import xiaozhi.modules.agent.dao.AgentDao;
|
||||
import xiaozhi.modules.agent.dto.AgentCreateDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
|
||||
import xiaozhi.modules.agent.entity.AgentContextProviderEntity;
|
||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||
import xiaozhi.modules.agent.entity.AgentPluginMapping;
|
||||
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
||||
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||
import xiaozhi.modules.agent.service.AgentContextProviderService;
|
||||
import xiaozhi.modules.agent.service.AgentPluginMappingService;
|
||||
import xiaozhi.modules.agent.service.AgentService;
|
||||
import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||
import xiaozhi.modules.agent.vo.AgentInfoVO;
|
||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||
import xiaozhi.modules.device.service.DeviceService;
|
||||
import xiaozhi.modules.model.dto.ModelProviderDTO;
|
||||
import xiaozhi.modules.model.dto.VoiceDTO;
|
||||
@@ -62,6 +67,7 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
private final AgentChatHistoryService agentChatHistoryService;
|
||||
private final AgentTemplateService agentTemplateService;
|
||||
private final ModelProviderService modelProviderService;
|
||||
private final AgentContextProviderService agentContextProviderService;
|
||||
|
||||
@Override
|
||||
public PageData<AgentEntity> adminAgentList(Map<String, Object> params) {
|
||||
@@ -81,10 +87,17 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
|
||||
if (agent.getMemModelId() != null && agent.getMemModelId().equals(Constant.MEMORY_NO_MEM)) {
|
||||
agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.IGNORE.getCode());
|
||||
if (agent.getChatHistoryConf() == null) {
|
||||
agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.RECORD_TEXT_AUDIO.getCode());
|
||||
}
|
||||
}
|
||||
if (agent.getChatHistoryConf() == null) {
|
||||
agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.RECORD_TEXT_AUDIO.getCode());
|
||||
}
|
||||
|
||||
// 查询上下文源配置
|
||||
AgentContextProviderEntity contextProviderEntity = agentContextProviderService.getByAgentId(id);
|
||||
if (contextProviderEntity != null) {
|
||||
agent.setContextProviders(contextProviderEntity.getContextProviders());
|
||||
}
|
||||
|
||||
// 无需额外查询插件列表,已通过SQL查询出来
|
||||
return agent;
|
||||
}
|
||||
@@ -117,38 +130,70 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AgentDTO> getUserAgents(Long userId) {
|
||||
QueryWrapper<AgentEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("user_id", userId);
|
||||
List<AgentEntity> agents = agentDao.selectList(wrapper);
|
||||
return agents.stream().map(agent -> {
|
||||
AgentDTO dto = new AgentDTO();
|
||||
dto.setId(agent.getId());
|
||||
dto.setAgentName(agent.getAgentName());
|
||||
dto.setSystemPrompt(agent.getSystemPrompt());
|
||||
public List<AgentDTO> getUserAgents(Long userId, String keyword, String searchType) {
|
||||
QueryWrapper<AgentEntity> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("user_id", userId).orderByDesc("created_at");
|
||||
|
||||
// 获取 TTS 模型名称
|
||||
dto.setTtsModelName(modelConfigService.getModelNameById(agent.getTtsModelId()));
|
||||
// 如果有搜索关键词,根据搜索类型添加相应的查询条件
|
||||
if (StringUtils.isNotBlank(keyword)) {
|
||||
if ("mac".equals(searchType)) {
|
||||
// 按MAC地址搜索:先搜索设备,再获取对应的智能体
|
||||
List<DeviceEntity> devices = Optional
|
||||
.ofNullable(deviceService.searchDevicesByMacAddress(keyword, userId)).orElseGet(ArrayList::new);
|
||||
// 获取设备对应的智能体ID列表
|
||||
List<String> agentIds = devices.stream()
|
||||
.map(DeviceEntity::getAgentId)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
if (ToolUtil.isNotEmpty(agentIds)) {
|
||||
queryWrapper.in("id", agentIds);
|
||||
} else {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
} else {
|
||||
// 按名称搜索
|
||||
queryWrapper.like("agent_name", keyword);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取 LLM 模型名称
|
||||
dto.setLlmModelName(modelConfigService.getModelNameById(agent.getLlmModelId()));
|
||||
// 执行查询
|
||||
List<AgentEntity> agentEntities = baseDao.selectList(queryWrapper);
|
||||
|
||||
// 获取 VLLM 模型名称
|
||||
dto.setVllmModelName(modelConfigService.getModelNameById(agent.getVllmModelId()));
|
||||
// 转换为DTO并设置所有必要字段
|
||||
return agentEntities.stream().map(this::buildAgentDTO).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// 获取记忆模型名称
|
||||
dto.setMemModelId(agent.getMemModelId());
|
||||
/**
|
||||
* 将AgentEntity转换为AgentDTO
|
||||
*/
|
||||
private AgentDTO buildAgentDTO(AgentEntity agent) {
|
||||
AgentDTO dto = new AgentDTO();
|
||||
dto.setId(agent.getId());
|
||||
dto.setAgentName(agent.getAgentName());
|
||||
dto.setSystemPrompt(agent.getSystemPrompt());
|
||||
|
||||
// 获取 TTS 音色名称
|
||||
dto.setTtsVoiceName(timbreModelService.getTimbreNameById(agent.getTtsVoiceId()));
|
||||
// 获取 TTS 模型名称
|
||||
dto.setTtsModelName(modelConfigService.getModelNameById(agent.getTtsModelId()));
|
||||
|
||||
// 获取智能体最近的最后连接时长
|
||||
dto.setLastConnectedAt(deviceService.getLatestLastConnectionTime(agent.getId()));
|
||||
// 获取 LLM 模型名称
|
||||
dto.setLlmModelName(modelConfigService.getModelNameById(agent.getLlmModelId()));
|
||||
|
||||
// 获取设备数量
|
||||
dto.setDeviceCount(getDeviceCountByAgentId(agent.getId()));
|
||||
return dto;
|
||||
}).collect(Collectors.toList());
|
||||
// 获取 VLLM 模型名称
|
||||
dto.setVllmModelName(modelConfigService.getModelNameById(agent.getVllmModelId()));
|
||||
|
||||
// 获取记忆模型名称
|
||||
dto.setMemModelId(agent.getMemModelId());
|
||||
|
||||
// 获取 TTS 音色名称
|
||||
dto.setTtsVoiceName(timbreModelService.getTimbreNameById(agent.getTtsVoiceId()));
|
||||
|
||||
// 获取智能体最近的最后连接时长
|
||||
dto.setLastConnectedAt(deviceService.getLatestLastConnectionTime(agent.getId()));
|
||||
|
||||
// 获取设备数量
|
||||
dto.setDeviceCount(getDeviceCountByAgentId(agent.getId()));
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -331,6 +376,14 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
agentChatHistoryService.deleteByAgentId(existingEntity.getId(), true, false);
|
||||
}
|
||||
|
||||
// 更新上下文源配置
|
||||
if (dto.getContextProviders() != null) {
|
||||
AgentContextProviderEntity contextEntity = new AgentContextProviderEntity();
|
||||
contextEntity.setAgentId(agentId);
|
||||
contextEntity.setContextProviders(dto.getContextProviders());
|
||||
agentContextProviderService.saveOrUpdateByAgentId(contextEntity);
|
||||
}
|
||||
|
||||
boolean b = validateLLMIntentParams(dto.getLlmModelId(), dto.getIntentModelId());
|
||||
if (!b) {
|
||||
throw new RenException(ErrorCode.LLM_INTENT_PARAMS_MISMATCH);
|
||||
@@ -395,7 +448,20 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
entity.setIntentModelId(template.getIntentModelId());
|
||||
entity.setSystemPrompt(template.getSystemPrompt());
|
||||
entity.setSummaryMemory(template.getSummaryMemory());
|
||||
entity.setChatHistoryConf(template.getChatHistoryConf());
|
||||
|
||||
// 根据记忆模型类型设置默认的chatHistoryConf值
|
||||
if (template.getMemModelId() != null) {
|
||||
if (template.getMemModelId().equals("Memory_nomem")) {
|
||||
// 无记忆功能的模型,默认不记录聊天记录
|
||||
entity.setChatHistoryConf(0);
|
||||
} else {
|
||||
// 有记忆功能的模型,默认记录文本和语音
|
||||
entity.setChatHistoryConf(2);
|
||||
}
|
||||
} else {
|
||||
entity.setChatHistoryConf(template.getChatHistoryConf());
|
||||
}
|
||||
|
||||
entity.setLangCode(template.getLangCode());
|
||||
entity.setLanguage(template.getLanguage());
|
||||
}
|
||||
@@ -437,4 +503,5 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
agentPluginMappingService.saveBatch(toInsert);
|
||||
return entity.getId();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import xiaozhi.modules.agent.dto.ContextProviderDTO;
|
||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||
import xiaozhi.modules.agent.entity.AgentPluginMapping;
|
||||
|
||||
@@ -21,4 +22,7 @@ public class AgentInfoVO extends AgentEntity
|
||||
@Schema(description = "插件列表Id")
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private List<AgentPluginMapping> functions;
|
||||
|
||||
@Schema(description = "上下文源配置")
|
||||
private List<ContextProviderDTO> contextProviders;
|
||||
}
|
||||
|
||||
+22
-3
@@ -20,10 +20,12 @@ import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.common.utils.JsonUtils;
|
||||
import xiaozhi.modules.agent.dao.AgentVoicePrintDao;
|
||||
import xiaozhi.modules.agent.entity.AgentContextProviderEntity;
|
||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||
import xiaozhi.modules.agent.entity.AgentPluginMapping;
|
||||
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
||||
import xiaozhi.modules.agent.entity.AgentVoicePrintEntity;
|
||||
import xiaozhi.modules.agent.service.AgentContextProviderService;
|
||||
import xiaozhi.modules.agent.service.AgentMcpAccessPointService;
|
||||
import xiaozhi.modules.agent.service.AgentPluginMappingService;
|
||||
import xiaozhi.modules.agent.service.AgentService;
|
||||
@@ -53,6 +55,7 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
private final TimbreService timbreService;
|
||||
private final AgentPluginMappingService agentPluginMappingService;
|
||||
private final AgentMcpAccessPointService agentMcpAccessPointService;
|
||||
private final AgentContextProviderService agentContextProviderService;
|
||||
private final VoiceCloneService cloneVoiceService;
|
||||
private final AgentVoicePrintDao agentVoicePrintDao;
|
||||
|
||||
@@ -103,6 +106,15 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getAgentModels(String macAddress, Map<String, String> selectedModule) {
|
||||
// 检查是否为管理控制台请求
|
||||
String redisKey = RedisKeys.getTmpRegisterMacKey(macAddress);
|
||||
Object isAdminRequest = redisUtils.get(redisKey);
|
||||
|
||||
if (isAdminRequest != null && "true".equals(isAdminRequest)) {
|
||||
// 管理控制台请求,返回getConfig的结果
|
||||
redisUtils.delete(redisKey); // 使用后清理
|
||||
return (Map<String, Object>) getConfig(true);
|
||||
}
|
||||
// 根据MAC地址查找设备
|
||||
DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress);
|
||||
if (device == null) {
|
||||
@@ -151,11 +163,11 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
}
|
||||
result.put("chat_history_conf", chatHistoryConf);
|
||||
// 如果客户端已实例化模型,则不返回
|
||||
String alreadySelectedVadModelId = (String) selectedModule.get("VAD");
|
||||
String alreadySelectedVadModelId = selectedModule.get("VAD");
|
||||
if (alreadySelectedVadModelId != null && alreadySelectedVadModelId.equals(agent.getVadModelId())) {
|
||||
agent.setVadModelId(null);
|
||||
}
|
||||
String alreadySelectedAsrModelId = (String) selectedModule.get("ASR");
|
||||
String alreadySelectedAsrModelId = selectedModule.get("ASR");
|
||||
if (alreadySelectedAsrModelId != null && alreadySelectedAsrModelId.equals(agent.getAsrModelId())) {
|
||||
agent.setAsrModelId(null);
|
||||
}
|
||||
@@ -178,6 +190,13 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
mcpEndpoint = mcpEndpoint.replace("/mcp/", "/call/");
|
||||
result.put("mcp_endpoint", mcpEndpoint);
|
||||
}
|
||||
|
||||
// 获取上下文源配置
|
||||
AgentContextProviderEntity contextProviderEntity = agentContextProviderService.getByAgentId(agent.getId());
|
||||
if (contextProviderEntity != null && contextProviderEntity.getContextProviders() != null && !contextProviderEntity.getContextProviders().isEmpty()) {
|
||||
result.put("context_providers", contextProviderEntity.getContextProviders());
|
||||
}
|
||||
|
||||
// 获取声纹信息
|
||||
buildVoiceprintConfig(agent.getId(), result);
|
||||
|
||||
@@ -283,7 +302,7 @@ public class ConfigServiceImpl implements ConfigService {
|
||||
private void buildVoiceprintConfig(String agentId, Map<String, Object> result) {
|
||||
try {
|
||||
// 获取声纹接口地址
|
||||
String voiceprintUrl = sysParamsService.getValue("server.voice_print", true);
|
||||
String voiceprintUrl = sysParamsService.getValue(Constant.SERVER_VOICE_PRINT, true);
|
||||
if (StringUtils.isBlank(voiceprintUrl) || "null".equals(voiceprintUrl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
+38
-87
@@ -1,14 +1,11 @@
|
||||
package xiaozhi.modules.device.controller;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
@@ -16,9 +13,6 @@ import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@@ -30,6 +24,7 @@ import xiaozhi.common.user.UserDetail;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.modules.device.dto.DeviceManualAddDTO;
|
||||
import xiaozhi.modules.device.dto.DeviceRegisterDTO;
|
||||
import xiaozhi.modules.device.dto.DeviceToolsCallReqDTO;
|
||||
import xiaozhi.modules.device.dto.DeviceUnBindDTO;
|
||||
import xiaozhi.modules.device.dto.DeviceUpdateDTO;
|
||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||
@@ -44,16 +39,11 @@ public class DeviceController {
|
||||
private final DeviceService deviceService;
|
||||
private final RedisUtils redisUtils;
|
||||
private final SysParamsService sysParamsService;
|
||||
private final RestTemplate restTemplate;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public DeviceController(DeviceService deviceService, RedisUtils redisUtils, SysParamsService sysParamsService,
|
||||
RestTemplate restTemplate, ObjectMapper objectMapper) {
|
||||
public DeviceController(DeviceService deviceService, RedisUtils redisUtils, SysParamsService sysParamsService) {
|
||||
this.deviceService = deviceService;
|
||||
this.redisUtils = redisUtils;
|
||||
this.sysParamsService = sysParamsService;
|
||||
this.restTemplate = restTemplate;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@PostMapping("/bind/{agentId}/{deviceCode}")
|
||||
@@ -72,10 +62,12 @@ public class DeviceController {
|
||||
return new Result<String>().error(ErrorCode.MCA_NOT_NULL);
|
||||
}
|
||||
// 生成六位验证码
|
||||
String code = String.valueOf(Math.random()).substring(2, 8);
|
||||
String key = RedisKeys.getDeviceCaptchaKey(code);
|
||||
String code;
|
||||
String key;
|
||||
String existsMac = null;
|
||||
do {
|
||||
code = String.valueOf(Math.random()).substring(2, 8);
|
||||
key = RedisKeys.getDeviceCaptchaKey(code);
|
||||
existsMac = (String) redisUtils.get(key);
|
||||
} while (StringUtils.isNotBlank(existsMac));
|
||||
|
||||
@@ -97,83 +89,12 @@ public class DeviceController {
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<String> forwardToMqttGateway(@PathVariable String agentId, @RequestBody String requestBody) {
|
||||
try {
|
||||
// 从系统参数中获取MQTT网关地址
|
||||
String mqttGatewayUrl = sysParamsService.getValue("server.mqtt_manager_api", true);
|
||||
if (StringUtils.isBlank(mqttGatewayUrl) || "null".equals(mqttGatewayUrl)) {
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
// 获取当前用户的设备列表
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
List<DeviceEntity> devices = deviceService.getUserDevices(user.getId(), agentId);
|
||||
|
||||
// 构建deviceIds数组
|
||||
java.util.List<String> deviceIds = new java.util.ArrayList<>();
|
||||
for (DeviceEntity device : devices) {
|
||||
String macAddress = device.getMacAddress() != null ? device.getMacAddress() : "unknown";
|
||||
String groupId = device.getBoard() != null ? device.getBoard() : "GID_default";
|
||||
|
||||
// 替换冒号为下划线
|
||||
groupId = groupId.replace(":", "_");
|
||||
macAddress = macAddress.replace(":", "_");
|
||||
|
||||
// 构建mqtt客户端ID格式:groupId@@@macAddress@@@macAddress
|
||||
String mqttClientId = groupId + "@@@" + macAddress + "@@@" + macAddress;
|
||||
deviceIds.add(mqttClientId);
|
||||
}
|
||||
|
||||
// 构建完整的URL
|
||||
String url = "http://" + mqttGatewayUrl + "/api/devices/status";
|
||||
|
||||
// 设置请求头
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("Content-Type", "application/json");
|
||||
|
||||
// 生成Bearer令牌
|
||||
String token = generateBearerToken();
|
||||
if (token == null) {
|
||||
return new Result<String>().error("令牌生成失败");
|
||||
}
|
||||
headers.set("Authorization", "Bearer " + token);
|
||||
|
||||
// 构建请求体JSON
|
||||
String jsonBody = "{\"clientIds\":" + objectMapper.writeValueAsString(deviceIds) + "}";
|
||||
HttpEntity<String> requestEntity = new HttpEntity<>(jsonBody, headers);
|
||||
|
||||
// 发送POST请求
|
||||
ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class);
|
||||
|
||||
// 返回响应
|
||||
return new Result<String>().ok(response.getBody());
|
||||
return new Result<String>().ok(deviceService.getDeviceOnlineData(agentId));
|
||||
} catch (Exception e) {
|
||||
return new Result<String>().error("转发请求失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private String generateBearerToken() {
|
||||
try {
|
||||
// 获取当前日期,格式为yyyy-MM-dd
|
||||
String dateStr = java.time.LocalDate.now()
|
||||
.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd"));
|
||||
|
||||
// 获取MQTT签名密钥
|
||||
String signatureKey = sysParamsService.getValue("server.mqtt_signature_key", false);
|
||||
if (StringUtils.isBlank(signatureKey)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 将日期字符串与MQTT_SIGNATURE_KEY连接
|
||||
String tokenContent = dateStr + signatureKey;
|
||||
|
||||
// 对连接后的字符串进行SHA256哈希计算
|
||||
String token = org.apache.commons.codec.digest.DigestUtils.sha256Hex(tokenContent);
|
||||
|
||||
return token;
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/unbind")
|
||||
@Operation(summary = "解绑设备")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
@@ -208,4 +129,34 @@ public class DeviceController {
|
||||
deviceService.manualAddDevice(user.getId(), dto);
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
@PostMapping("/tools/list/{deviceId}")
|
||||
@Operation(summary = "获取设备工具列表")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Object> getDeviceTools(@PathVariable String deviceId) {
|
||||
Object toolsData = deviceService.getDeviceTools(deviceId);
|
||||
if (toolsData == null) {
|
||||
return new Result<Object>().error(ErrorCode.DEVICE_NOT_EXIST);
|
||||
}
|
||||
|
||||
return new Result<Object>().ok(toolsData);
|
||||
}
|
||||
|
||||
@PostMapping("/tools/call/{deviceId}")
|
||||
@Operation(summary = "调用设备工具")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Object> callDeviceTool(@PathVariable String deviceId,
|
||||
@Valid @RequestBody DeviceToolsCallReqDTO request) {
|
||||
String toolName = request.getName();
|
||||
Map<String, Object> arguments = request.getArguments();
|
||||
|
||||
Object result = deviceService.callDeviceTool(deviceId, toolName, arguments);
|
||||
if (result == null) {
|
||||
return new Result<Object>().error(ErrorCode.DEVICE_NOT_EXIST);
|
||||
}
|
||||
|
||||
Result<Object> response = new Result<Object>();
|
||||
response.setMsg("Tools called successfully");
|
||||
return response.ok(result);
|
||||
}
|
||||
}
|
||||
+59
-9
@@ -7,7 +7,9 @@ import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
@@ -36,6 +38,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
@@ -43,8 +46,11 @@ import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.common.validator.ValidatorUtils;
|
||||
import xiaozhi.modules.device.entity.OtaEntity;
|
||||
import xiaozhi.modules.device.service.OtaService;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
import xiaozhi.modules.sys.enums.SuperAdminEnum;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
|
||||
@Tag(name = "设备管理", description = "OTA 相关接口")
|
||||
@Tag(name = "固件升级管理", description = "OTA 相关接口")
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@@ -53,6 +59,7 @@ public class OTAMagController {
|
||||
private static final Logger logger = LoggerFactory.getLogger(OTAController.class);
|
||||
private final OtaService otaService;
|
||||
private final RedisUtils redisUtils;
|
||||
private final SysParamsService sysParamsService;
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "分页查询 OTA 固件信息")
|
||||
@@ -145,15 +152,11 @@ public class OTAMagController {
|
||||
|
||||
// 检查下载次数
|
||||
String downloadCountKey = RedisKeys.getOtaDownloadCountKey(uuid);
|
||||
Integer downloadCount = (Integer) redisUtils.get(downloadCountKey);
|
||||
if (downloadCount == null) {
|
||||
downloadCount = 0;
|
||||
}
|
||||
Integer downloadCount = (Integer) Optional.ofNullable(redisUtils.get(downloadCountKey)).orElse(0);
|
||||
|
||||
// 如果下载次数超过3次,返回404
|
||||
if (downloadCount >= 3) {
|
||||
redisUtils.delete(downloadCountKey);
|
||||
redisUtils.delete(RedisKeys.getOtaIdKey(uuid));
|
||||
redisUtils.delete(List.of(downloadCountKey, RedisKeys.getOtaIdKey(uuid)));
|
||||
logger.warn("Download limit exceeded for UUID: {}", uuid);
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
@@ -162,7 +165,17 @@ public class OTAMagController {
|
||||
|
||||
try {
|
||||
// 获取固件信息
|
||||
OtaEntity otaEntity = otaService.selectById(id);
|
||||
OtaEntity otaEntity = null;
|
||||
if (id.indexOf("file:") == 0) {
|
||||
id = id.substring(5);
|
||||
otaEntity = new OtaEntity();
|
||||
otaEntity.setFirmwarePath(id);
|
||||
otaEntity.setType("assets");
|
||||
otaEntity.setVersion("1.0.0");
|
||||
} else {
|
||||
otaEntity = otaService.selectById(id);
|
||||
}
|
||||
|
||||
if (otaEntity == null || StringUtils.isBlank(otaEntity.getFirmwarePath())) {
|
||||
logger.warn("Firmware not found or path is empty for ID: {}", id);
|
||||
return ResponseEntity.notFound().build();
|
||||
@@ -170,6 +183,7 @@ public class OTAMagController {
|
||||
|
||||
// 获取文件路径 - 确保路径是绝对路径或正确的相对路径
|
||||
String firmwarePath = otaEntity.getFirmwarePath();
|
||||
String originalFilename = otaEntity.getType() + "_" + otaEntity.getVersion();
|
||||
Path path;
|
||||
|
||||
// 检查是否是绝对路径
|
||||
@@ -203,7 +217,7 @@ public class OTAMagController {
|
||||
byte[] fileContent = Files.readAllBytes(path);
|
||||
|
||||
// 设置响应头
|
||||
String originalFilename = otaEntity.getType() + "_" + otaEntity.getVersion();
|
||||
|
||||
if (firmwarePath.contains(".")) {
|
||||
String extension = firmwarePath.substring(firmwarePath.lastIndexOf("."));
|
||||
originalFilename += extension;
|
||||
@@ -279,6 +293,42 @@ public class OTAMagController {
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/uploadAssetsBin")
|
||||
@Operation(summary = "上传资源固件文件")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<String> uploadAssetsBin(@RequestParam("file") MultipartFile file) {
|
||||
String otaUrl = sysParamsService.getValue(Constant.SERVER_OTA, true);
|
||||
if (StringUtils.isBlank(otaUrl) || otaUrl.equals("null")) {
|
||||
return new Result<String>().error(ErrorCode.OTA_URL_EMPTY);
|
||||
}
|
||||
logger.info("username:{},uploadAssetsBin size: {}", SecurityUser.getUser().getUsername(), file.getSize());
|
||||
// 验证文件大小 (资源固件最大20MB)
|
||||
if (file.getSize() > 20 * 1024 * 1024) {
|
||||
return new Result<String>().error(ErrorCode.VOICE_CLONE_AUDIO_TOO_LARGE);
|
||||
}
|
||||
// 普通用户只能每天上传50次
|
||||
if (SecurityUser.getUser().getSuperAdmin() == SuperAdminEnum.NO.value()) {
|
||||
String uploadCountKey = RedisKeys.getOtaUploadCountKey(SecurityUser.getUser().getId());
|
||||
Integer uploadCount = (Integer) Optional.ofNullable(redisUtils.get(uploadCountKey)).orElse(0);
|
||||
if (uploadCount >= 50) {
|
||||
return new Result<String>().error(ErrorCode.OTA_UPLOAD_COUNT_EXCEED);
|
||||
}
|
||||
// 增加上传次数
|
||||
redisUtils.increment(RedisKeys.getOtaUploadCountKey(SecurityUser.getUser().getId()),
|
||||
RedisUtils.DEFAULT_EXPIRE);
|
||||
}
|
||||
Result<String> result = uploadFirmware(file);
|
||||
|
||||
// 生成资源文件路径
|
||||
if (StringUtils.isNotBlank(result.getData())) {
|
||||
String uuid = UUID.randomUUID().toString();
|
||||
redisUtils.set(RedisKeys.getOtaIdKey(uuid), "file:" + result.getData());
|
||||
String downloadUrl = otaUrl.replace("/ota/", "/otaMag/download/") + uuid;
|
||||
result.setData(downloadUrl);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private String calculateMD5(MultipartFile file) throws IOException, NoSuchAlgorithmException {
|
||||
MessageDigest md = MessageDigest.getInstance("MD5");
|
||||
byte[] digest = md.digest(file.getBytes());
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package xiaozhi.modules.device.dto;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class DeviceToolsCallReqDTO {
|
||||
|
||||
@NotBlank(message = "工具名称不能为空")
|
||||
private String name;
|
||||
|
||||
private Map<String, Object> arguments;
|
||||
}
|
||||
@@ -2,17 +2,22 @@ package xiaozhi.modules.device.service;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.BaseService;
|
||||
import xiaozhi.modules.device.dto.DeviceManualAddDTO;
|
||||
import xiaozhi.modules.device.dto.DevicePageUserDTO;
|
||||
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
||||
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
||||
import xiaozhi.modules.device.dto.DeviceManualAddDTO;
|
||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
|
||||
|
||||
public interface DeviceService extends BaseService<DeviceEntity> {
|
||||
/**
|
||||
* 获取设备在线数据
|
||||
*/
|
||||
String getDeviceOnlineData(String agentId);
|
||||
|
||||
/**
|
||||
* 检查设备是否激活
|
||||
@@ -83,6 +88,7 @@ public interface DeviceService extends BaseService<DeviceEntity> {
|
||||
|
||||
/**
|
||||
* 获取这个智能体设备理的最近的最后连接时间
|
||||
*
|
||||
* @param agentId 智能体id
|
||||
* @return 返回设备最近的最后连接时间
|
||||
*/
|
||||
@@ -98,4 +104,33 @@ public interface DeviceService extends BaseService<DeviceEntity> {
|
||||
*/
|
||||
void updateDeviceConnectionInfo(String agentId, String deviceId, String appVersion);
|
||||
|
||||
/**
|
||||
* 生成WebSocket认证token
|
||||
*
|
||||
* @param clientId 客户端ID
|
||||
* @param username 用户名(通常为deviceId)
|
||||
* @return 认证token字符串
|
||||
* @throws Exception 生成token时的异常
|
||||
*/
|
||||
String generateWebSocketToken(String clientId, String username) throws Exception;
|
||||
|
||||
/**
|
||||
* 根据MAC地址搜索设备
|
||||
*
|
||||
* @param macAddress MAC地址关键词
|
||||
* @param userId 用户ID
|
||||
* @return 设备列表
|
||||
*/
|
||||
List<DeviceEntity> searchDevicesByMacAddress(String macAddress, Long userId);
|
||||
|
||||
/**
|
||||
* 获取设备工具列表
|
||||
*/
|
||||
Object getDeviceTools(String deviceId);
|
||||
|
||||
/**
|
||||
* 调用设备工具
|
||||
*/
|
||||
Object callDeviceTool(String deviceId, String toolName, Map<String, Object> arguments);
|
||||
|
||||
}
|
||||
+299
-19
@@ -1,14 +1,19 @@
|
||||
package xiaozhi.modules.device.service.impl;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Instant;
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.TimeZone;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
@@ -24,7 +29,16 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
|
||||
import cn.hutool.core.date.DatePattern;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.digest.DigestUtil;
|
||||
import cn.hutool.http.ContentType;
|
||||
import cn.hutool.http.Header;
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -38,6 +52,7 @@ import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.common.user.UserDetail;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.common.utils.DateUtils;
|
||||
import xiaozhi.common.utils.ToolUtil;
|
||||
import xiaozhi.modules.device.dao.DeviceDao;
|
||||
import xiaozhi.modules.device.dto.DeviceManualAddDTO;
|
||||
import xiaozhi.modules.device.dto.DevicePageUserDTO;
|
||||
@@ -86,16 +101,16 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
if (StringUtils.isBlank(activationCode)) {
|
||||
throw new RenException(ErrorCode.ACTIVATION_CODE_EMPTY);
|
||||
}
|
||||
String deviceKey = "ota:activation:code:" + activationCode;
|
||||
String deviceKey = RedisKeys.getOtaActivationCode(activationCode);
|
||||
Object cacheDeviceId = redisUtils.get(deviceKey);
|
||||
if (cacheDeviceId == null) {
|
||||
if (ToolUtil.isEmpty(cacheDeviceId)) {
|
||||
throw new RenException(ErrorCode.ACTIVATION_CODE_ERROR);
|
||||
}
|
||||
String deviceId = (String) cacheDeviceId;
|
||||
String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
|
||||
String cacheDeviceKey = String.format("ota:activation:data:%s", safeDeviceId);
|
||||
String cacheDeviceKey = RedisKeys.getOtaDeviceActivationInfo(safeDeviceId);
|
||||
Map<String, Object> cacheMap = (Map<String, Object>) redisUtils.get(cacheDeviceKey);
|
||||
if (cacheMap == null) {
|
||||
if (ToolUtil.isEmpty(cacheMap)) {
|
||||
throw new RenException(ErrorCode.ACTIVATION_CODE_ERROR);
|
||||
}
|
||||
String cachedCode = (String) cacheMap.get("activation_code");
|
||||
@@ -131,19 +146,56 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
deviceEntity.setLastConnectedAt(currentTime);
|
||||
deviceDao.insert(deviceEntity);
|
||||
|
||||
// 清理redis缓存
|
||||
redisUtils.delete(cacheDeviceKey);
|
||||
redisUtils.delete(deviceKey);
|
||||
|
||||
// 添加:清除智能体设备数量缓存
|
||||
redisUtils.delete(RedisKeys.getAgentDeviceCountById(agentId));
|
||||
|
||||
// 清理redis缓存、清除智能体设备数量缓存
|
||||
redisUtils.delete(List.of(cacheDeviceKey, deviceKey, RedisKeys.getAgentDeviceCountById(agentId)));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备在线数据
|
||||
*/
|
||||
@Override
|
||||
public DeviceReportRespDTO checkDeviceActive(String macAddress, String clientId,
|
||||
DeviceReportReqDTO deviceReport) {
|
||||
public String getDeviceOnlineData(String agentId) {
|
||||
// 从系统参数中获取MQTT网关地址
|
||||
String mqttGatewayUrl = sysParamsService.getValue("server.mqtt_manager_api", true);
|
||||
if (StringUtils.isBlank(mqttGatewayUrl) || "null".equals(mqttGatewayUrl)) {
|
||||
return "";
|
||||
}
|
||||
// 构建完整的URL
|
||||
String url = StrUtil.format("http://{}/api/devices/status", mqttGatewayUrl);
|
||||
|
||||
// 获取当前用户的设备列表
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
List<DeviceEntity> devices = getUserDevices(user.getId(), agentId);
|
||||
|
||||
// 构建deviceIds数组
|
||||
Set<String> deviceIds = devices.stream().map(o -> {
|
||||
String macAddress = Optional.ofNullable(o.getMacAddress()).orElse("unknown").replace(":", "_");
|
||||
String groupId = Optional.ofNullable(o.getBoard()).orElse("GID_default").replace(":", "_");
|
||||
return StrUtil.format("{}@@@{}@@@{}", groupId, macAddress, macAddress);
|
||||
}).collect(Collectors.toSet());
|
||||
|
||||
// 构建请求入参
|
||||
Map<String, Set<String>> params = MapUtil
|
||||
.builder(new HashMap<String, Set<String>>())
|
||||
.put("clientIds", deviceIds).build();
|
||||
|
||||
if (ToolUtil.isNotEmpty(deviceIds)) {
|
||||
// 发送请求
|
||||
String resultMessage = HttpRequest.post(url)
|
||||
.header(Header.CONTENT_TYPE, ContentType.JSON.getValue())
|
||||
.header(Header.AUTHORIZATION, "Bearer " + generateBearerToken())
|
||||
.body(JSONUtil.toJsonStr(params))
|
||||
.timeout(10000) // 超时,毫秒
|
||||
.execute().body();
|
||||
return resultMessage;
|
||||
}
|
||||
// 返回响应
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceReportRespDTO checkDeviceActive(String macAddress, String clientId, DeviceReportReqDTO deviceReport) {
|
||||
DeviceReportRespDTO response = new DeviceReportRespDTO();
|
||||
response.setServer_time(buildServerTime());
|
||||
|
||||
@@ -169,7 +221,22 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
DeviceReportRespDTO.Websocket websocket = new DeviceReportRespDTO.Websocket();
|
||||
// 从系统参数获取WebSocket URL,如果未配置则使用默认值
|
||||
String wsUrl = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
|
||||
websocket.setToken("");
|
||||
|
||||
// 检查是否启用认证并生成token
|
||||
String authEnabled = sysParamsService.getValue(Constant.SERVER_AUTH_ENABLED, true);
|
||||
if ("true".equalsIgnoreCase(authEnabled)) {
|
||||
try {
|
||||
// 生成token
|
||||
String token = generateWebSocketToken(clientId, macAddress);
|
||||
websocket.setToken(token);
|
||||
} catch (Exception e) {
|
||||
log.error("生成WebSocket token失败: {}", e.getMessage());
|
||||
websocket.setToken("");
|
||||
}
|
||||
} else {
|
||||
websocket.setToken("");
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(wsUrl) || wsUrl.equals("null")) {
|
||||
log.error("WebSocket地址未配置,请登录智控台,在参数管理找到【server.websocket】配置");
|
||||
wsUrl = "ws://xiaozhi.server.com:8000/xiaozhi/v1/";
|
||||
@@ -189,7 +256,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
|
||||
// 添加MQTT UDP配置
|
||||
// 从系统参数获取MQTT Gateway地址,仅在配置有效时使用
|
||||
String mqttUdpConfig = sysParamsService.getValue(Constant.SERVER_MQTT_GATEWAY, false);
|
||||
String mqttUdpConfig = sysParamsService.getValue(Constant.SERVER_MQTT_GATEWAY, true);
|
||||
if (mqttUdpConfig != null && !mqttUdpConfig.equals("null") && !mqttUdpConfig.isEmpty()) {
|
||||
try {
|
||||
String groupId = deviceById != null && deviceById.getBoard() != null ? deviceById.getBoard()
|
||||
@@ -339,8 +406,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
|
||||
private String getDeviceCacheKey(String deviceId) {
|
||||
String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
|
||||
String dataKey = String.format("ota:activation:data:%s", safeDeviceId);
|
||||
return dataKey;
|
||||
return RedisKeys.getOtaDeviceActivationInfo(safeDeviceId);
|
||||
}
|
||||
|
||||
public DeviceReportRespDTO.Activation buildActivation(String deviceId, DeviceReportReqDTO deviceReport) {
|
||||
@@ -379,7 +445,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
redisUtils.set(dataKey, dataMap);
|
||||
|
||||
// 写入反查激活码 key
|
||||
String codeKey = "ota:activation:code:" + newCode;
|
||||
String codeKey = RedisKeys.getOtaActivationCode(newCode);
|
||||
redisUtils.set(codeKey, deviceId);
|
||||
}
|
||||
return code;
|
||||
@@ -479,6 +545,14 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
redisUtils.delete(RedisKeys.getAgentDeviceCountById(dto.getAgentId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DeviceEntity> searchDevicesByMacAddress(String macAddress, Long userId) {
|
||||
QueryWrapper<DeviceEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.like("mac_address", macAddress);
|
||||
wrapper.eq("user_id", userId);
|
||||
return deviceDao.selectList(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成MQTT密码签名
|
||||
*
|
||||
@@ -494,6 +568,40 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
return Base64.getEncoder().encodeToString(signature);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成WebSocket认证token 遵循Python端AuthManager的实现逻辑:token = signature.timestamp
|
||||
*
|
||||
* @param clientId 客户端ID
|
||||
* @param username 用户名 (通常为deviceId/macAddress)
|
||||
* @return 认证token字符串
|
||||
*/
|
||||
public String generateWebSocketToken(String clientId, String username)
|
||||
throws NoSuchAlgorithmException, InvalidKeyException {
|
||||
// 从系统参数获取密钥
|
||||
String secretKey = sysParamsService.getValue(Constant.SERVER_SECRET, false);
|
||||
if (StringUtils.isBlank(secretKey)) {
|
||||
throw new IllegalStateException("WebSocket认证密钥未配置(server.secret)");
|
||||
}
|
||||
|
||||
// 获取当前时间戳(秒)
|
||||
long timestamp = System.currentTimeMillis() / 1000;
|
||||
|
||||
// 构建签名内容: clientId|username|timestamp
|
||||
String content = String.format("%s|%s|%d", clientId, username, timestamp);
|
||||
|
||||
// 生成HMAC-SHA256签名
|
||||
Mac hmac = Mac.getInstance("HmacSHA256");
|
||||
SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
|
||||
hmac.init(keySpec);
|
||||
byte[] signature = hmac.doFinal(content.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// Base64 URL-safe编码签名(去除填充符=)
|
||||
String signatureBase64 = Base64.getUrlEncoder().withoutPadding().encodeToString(signature);
|
||||
|
||||
// 返回格式: signature.timestamp
|
||||
return String.format("%s.%d", signatureBase64, timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建MQTT配置信息
|
||||
*
|
||||
@@ -504,7 +612,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
private DeviceReportRespDTO.MQTT buildMqttConfig(String macAddress, String groupId)
|
||||
throws Exception {
|
||||
// 从环境变量或系统参数获取签名密钥
|
||||
String signatureKey = sysParamsService.getValue("server.mqtt_signature_key", false);
|
||||
String signatureKey = sysParamsService.getValue("server.mqtt_signature_key", true);
|
||||
if (StringUtils.isBlank(signatureKey)) {
|
||||
log.warn("缺少MQTT_SIGNATURE_KEY,跳过MQTT配置生成");
|
||||
return null;
|
||||
@@ -547,4 +655,176 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
|
||||
return mqtt;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成BearerToken
|
||||
*/
|
||||
private String generateBearerToken() {
|
||||
try {
|
||||
String dateStr = DateUtil.format(new Date(), DatePattern.NORM_DATE_PATTERN);
|
||||
String signatureKey = sysParamsService.getValue(Constant.SERVER_MQTT_SECRET, false);
|
||||
if (ToolUtil.isEmpty(signatureKey)) {
|
||||
return null;
|
||||
}
|
||||
return DigestUtil.sha256Hex(dateStr + signatureKey);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getDeviceTools(String deviceId) {
|
||||
// 从系统参数中获取MQTT网关地址
|
||||
String mqttGatewayUrl = sysParamsService.getValue("server.mqtt_manager_api", true);
|
||||
if (StringUtils.isBlank(mqttGatewayUrl) || "null".equals(mqttGatewayUrl)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取设备信息
|
||||
DeviceEntity device = baseDao.selectById(deviceId);
|
||||
if (device == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查设备是否属于当前用户
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
if (!device.getUserId().equals(user.getId())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 构建clientId
|
||||
String macAddress = Optional.ofNullable(device.getMacAddress()).orElse("unknown").replace(":", "_");
|
||||
String groupId = Optional.ofNullable(device.getBoard()).orElse("GID_default").replace(":", "_");
|
||||
String clientId = StrUtil.format("{}@@@{}@@@{}", groupId, macAddress, macAddress);
|
||||
|
||||
// 构建完整的URL
|
||||
String url = StrUtil.format("http://{}/api/commands/{}", mqttGatewayUrl, clientId);
|
||||
|
||||
// 构建请求体
|
||||
Map<String, Object> payload = MapUtil
|
||||
.builder(new HashMap<String, Object>())
|
||||
.put("jsonrpc", "2.0")
|
||||
.put("id", 2)
|
||||
.put("method", "tools/list")
|
||||
.put("params", MapUtil.builder(new HashMap<String, Object>())
|
||||
.put("withUserTools", true)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
Map<String, Object> requestBody = MapUtil
|
||||
.builder(new HashMap<String, Object>())
|
||||
.put("type", "mcp")
|
||||
.put("payload", payload)
|
||||
.build();
|
||||
|
||||
// 发送请求
|
||||
String resultMessage = HttpRequest.post(url)
|
||||
.header(Header.CONTENT_TYPE, ContentType.JSON.getValue())
|
||||
.header(Header.AUTHORIZATION, "Bearer " + generateBearerToken())
|
||||
.body(JSONUtil.toJsonStr(requestBody))
|
||||
.timeout(10000) // 超时,毫秒
|
||||
.execute().body();
|
||||
|
||||
// 解析响应
|
||||
if (StringUtils.isNotBlank(resultMessage)) {
|
||||
cn.hutool.json.JSONObject jsonObject = JSONUtil.parseObj(resultMessage);
|
||||
if (jsonObject.getBool("success", false)) {
|
||||
return jsonObject.get("data");
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object callDeviceTool(String deviceId, String toolName, Map<String, Object> arguments) {
|
||||
// 从系统参数中获取MQTT网关地址
|
||||
String mqttGatewayUrl = sysParamsService.getValue("server.mqtt_manager_api", true);
|
||||
if (StringUtils.isBlank(mqttGatewayUrl) || "null".equals(mqttGatewayUrl)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取设备信息
|
||||
DeviceEntity device = baseDao.selectById(deviceId);
|
||||
if (device == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查设备是否属于当前用户
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
if (!device.getUserId().equals(user.getId())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 构建clientId
|
||||
String macAddress = Optional.ofNullable(device.getMacAddress()).orElse("unknown").replace(":", "_");
|
||||
String groupId = Optional.ofNullable(device.getBoard()).orElse("GID_default").replace(":", "_");
|
||||
String clientId = StrUtil.format("{}@@@{}@@@{}", groupId, macAddress, macAddress);
|
||||
|
||||
// 构建完整的URL
|
||||
String url = StrUtil.format("http://{}/api/commands/{}", mqttGatewayUrl, clientId);
|
||||
|
||||
// 构建请求体
|
||||
Map<String, Object> params = MapUtil
|
||||
.builder(new HashMap<String, Object>())
|
||||
.put("name", toolName)
|
||||
.put("arguments", arguments)
|
||||
.build();
|
||||
|
||||
Map<String, Object> payload = MapUtil
|
||||
.builder(new HashMap<String, Object>())
|
||||
.put("jsonrpc", "2.0")
|
||||
.put("id", 2)
|
||||
.put("method", "tools/call")
|
||||
.put("params", params)
|
||||
.build();
|
||||
|
||||
Map<String, Object> requestBody = MapUtil
|
||||
.builder(new HashMap<String, Object>())
|
||||
.put("type", "mcp")
|
||||
.put("payload", payload)
|
||||
.build();
|
||||
|
||||
// 发送请求
|
||||
String resultMessage = HttpRequest.post(url)
|
||||
.header(Header.CONTENT_TYPE, ContentType.JSON.getValue())
|
||||
.header(Header.AUTHORIZATION, "Bearer " + generateBearerToken())
|
||||
.body(JSONUtil.toJsonStr(requestBody))
|
||||
.timeout(10000) // 超时,毫秒
|
||||
.execute().body();
|
||||
|
||||
// 解析响应
|
||||
if (StringUtils.isNotBlank(resultMessage)) {
|
||||
cn.hutool.json.JSONObject jsonObject = JSONUtil.parseObj(resultMessage);
|
||||
if (jsonObject.getBool("success", false)) {
|
||||
cn.hutool.json.JSONObject data = jsonObject.getJSONObject("data");
|
||||
if (data != null) {
|
||||
cn.hutool.json.JSONArray content = data.getJSONArray("content");
|
||||
if (content != null && content.size() > 0) {
|
||||
cn.hutool.json.JSONObject firstContent = content.getJSONObject(0);
|
||||
if (firstContent != null && "text".equals(firstContent.getStr("type"))) {
|
||||
String text = firstContent.getStr("text");
|
||||
if (StringUtils.isNotBlank(text)) {
|
||||
String trimmedText = text.trim();
|
||||
if (trimmedText.startsWith("{") || trimmedText.startsWith("[")) {
|
||||
try {
|
||||
return JSONUtil.parseObj(trimmedText);
|
||||
} catch (Exception e) {
|
||||
return trimmedText;
|
||||
}
|
||||
} else if ("true".equals(trimmedText)) {
|
||||
return true;
|
||||
} else if ("false".equals(trimmedText)) {
|
||||
return false;
|
||||
} else {
|
||||
return trimmedText;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package xiaozhi.modules.knowledge.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import xiaozhi.modules.knowledge.rag.KnowledgeBaseAdapterFactory;
|
||||
|
||||
/**
|
||||
* 知识库配置类
|
||||
* 配置知识库相关的Bean
|
||||
*/
|
||||
@Configuration
|
||||
public class KnowledgeBaseConfig {
|
||||
|
||||
/**
|
||||
* 提供KnowledgeBaseAdapterFactory的Bean实例
|
||||
* @return KnowledgeBaseAdapterFactory实例
|
||||
*/
|
||||
@Bean
|
||||
public KnowledgeBaseAdapterFactory knowledgeBaseAdapterFactory() {
|
||||
return new KnowledgeBaseAdapterFactory();
|
||||
}
|
||||
}
|
||||
+14
-16
@@ -1,7 +1,6 @@
|
||||
package xiaozhi.modules.knowledge.controller;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
@@ -24,8 +23,10 @@ import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.common.utils.ToolUtil;
|
||||
import xiaozhi.modules.knowledge.dto.KnowledgeBaseDTO;
|
||||
import xiaozhi.modules.knowledge.service.KnowledgeBaseService;
|
||||
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
|
||||
@AllArgsConstructor
|
||||
@@ -131,20 +132,17 @@ public class KnowledgeBaseController {
|
||||
|
||||
// 获取当前登录用户ID
|
||||
Long currentUserId = SecurityUser.getUserId();
|
||||
String[] idArray = ids.split(",");
|
||||
for (String datasetId : idArray) {
|
||||
if (StringUtils.isNotBlank(datasetId)) {
|
||||
// 先获取现有知识库信息以检查权限
|
||||
KnowledgeBaseDTO existingKnowledgeBase = knowledgeBaseService.getByDatasetId(datasetId.trim());
|
||||
|
||||
List<String> idList = Arrays.asList(ids.split(","));
|
||||
List<KnowledgeBaseDTO> knowledgeBaseDTOs = Optional.ofNullable(knowledgeBaseService.getByDatasetIdList(idList)).orElseGet(ArrayList::new);
|
||||
if (ToolUtil.isNotEmpty(knowledgeBaseDTOs)) {
|
||||
knowledgeBaseDTOs.forEach(item->{
|
||||
// 检查权限:用户只能删除自己创建的知识库
|
||||
if (existingKnowledgeBase.getCreator() == null
|
||||
|| !existingKnowledgeBase.getCreator().equals(currentUserId)) {
|
||||
if (item.getCreator() == null || !item.getCreator().equals(currentUserId)) {
|
||||
throw new RenException(ErrorCode.NO_PERMISSION);
|
||||
}
|
||||
|
||||
knowledgeBaseService.deleteByDatasetId(datasetId.trim());
|
||||
}
|
||||
//删除
|
||||
knowledgeBaseService.deleteByDatasetId(item.getDatasetId());
|
||||
});
|
||||
}
|
||||
return new Result<>();
|
||||
}
|
||||
@@ -152,8 +150,8 @@ public class KnowledgeBaseController {
|
||||
@GetMapping("/rag-models")
|
||||
@Operation(summary = "获取RAG模型列表")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<List<Map<String, Object>>> getRAGModels() {
|
||||
List<Map<String, Object>> result = knowledgeBaseService.getRAGModels();
|
||||
return new Result<List<Map<String, Object>>>().ok(result);
|
||||
public Result<List<ModelConfigEntity>> getRAGModels() {
|
||||
List<ModelConfigEntity> result = knowledgeBaseService.getRAGModels();
|
||||
return new Result<List<ModelConfigEntity>>().ok(result);
|
||||
}
|
||||
}
|
||||
+8
-11
@@ -63,16 +63,11 @@ public class KnowledgeFilesController {
|
||||
// 验证知识库权限
|
||||
validateKnowledgeBasePermission(datasetId);
|
||||
|
||||
// 如果指定了状态参数,使用状态查询接口
|
||||
if (status != null) {
|
||||
PageData<KnowledgeFilesDTO> pageData = knowledgeFilesService.getPageListByStatus(datasetId, status, page, page_size);
|
||||
return new Result<PageData<KnowledgeFilesDTO>>().ok(pageData);
|
||||
}
|
||||
|
||||
// 否则使用通用查询接口
|
||||
//组装参数
|
||||
KnowledgeFilesDTO knowledgeFilesDTO = new KnowledgeFilesDTO();
|
||||
knowledgeFilesDTO.setDatasetId(datasetId);
|
||||
knowledgeFilesDTO.setName(name);
|
||||
knowledgeFilesDTO.setStatus(status);
|
||||
PageData<KnowledgeFilesDTO> pageData = knowledgeFilesService.getPageList(knowledgeFilesDTO, page, page_size);
|
||||
return new Result<PageData<KnowledgeFilesDTO>>().ok(pageData);
|
||||
}
|
||||
@@ -87,8 +82,11 @@ public class KnowledgeFilesController {
|
||||
@RequestParam(required = false, defaultValue = "10") Integer page_size) {
|
||||
// 验证知识库权限
|
||||
validateKnowledgeBasePermission(datasetId);
|
||||
|
||||
PageData<KnowledgeFilesDTO> pageData = knowledgeFilesService.getPageListByStatus(datasetId, status, page, page_size);
|
||||
//组装参数
|
||||
KnowledgeFilesDTO knowledgeFilesDTO = new KnowledgeFilesDTO();
|
||||
knowledgeFilesDTO.setDatasetId(datasetId);
|
||||
knowledgeFilesDTO.setStatus(status);
|
||||
PageData<KnowledgeFilesDTO> pageData = knowledgeFilesService.getPageList(knowledgeFilesDTO, page, page_size);
|
||||
return new Result<PageData<KnowledgeFilesDTO>>().ok(pageData);
|
||||
}
|
||||
|
||||
@@ -159,8 +157,7 @@ public class KnowledgeFilesController {
|
||||
// 验证知识库权限
|
||||
validateKnowledgeBasePermission(datasetId);
|
||||
|
||||
Map<String, Object> result = knowledgeFilesService.listChunks(datasetId, documentId, keywords, page, page_size,
|
||||
id);
|
||||
Map<String, Object> result = knowledgeFilesService.listChunks(datasetId, documentId, keywords, page, page_size, id);
|
||||
return new Result<Map<String, Object>>().ok(result);
|
||||
}
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ public class KnowledgeFilesDTO implements Serializable {
|
||||
return STATUS_UNSTART;
|
||||
}
|
||||
|
||||
// 根据run字段的值直接映射到对应的状态码
|
||||
// RAGFlow根据run字段的值直接映射到对应的状态码
|
||||
switch (run.toUpperCase()) {
|
||||
case "RUNNING":
|
||||
return STATUS_RUNNING;
|
||||
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
package xiaozhi.modules.knowledge.rag;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.modules.knowledge.dto.KnowledgeFilesDTO;
|
||||
|
||||
/**
|
||||
* 知识库API适配器抽象基类
|
||||
* 定义通用的知识库操作接口,支持多种后端API实现
|
||||
*/
|
||||
public abstract class KnowledgeBaseAdapter {
|
||||
|
||||
/**
|
||||
* 获取适配器类型标识
|
||||
*
|
||||
* @return 适配器类型(如:ragflow, milvus, pinecone等)
|
||||
*/
|
||||
public abstract String getAdapterType();
|
||||
|
||||
/**
|
||||
* 初始化适配器配置
|
||||
*
|
||||
* @param config 配置参数
|
||||
*/
|
||||
public abstract void initialize(Map<String, Object> config);
|
||||
|
||||
/**
|
||||
* 验证配置是否有效
|
||||
*
|
||||
* @param config 配置参数
|
||||
* @return 验证结果
|
||||
*/
|
||||
public abstract boolean validateConfig(Map<String, Object> config);
|
||||
|
||||
/**
|
||||
* 分页查询文档列表
|
||||
*
|
||||
* @param datasetId 知识库ID
|
||||
* @param queryParams 查询参数
|
||||
* @param page 页码
|
||||
* @param limit 每页数量
|
||||
* @return 分页数据
|
||||
*/
|
||||
public abstract PageData<KnowledgeFilesDTO> getDocumentList(String datasetId,
|
||||
Map<String, Object> queryParams,
|
||||
Integer page,
|
||||
Integer limit);
|
||||
|
||||
/**
|
||||
* 根据文档ID获取文档详情
|
||||
*
|
||||
* @param datasetId 知识库ID
|
||||
* @return 文档详情
|
||||
*/
|
||||
public abstract KnowledgeFilesDTO getDocumentById(String datasetId, String documentId);
|
||||
|
||||
/**
|
||||
* 上传文档到知识库
|
||||
*
|
||||
* @param datasetId 知识库ID
|
||||
* @param file 上传的文件
|
||||
* @param name 文档名称
|
||||
* @param metaFields 元数据字段
|
||||
* @param chunkMethod 分块方法
|
||||
* @param parserConfig 解析器配置
|
||||
* @return 上传的文档信息
|
||||
*/
|
||||
public abstract KnowledgeFilesDTO uploadDocument(String datasetId,
|
||||
MultipartFile file,
|
||||
String name,
|
||||
Map<String, Object> metaFields,
|
||||
String chunkMethod,
|
||||
Map<String, Object> parserConfig);
|
||||
|
||||
/**
|
||||
* 根据状态分页查询文档列表
|
||||
*
|
||||
* @param datasetId 知识库ID
|
||||
* @param status 文档解析状态
|
||||
* @param page 页码
|
||||
* @param limit 每页数量
|
||||
* @return 分页数据
|
||||
*/
|
||||
public abstract PageData<KnowledgeFilesDTO> getDocumentListByStatus(String datasetId,
|
||||
Integer status,
|
||||
Integer page,
|
||||
Integer limit);
|
||||
|
||||
/**
|
||||
* 删除文档
|
||||
*
|
||||
* @param datasetId 知识库ID
|
||||
* @param documentId 文档ID
|
||||
*/
|
||||
public abstract void deleteDocument(String datasetId, String documentId);
|
||||
|
||||
/**
|
||||
* 解析文档(切块)
|
||||
*
|
||||
* @param datasetId 知识库ID
|
||||
* @param documentIds 文档ID列表
|
||||
* @return 解析结果
|
||||
*/
|
||||
public abstract boolean parseDocuments(String datasetId, List<String> documentIds);
|
||||
|
||||
/**
|
||||
* 列出指定文档的切片
|
||||
*
|
||||
* @param datasetId 知识库ID
|
||||
* @param documentId 文档ID
|
||||
* @param keywords 关键词过滤
|
||||
* @param page 页码
|
||||
* @param pageSize 每页数量
|
||||
* @param chunkId 切片ID
|
||||
* @return 切片列表信息
|
||||
*/
|
||||
public abstract Map<String, Object> listChunks(String datasetId,
|
||||
String documentId,
|
||||
String keywords,
|
||||
Integer page,
|
||||
Integer pageSize,
|
||||
String chunkId);
|
||||
|
||||
/**
|
||||
* 召回测试 - 从知识库中检索相关切片
|
||||
*
|
||||
* @param question 用户查询
|
||||
* @param datasetIds 数据集ID列表
|
||||
* @param documentIds 文档ID列表
|
||||
* @param retrievalParams 检索参数
|
||||
* @return 召回测试结果
|
||||
*/
|
||||
public abstract Map<String, Object> retrievalTest(String question,
|
||||
List<String> datasetIds,
|
||||
List<String> documentIds,
|
||||
Map<String, Object> retrievalParams);
|
||||
|
||||
/**
|
||||
* 测试连接
|
||||
*
|
||||
* @return 连接测试结果
|
||||
*/
|
||||
public abstract boolean testConnection();
|
||||
|
||||
/**
|
||||
* 获取适配器状态信息
|
||||
*
|
||||
* @return 状态信息
|
||||
*/
|
||||
public abstract Map<String, Object> getStatus();
|
||||
|
||||
/**
|
||||
* 获取支持的配置参数
|
||||
*
|
||||
* @return 配置参数说明
|
||||
*/
|
||||
public abstract Map<String, Object> getSupportedConfig();
|
||||
|
||||
/**
|
||||
* 获取默认配置
|
||||
*
|
||||
* @return 默认配置
|
||||
*/
|
||||
public abstract Map<String, Object> getDefaultConfig();
|
||||
|
||||
/**
|
||||
* 创建数据集
|
||||
*
|
||||
* @param createParams 创建参数
|
||||
* @return 数据集ID
|
||||
*/
|
||||
public abstract String createDataset(Map<String, Object> createParams);
|
||||
|
||||
/**
|
||||
* 更新数据集
|
||||
*
|
||||
* @param datasetId 数据集ID
|
||||
* @param updateParams 更新参数
|
||||
*/
|
||||
public abstract void updateDataset(String datasetId, Map<String, Object> updateParams);
|
||||
|
||||
/**
|
||||
* 删除数据集
|
||||
*
|
||||
* @param datasetId 数据集ID
|
||||
*/
|
||||
public abstract void deleteDataset(String datasetId);
|
||||
|
||||
/**
|
||||
* 获取数据集的文档数量
|
||||
*
|
||||
* @param datasetId 数据集ID
|
||||
* @return 文档数量
|
||||
*/
|
||||
public abstract Integer getDocumentCount(String datasetId);
|
||||
}
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
package xiaozhi.modules.knowledge.rag;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
|
||||
/**
|
||||
* 知识库适配器工厂类
|
||||
* 负责创建和管理不同类型的知识库API适配器
|
||||
*/
|
||||
@Slf4j
|
||||
public class KnowledgeBaseAdapterFactory {
|
||||
|
||||
// 注册的适配器类型映射
|
||||
private static final Map<String, Class<? extends KnowledgeBaseAdapter>> adapterRegistry = new HashMap<>();
|
||||
|
||||
// 适配器实例缓存
|
||||
private static final Map<String, KnowledgeBaseAdapter> adapterCache = new ConcurrentHashMap<>();
|
||||
|
||||
static {
|
||||
// 注册内置适配器类型
|
||||
registerAdapter("ragflow", xiaozhi.modules.knowledge.rag.impl.RAGFlowAdapter.class);
|
||||
// 可以在这里注册更多适配器类型
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册新的适配器类型
|
||||
*
|
||||
* @param adapterType 适配器类型标识
|
||||
* @param adapterClass 适配器类
|
||||
*/
|
||||
public static void registerAdapter(String adapterType, Class<? extends KnowledgeBaseAdapter> adapterClass) {
|
||||
if (adapterRegistry.containsKey(adapterType)) {
|
||||
log.warn("适配器类型 '{}' 已存在,将被覆盖", adapterType);
|
||||
}
|
||||
adapterRegistry.put(adapterType, adapterClass);
|
||||
log.info("注册适配器类型: {} -> {}", adapterType, adapterClass.getSimpleName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取适配器实例
|
||||
*
|
||||
* @param adapterType 适配器类型
|
||||
* @param config 配置参数
|
||||
* @return 适配器实例
|
||||
*/
|
||||
public static KnowledgeBaseAdapter getAdapter(String adapterType, Map<String, Object> config) {
|
||||
String cacheKey = buildCacheKey(adapterType, config);
|
||||
|
||||
// 检查缓存中是否已存在实例
|
||||
if (adapterCache.containsKey(cacheKey)) {
|
||||
log.debug("从缓存获取适配器实例: {}", cacheKey);
|
||||
return adapterCache.get(cacheKey);
|
||||
}
|
||||
|
||||
// 创建新的适配器实例
|
||||
KnowledgeBaseAdapter adapter = createAdapter(adapterType, config);
|
||||
|
||||
// 缓存适配器实例
|
||||
adapterCache.put(cacheKey, adapter);
|
||||
log.info("创建并缓存适配器实例: {}", cacheKey);
|
||||
|
||||
return adapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取适配器实例(无配置)
|
||||
*
|
||||
* @param adapterType 适配器类型
|
||||
* @return 适配器实例
|
||||
*/
|
||||
public static KnowledgeBaseAdapter getAdapter(String adapterType) {
|
||||
return getAdapter(adapterType, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有已注册的适配器类型
|
||||
*
|
||||
* @return 适配器类型集合
|
||||
*/
|
||||
public static Set<String> getRegisteredAdapterTypes() {
|
||||
return adapterRegistry.keySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查适配器类型是否已注册
|
||||
*
|
||||
* @param adapterType 适配器类型
|
||||
* @return 是否已注册
|
||||
*/
|
||||
public static boolean isAdapterTypeRegistered(String adapterType) {
|
||||
return adapterRegistry.containsKey(adapterType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除适配器缓存
|
||||
*/
|
||||
public static void clearCache() {
|
||||
int cacheSize = adapterCache.size();
|
||||
adapterCache.clear();
|
||||
log.info("清除适配器缓存,共清除 {} 个实例", cacheSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除特定适配器类型的缓存
|
||||
*
|
||||
* @param adapterType 适配器类型
|
||||
*/
|
||||
public static void removeCacheByType(String adapterType) {
|
||||
int removedCount = 0;
|
||||
for (String cacheKey : adapterCache.keySet()) {
|
||||
if (cacheKey.startsWith(adapterType + "@")) {
|
||||
adapterCache.remove(cacheKey);
|
||||
removedCount++;
|
||||
}
|
||||
}
|
||||
log.info("移除适配器类型 '{}' 的缓存,共移除 {} 个实例", adapterType, removedCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取适配器工厂状态信息
|
||||
*
|
||||
* @return 状态信息
|
||||
*/
|
||||
public static Map<String, Object> getFactoryStatus() {
|
||||
Map<String, Object> status = new HashMap<>();
|
||||
status.put("registeredAdapterTypes", adapterRegistry.keySet());
|
||||
status.put("cachedAdapterCount", adapterCache.size());
|
||||
status.put("cacheKeys", adapterCache.keySet());
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建适配器实例
|
||||
*
|
||||
* @param adapterType 适配器类型
|
||||
* @param config 配置参数
|
||||
* @return 适配器实例
|
||||
*/
|
||||
private static KnowledgeBaseAdapter createAdapter(String adapterType, Map<String, Object> config) {
|
||||
if (!adapterRegistry.containsKey(adapterType)) {
|
||||
throw new RenException(ErrorCode.RAG_ADAPTER_TYPE_NOT_SUPPORTED,
|
||||
"不支持的适配器类型: " + adapterType);
|
||||
}
|
||||
|
||||
try {
|
||||
Class<? extends KnowledgeBaseAdapter> adapterClass = adapterRegistry.get(adapterType);
|
||||
KnowledgeBaseAdapter adapter = adapterClass.getDeclaredConstructor().newInstance();
|
||||
|
||||
// 初始化适配器
|
||||
if (config != null) {
|
||||
adapter.initialize(config);
|
||||
|
||||
// 验证配置
|
||||
if (!adapter.validateConfig(config)) {
|
||||
throw new RenException(ErrorCode.RAG_CONFIG_VALIDATION_FAILED,
|
||||
"适配器配置验证失败: " + adapterType);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("成功创建适配器实例: {}", adapterType);
|
||||
return adapter;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("创建适配器实例失败: {}", adapterType, e);
|
||||
throw new RenException(ErrorCode.RAG_ADAPTER_CREATION_FAILED,
|
||||
"创建适配器失败: " + adapterType + ", 错误: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建缓存键
|
||||
*
|
||||
* @param adapterType 适配器类型
|
||||
* @param config 配置参数
|
||||
* @return 缓存键
|
||||
*/
|
||||
private static String buildCacheKey(String adapterType, Map<String, Object> config) {
|
||||
if (config == null || config.isEmpty()) {
|
||||
return adapterType + "@default";
|
||||
}
|
||||
|
||||
// 基于配置参数生成缓存键
|
||||
StringBuilder keyBuilder = new StringBuilder(adapterType + "@");
|
||||
|
||||
// 使用配置的哈希值作为缓存键的一部分
|
||||
int configHash = config.hashCode();
|
||||
keyBuilder.append(configHash);
|
||||
|
||||
return keyBuilder.toString();
|
||||
}
|
||||
}
|
||||
+1155
File diff suppressed because it is too large
Load Diff
+10
-1
@@ -7,6 +7,7 @@ import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.BaseService;
|
||||
import xiaozhi.modules.knowledge.dto.KnowledgeBaseDTO;
|
||||
import xiaozhi.modules.knowledge.entity.KnowledgeBaseEntity;
|
||||
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
||||
|
||||
/**
|
||||
* 知识库知识库服务接口
|
||||
@@ -55,6 +56,14 @@ public interface KnowledgeBaseService extends BaseService<KnowledgeBaseEntity> {
|
||||
*/
|
||||
KnowledgeBaseDTO getByDatasetId(String datasetId);
|
||||
|
||||
/**
|
||||
* 根据知识库ID集合查询知识库
|
||||
*
|
||||
* @param datasetIdList 知识库ID集合
|
||||
* @return 知识库详情
|
||||
*/
|
||||
List<KnowledgeBaseDTO> getByDatasetIdList(List<String> datasetIdList);
|
||||
|
||||
/**
|
||||
* 根据知识库ID删除知识库
|
||||
*
|
||||
@@ -83,5 +92,5 @@ public interface KnowledgeBaseService extends BaseService<KnowledgeBaseEntity> {
|
||||
*
|
||||
* @return RAG模型列表
|
||||
*/
|
||||
List<Map<String, Object>> getRAGModels();
|
||||
List<ModelConfigEntity> getRAGModels();
|
||||
}
|
||||
-11
@@ -47,17 +47,6 @@ public interface KnowledgeFilesService {
|
||||
Map<String, Object> metaFields, String chunkMethod,
|
||||
Map<String, Object> parserConfig);
|
||||
|
||||
/**
|
||||
* 根据状态分页查询文档列表
|
||||
*
|
||||
* @param datasetId 知识库ID
|
||||
* @param status 文档解析状态(0-未开始,1-进行中,2-已取消,3-已完成,4-失败)
|
||||
* @param page 页码
|
||||
* @param limit 每页数量
|
||||
* @return 分页数据
|
||||
*/
|
||||
PageData<KnowledgeFilesDTO> getPageListByStatus(String datasetId, Integer status, Integer page, Integer limit);
|
||||
|
||||
/**
|
||||
* 根据文档ID和知识库ID删除文档
|
||||
*
|
||||
|
||||
+164
-365
@@ -1,21 +1,12 @@
|
||||
package xiaozhi.modules.knowledge.service.impl;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
@@ -32,9 +23,12 @@ import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.common.utils.MessageUtils;
|
||||
import xiaozhi.common.utils.ToolUtil;
|
||||
import xiaozhi.modules.knowledge.dao.KnowledgeBaseDao;
|
||||
import xiaozhi.modules.knowledge.dto.KnowledgeBaseDTO;
|
||||
import xiaozhi.modules.knowledge.entity.KnowledgeBaseEntity;
|
||||
import xiaozhi.modules.knowledge.rag.KnowledgeBaseAdapter;
|
||||
import xiaozhi.modules.knowledge.rag.KnowledgeBaseAdapterFactory;
|
||||
import xiaozhi.modules.knowledge.service.KnowledgeBaseService;
|
||||
import xiaozhi.modules.model.dao.ModelConfigDao;
|
||||
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
||||
@@ -51,8 +45,6 @@ public class KnowledgeBaseServiceImpl extends BaseServiceImpl<KnowledgeBaseDao,
|
||||
private final ModelConfigService modelConfigService;
|
||||
private final ModelConfigDao modelConfigDao;
|
||||
private final RedisUtils redisUtils;
|
||||
private RestTemplate restTemplate = new RestTemplate();
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Override
|
||||
public KnowledgeBaseEntity selectById(Serializable datasetId) {
|
||||
@@ -106,7 +98,7 @@ public class KnowledgeBaseServiceImpl extends BaseServiceImpl<KnowledgeBaseDao,
|
||||
if (pageData != null && pageData.getList() != null) {
|
||||
for (KnowledgeBaseDTO knowledgeBase : pageData.getList()) {
|
||||
try {
|
||||
Integer documentCount = getDocumentCountFromRAGFlow(knowledgeBase.getDatasetId(),
|
||||
Integer documentCount = getDocumentCountFromRAG(knowledgeBase.getDatasetId(),
|
||||
knowledgeBase.getRagModelId());
|
||||
knowledgeBase.setDocumentCount(documentCount);
|
||||
} catch (Exception e) {
|
||||
@@ -146,10 +138,10 @@ public class KnowledgeBaseServiceImpl extends BaseServiceImpl<KnowledgeBaseDao,
|
||||
checkDuplicateKnowledgeBaseName(knowledgeBaseDTO, null);
|
||||
|
||||
String datasetId = null;
|
||||
// 调用RAGFlow API创建数据集
|
||||
// 调用RAG API创建数据集
|
||||
try {
|
||||
Map<String, Object> ragConfig = getValidatedRAGConfig(knowledgeBaseDTO.getRagModelId());
|
||||
datasetId = createDatasetInRAGFlow(
|
||||
datasetId = createDatasetInRAG(
|
||||
knowledgeBaseDTO.getName(),
|
||||
knowledgeBaseDTO.getDescription(),
|
||||
ragConfig);
|
||||
@@ -162,13 +154,13 @@ public class KnowledgeBaseServiceImpl extends BaseServiceImpl<KnowledgeBaseDao,
|
||||
KnowledgeBaseEntity existingEntity = knowledgeBaseDao.selectOne(
|
||||
new QueryWrapper<KnowledgeBaseEntity>().eq("dataset_id", datasetId));
|
||||
if (existingEntity != null) {
|
||||
// 如果datasetId已存在,删除RAGFlow中的数据集并抛出异常
|
||||
// 如果datasetId已存在,删除RAG中的数据集并抛出异常
|
||||
try {
|
||||
Map<String, Object> ragConfig = getValidatedRAGConfig(knowledgeBaseDTO.getRagModelId());
|
||||
deleteDatasetInRAGFlow(datasetId, ragConfig);
|
||||
deleteDatasetInRAG(datasetId, ragConfig);
|
||||
} catch (Exception deleteException) {
|
||||
// 提供更详细的错误信息,包括异常类型和消息
|
||||
String errorMessage = "删除重复datasetId的RAGFlow数据集失败: " + deleteException.getClass().getSimpleName();
|
||||
String errorMessage = "删除重复datasetId的RAG数据集失败: " + deleteException.getClass().getSimpleName();
|
||||
if (deleteException.getMessage() != null) {
|
||||
errorMessage += " - " + deleteException.getMessage();
|
||||
}
|
||||
@@ -219,17 +211,17 @@ public class KnowledgeBaseServiceImpl extends BaseServiceImpl<KnowledgeBaseDao,
|
||||
// 先校验RAG配置
|
||||
Map<String, Object> ragConfig = getValidatedRAGConfig(knowledgeBaseDTO.getRagModelId());
|
||||
|
||||
// 调用RAGFlow API更新数据集
|
||||
updateDatasetInRAGFlow(
|
||||
// 调用RAG API更新数据集
|
||||
updateDatasetInRAG(
|
||||
knowledgeBaseDTO.getDatasetId(),
|
||||
knowledgeBaseDTO.getName(),
|
||||
knowledgeBaseDTO.getDescription(),
|
||||
ragConfig);
|
||||
|
||||
log.info("RAGFlow API更新成功,datasetId: {}", knowledgeBaseDTO.getDatasetId());
|
||||
log.info("RAG API更新成功,datasetId: {}", knowledgeBaseDTO.getDatasetId());
|
||||
} catch (Exception e) {
|
||||
// 提供更详细的错误信息,包括异常类型和消息
|
||||
String errorMessage = "更新RAGFlow数据集失败: " + e.getClass().getSimpleName();
|
||||
String errorMessage = "更新RAG数据集失败: " + e.getClass().getSimpleName();
|
||||
if (e.getMessage() != null) {
|
||||
errorMessage += " - " + e.getMessage();
|
||||
}
|
||||
@@ -237,7 +229,7 @@ public class KnowledgeBaseServiceImpl extends BaseServiceImpl<KnowledgeBaseDao,
|
||||
throw e;
|
||||
}
|
||||
} else {
|
||||
log.warn("datasetId或ragModelId为空,跳过RAGFlow更新");
|
||||
log.warn("datasetId或ragModelId为空,跳过RAG更新");
|
||||
}
|
||||
|
||||
KnowledgeBaseEntity entity = ConvertUtils.sourceToTarget(knowledgeBaseDTO, KnowledgeBaseEntity.class);
|
||||
@@ -267,6 +259,26 @@ public class KnowledgeBaseServiceImpl extends BaseServiceImpl<KnowledgeBaseDao,
|
||||
return ConvertUtils.sourceToTarget(entity, KnowledgeBaseDTO.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据知识库ID集合查询知识库
|
||||
* @param datasetIdList 知识库ID集合
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public List<KnowledgeBaseDTO> getByDatasetIdList(List<String> datasetIdList) {
|
||||
//判断参数
|
||||
if (ToolUtil.isEmpty(datasetIdList)) {
|
||||
throw new RenException(ErrorCode.PARAMS_GET_ERROR);
|
||||
}
|
||||
//批量查询
|
||||
List<KnowledgeBaseEntity> entityList = knowledgeBaseDao.selectList(
|
||||
new QueryWrapper<KnowledgeBaseEntity>().in("dataset_id", datasetIdList));
|
||||
if (ToolUtil.isEmpty(entityList)) {
|
||||
throw new RenException(ErrorCode.Knowledge_Base_RECORD_NOT_EXISTS);
|
||||
}
|
||||
return ConvertUtils.sourceToTarget(entityList, KnowledgeBaseDTO.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteByDatasetId(String datasetId) {
|
||||
if (StringUtils.isBlank(datasetId)) {
|
||||
@@ -288,19 +300,19 @@ public class KnowledgeBaseServiceImpl extends BaseServiceImpl<KnowledgeBaseDao,
|
||||
log.info("找到记录: ID={}, datasetId={}, ragModelId={}",
|
||||
entity.getId(), entity.getDatasetId(), entity.getRagModelId());
|
||||
|
||||
// 先调用RAGFlow API删除数据集
|
||||
// 先调用RAG API删除数据集
|
||||
boolean apiDeleteSuccess = false;
|
||||
if (StringUtils.isNotBlank(entity.getDatasetId()) && StringUtils.isNotBlank(entity.getRagModelId())) {
|
||||
try {
|
||||
log.info("开始调用RAGFlow API删除数据集");
|
||||
log.info("开始调用RAG API删除数据集");
|
||||
// 在删除前进行RAG配置校验
|
||||
Map<String, Object> ragConfig = getValidatedRAGConfig(entity.getRagModelId());
|
||||
deleteDatasetInRAGFlow(entity.getDatasetId(), ragConfig);
|
||||
log.info("RAGFlow API删除调用完成");
|
||||
deleteDatasetInRAG(entity.getDatasetId(), ragConfig);
|
||||
log.info("RAG API删除调用完成");
|
||||
apiDeleteSuccess = true;
|
||||
} catch (Exception e) {
|
||||
// 提供更详细的错误信息,包括异常类型和消息
|
||||
String errorMessage = "删除RAGFlow数据集失败: " + e.getClass().getSimpleName();
|
||||
String errorMessage = "删除RAG数据集失败: " + e.getClass().getSimpleName();
|
||||
if (e.getMessage() != null) {
|
||||
errorMessage += " - " + e.getMessage();
|
||||
}
|
||||
@@ -308,7 +320,7 @@ public class KnowledgeBaseServiceImpl extends BaseServiceImpl<KnowledgeBaseDao,
|
||||
throw e;
|
||||
}
|
||||
} else {
|
||||
log.warn("datasetId或ragModelId为空,跳过RAGFlow删除");
|
||||
log.warn("datasetId或ragModelId为空,跳过RAG删除");
|
||||
apiDeleteSuccess = true; // 没有RAG数据集,视为成功
|
||||
}
|
||||
|
||||
@@ -378,24 +390,17 @@ public class KnowledgeBaseServiceImpl extends BaseServiceImpl<KnowledgeBaseDao,
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Map<String, Object>> getRAGModels() {
|
||||
public List<ModelConfigEntity> getRAGModels() {
|
||||
// 查询RAG类型的模型配置
|
||||
QueryWrapper<ModelConfigEntity> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("model_type", Constant.RAG_CONFIG_TYPE)
|
||||
QueryWrapper<ModelConfigEntity> queryWrapper = new QueryWrapper<ModelConfigEntity>()
|
||||
.select("id", "model_name")
|
||||
.eq("model_type", Constant.RAG_CONFIG_TYPE)
|
||||
.eq("is_enabled", 1)
|
||||
.orderByDesc("is_default")
|
||||
.orderByDesc("create_date");
|
||||
|
||||
List<ModelConfigEntity> modelConfigs = modelConfigDao.selectList(queryWrapper);
|
||||
|
||||
List<Map<String, Object>> modelList = new ArrayList<>();
|
||||
for (ModelConfigEntity modelConfig : modelConfigs) {
|
||||
Map<String, Object> modelInfo = new HashMap<>();
|
||||
modelInfo.put("id", modelConfig.getId());
|
||||
modelInfo.put("modelName", modelConfig.getModelName());
|
||||
modelList.add(modelInfo);
|
||||
}
|
||||
return modelList;
|
||||
return modelConfigs;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -432,289 +437,133 @@ public class KnowledgeBaseServiceImpl extends BaseServiceImpl<KnowledgeBaseDao,
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用RAGFlow API创建数据集
|
||||
* 从RAG配置中提取适配器类型
|
||||
*
|
||||
* @param config RAG配置
|
||||
* @return 适配器类型
|
||||
*/
|
||||
private String createDatasetInRAGFlow(String name, String description, Map<String, Object> ragConfig) {
|
||||
String datasetId = null;
|
||||
String baseUrl = (String) ragConfig.get("base_url");
|
||||
String apiKey = (String) ragConfig.get("api_key");
|
||||
|
||||
log.info("开始调用RAGFlow API创建数据集, name: {}", name);
|
||||
log.debug("RAGFlow配置 - baseUrl: {}, apiKey: {}", baseUrl, StringUtils.isBlank(apiKey) ? "未配置" : "已配置");
|
||||
|
||||
// 构建请求URL
|
||||
String url = baseUrl + "/api/v1/datasets";
|
||||
log.debug("请求URL: {}", url);
|
||||
|
||||
// 构建请求体
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
String username = SecurityUser.getUser().getUsername();
|
||||
requestBody.put("name", username + "_" + name);
|
||||
if (StringUtils.isNotBlank(description)) {
|
||||
requestBody.put("description", description);
|
||||
}
|
||||
log.debug("请求体: {}", requestBody);
|
||||
|
||||
// 设置请求头
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.set("Authorization", "Bearer " + apiKey);
|
||||
|
||||
HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(requestBody, headers);
|
||||
|
||||
// 发送POST请求
|
||||
log.info("发送POST请求到RAGFlow API...");
|
||||
ResponseEntity<String> response;
|
||||
try {
|
||||
response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class);
|
||||
} catch (Exception e) {
|
||||
String errorMessage = url + e.getMessage();
|
||||
log.error(errorMessage, e);
|
||||
throw new RenException(ErrorCode.RAG_API_ERROR, errorMessage);
|
||||
private String extractAdapterType(Map<String, Object> config) {
|
||||
if (config == null) {
|
||||
throw new RenException(ErrorCode.RAG_CONFIG_NOT_FOUND);
|
||||
}
|
||||
|
||||
log.info("RAGFlow API响应状态码: {}", response.getStatusCode());
|
||||
log.debug("RAGFlow API响应内容: {}", response.getBody());
|
||||
// 从配置中提取适配器类型
|
||||
String adapterType = (String) config.get("type");
|
||||
|
||||
if (!response.getStatusCode().is2xxSuccessful()) {
|
||||
String errorMessage = response.getStatusCode() + ", 响应内容: " + response.getBody();
|
||||
log.error(errorMessage);
|
||||
throw new RenException(ErrorCode.RAG_API_ERROR, errorMessage);
|
||||
// 验证适配器类型是否存在且非空
|
||||
if (StringUtils.isBlank(adapterType)) {
|
||||
throw new RenException(ErrorCode.RAG_ADAPTER_TYPE_NOT_FOUND);
|
||||
}
|
||||
|
||||
// 解析响应体,提取datasetId
|
||||
String responseBody = response.getBody();
|
||||
if (StringUtils.isNotBlank(responseBody)) {
|
||||
try {
|
||||
// 解析RAGFlow API响应,支持多种可能的字段名
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
Map<String, Object> responseMap = objectMapper.readValue(responseBody, Map.class);
|
||||
|
||||
log.debug("RAGFlow API响应解析结果: {}", responseMap);
|
||||
|
||||
// 首先检查响应码
|
||||
Integer code = (Integer) responseMap.get("code");
|
||||
String message = (String) responseMap.get("message");
|
||||
if (code != null && code == 0) {
|
||||
// 响应码为0表示成功,从data字段中获取datasetId
|
||||
Object dataObj = responseMap.get("data");
|
||||
if (dataObj instanceof Map) {
|
||||
Map<String, Object> dataMap = (Map<String, Object>) dataObj;
|
||||
datasetId = (String) dataMap.get("id");
|
||||
|
||||
if (StringUtils.isBlank(datasetId)) {
|
||||
// 如果id字段为空,尝试其他可能的字段名
|
||||
datasetId = (String) dataMap.get("dataset_id");
|
||||
datasetId = (String) dataMap.get("datasetId");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 如果响应码不为0,说明API调用失败
|
||||
String errorMessage = code + (message != null ? message : "null");
|
||||
log.error(errorMessage);
|
||||
throw new RenException(ErrorCode.RAG_API_ERROR, errorMessage);
|
||||
}
|
||||
|
||||
log.info("从RAGFlow API响应中解析出datasetId: {}", datasetId);
|
||||
log.debug("完整响应内容: {}", responseBody);
|
||||
} catch (IOException e) {
|
||||
// 构建详细的错误信息,包含异常类型和消息
|
||||
String baseErrorMessage = e.getClass().getSimpleName();
|
||||
String errorMessage = baseErrorMessage + (e.getMessage() != null ? ": " + e.getMessage() : "");
|
||||
log.error(errorMessage, e);
|
||||
throw new RenException(ErrorCode.RAG_API_ERROR, errorMessage);
|
||||
} catch (Exception e) {
|
||||
// 构建详细的错误信息,包含异常类型和消息
|
||||
String baseErrorMessage = e.getClass().getSimpleName();
|
||||
String errorMessage = baseErrorMessage + (e.getMessage() != null ? ": " + e.getMessage() : "");
|
||||
log.error(errorMessage, e);
|
||||
// 如果解析失败,但响应体不为空,尝试直接使用响应体作为错误信息
|
||||
String finalErrorMessage = responseBody;
|
||||
if (e.getMessage() != null) {
|
||||
finalErrorMessage += errorMessage;
|
||||
}
|
||||
throw new RenException(ErrorCode.RAG_API_ERROR, finalErrorMessage);
|
||||
}
|
||||
// 验证适配器类型是否已注册
|
||||
if (!KnowledgeBaseAdapterFactory.isAdapterTypeRegistered(adapterType)) {
|
||||
throw new RenException(ErrorCode.RAG_ADAPTER_TYPE_NOT_SUPPORTED,
|
||||
"不支持的适配器类型: " + adapterType);
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(datasetId)) {
|
||||
log.error("无法从RAGFlow API响应中获取datasetId,响应内容: {}", responseBody);
|
||||
throw new RenException(ErrorCode.RAG_DATASET_ID_NOT_NULL);
|
||||
}
|
||||
log.info("RAGFlow数据集创建成功,datasetId: {}", datasetId);
|
||||
|
||||
return datasetId;
|
||||
return adapterType;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用RAGFlow API更新数据集
|
||||
* 使用适配器创建数据集
|
||||
*/
|
||||
private void updateDatasetInRAGFlow(String datasetId, String name, String description,
|
||||
private String createDatasetInRAG(String name, String description, Map<String, Object> ragConfig) {
|
||||
log.info("开始使用适配器创建数据集, name: {}", name);
|
||||
|
||||
try {
|
||||
// 从RAG配置中提取适配器类型
|
||||
String adapterType = extractAdapterType(ragConfig);
|
||||
|
||||
// 使用适配器工厂获取适配器实例
|
||||
KnowledgeBaseAdapter adapter = KnowledgeBaseAdapterFactory.getAdapter(adapterType, ragConfig);
|
||||
|
||||
// 构建数据集创建参数
|
||||
Map<String, Object> createParams = new HashMap<>();
|
||||
String username = SecurityUser.getUser().getUsername();
|
||||
createParams.put("name", username + "_" + name);
|
||||
if (StringUtils.isNotBlank(description)) {
|
||||
createParams.put("description", description);
|
||||
}
|
||||
|
||||
// 调用适配器的创建数据集方法
|
||||
String datasetId = adapter.createDataset(createParams);
|
||||
|
||||
log.info("数据集创建成功,datasetId: {}", datasetId);
|
||||
return datasetId;
|
||||
|
||||
} catch (Exception e) {
|
||||
// 直接传递底层适配器的详细错误信息
|
||||
log.error("创建数据集失败", e);
|
||||
if (e instanceof RenException) {
|
||||
throw (RenException) e;
|
||||
}
|
||||
throw new RenException(ErrorCode.RAG_API_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用适配器更新数据集
|
||||
*/
|
||||
private void updateDatasetInRAG(String datasetId, String name, String description,
|
||||
Map<String, Object> ragConfig) {
|
||||
String baseUrl = (String) ragConfig.get("base_url");
|
||||
String apiKey = (String) ragConfig.get("api_key");
|
||||
log.info("开始使用适配器更新数据集,datasetId: {}, name: {}", datasetId, name);
|
||||
|
||||
log.info("开始调用RAGFlow API更新数据集,datasetId: {}, name: {}", datasetId, name);
|
||||
log.debug("RAGFlow配置 - baseUrl: {}, apiKey: {}", baseUrl, StringUtils.isBlank(apiKey) ? "未配置" : "已配置");
|
||||
|
||||
// 构建请求URL
|
||||
String url = baseUrl + "/api/v1/datasets/" + datasetId;
|
||||
log.debug("请求URL: {}", url);
|
||||
// 构建请求体
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("dataset_id", datasetId);
|
||||
String username = SecurityUser.getUser().getUsername();
|
||||
requestBody.put("name", username + "_" + name);
|
||||
if (StringUtils.isNotBlank(description)) {
|
||||
requestBody.put("description", description);
|
||||
}
|
||||
log.debug("请求体: {}", requestBody);
|
||||
|
||||
// 设置请求头
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.set("Authorization", "Bearer " + apiKey);
|
||||
|
||||
HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(requestBody, headers);
|
||||
|
||||
// 发送PUT请求
|
||||
log.info("发送PUT请求到RAGFlow API...");
|
||||
ResponseEntity<String> response;
|
||||
try {
|
||||
response = restTemplate.exchange(url, HttpMethod.PUT, requestEntity, String.class);
|
||||
} catch (Exception e) {
|
||||
String errorMessage = url + e.getMessage();
|
||||
log.error(errorMessage, e);
|
||||
throw new RenException(ErrorCode.RAG_API_ERROR, errorMessage);
|
||||
}
|
||||
// 从RAG配置中提取适配器类型
|
||||
String adapterType = extractAdapterType(ragConfig);
|
||||
|
||||
log.info("RAGFlow API响应状态码: {}", response.getStatusCode());
|
||||
log.debug("RAGFlow API响应内容: {}", response.getBody());
|
||||
// 使用适配器工厂获取适配器实例
|
||||
KnowledgeBaseAdapter adapter = KnowledgeBaseAdapterFactory.getAdapter(adapterType, ragConfig);
|
||||
|
||||
if (!response.getStatusCode().is2xxSuccessful()) {
|
||||
String errorMessage = response.getStatusCode() + ", 响应内容: " + response.getBody();
|
||||
log.error(errorMessage);
|
||||
throw new RenException(ErrorCode.RAG_API_ERROR, errorMessage);
|
||||
}
|
||||
|
||||
// 解析响应体,验证操作是否真正成功
|
||||
String responseBody = response.getBody();
|
||||
if (responseBody != null) {
|
||||
try {
|
||||
Map<String, Object> responseMap = objectMapper.readValue(responseBody, Map.class);
|
||||
Integer code = (Integer) responseMap.get("code");
|
||||
String message = (String) responseMap.get("message");
|
||||
|
||||
if (code != null && code != 0) {
|
||||
String errorMessage = code + (message != null ? message : "null");
|
||||
log.error(errorMessage);
|
||||
throw new RenException(ErrorCode.RAG_API_ERROR, errorMessage);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// 构建详细的错误信息,包含异常类型和消息
|
||||
String baseErrorMessage = e.getClass().getSimpleName();
|
||||
String errorMessage = baseErrorMessage + (e.getMessage() != null ? ": " + e.getMessage() : "");
|
||||
log.error(errorMessage, e);
|
||||
throw new RenException(ErrorCode.RAG_API_ERROR, errorMessage);
|
||||
} catch (Exception e) {
|
||||
// 构建详细的错误信息,包含异常类型和消息
|
||||
String baseErrorMessage = e.getClass().getSimpleName();
|
||||
String errorMessage = baseErrorMessage + (e.getMessage() != null ? ": " + e.getMessage() : "");
|
||||
log.error(errorMessage, e);
|
||||
// 如果解析失败,但响应体不为空,尝试直接使用响应体作为错误信息
|
||||
String finalErrorMessage = responseBody;
|
||||
if (e.getMessage() != null) {
|
||||
finalErrorMessage += errorMessage;
|
||||
}
|
||||
throw new RenException(ErrorCode.RAG_API_ERROR, finalErrorMessage);
|
||||
// 构建数据集更新参数
|
||||
Map<String, Object> updateParams = new HashMap<>();
|
||||
String username = SecurityUser.getUser().getUsername();
|
||||
updateParams.put("name", username + "_" + name);
|
||||
if (StringUtils.isNotBlank(description)) {
|
||||
updateParams.put("description", description);
|
||||
}
|
||||
|
||||
// 调用适配器的更新数据集方法
|
||||
adapter.updateDataset(datasetId, updateParams);
|
||||
|
||||
log.info("数据集更新成功,datasetId: {}", datasetId);
|
||||
|
||||
} catch (Exception e) {
|
||||
// 直接传递底层适配器的详细错误信息
|
||||
log.error("更新数据集失败", e);
|
||||
if (e instanceof RenException) {
|
||||
throw (RenException) e;
|
||||
}
|
||||
throw new RenException(ErrorCode.RAG_API_ERROR, e.getMessage());
|
||||
}
|
||||
|
||||
log.info("RAGFlow数据集更新成功,datasetId: {}", datasetId);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用RAGFlow API删除数据集
|
||||
* 使用适配器删除数据集
|
||||
*/
|
||||
private void deleteDatasetInRAGFlow(String datasetId, Map<String, Object> ragConfig) {
|
||||
String baseUrl = (String) ragConfig.get("base_url");
|
||||
String apiKey = (String) ragConfig.get("api_key");
|
||||
private void deleteDatasetInRAG(String datasetId, Map<String, Object> ragConfig) {
|
||||
log.info("开始使用适配器删除数据集,datasetId: {}", datasetId);
|
||||
|
||||
log.info("开始调用RAGFlow API删除数据集,datasetId: {}", datasetId);
|
||||
log.debug("RAGFlow配置 - baseUrl: {}, apiKey: {}", baseUrl, StringUtils.isBlank(apiKey) ? "未配置" : "已配置");
|
||||
|
||||
// 构建请求URL
|
||||
String url = baseUrl + "/api/v1/datasets";
|
||||
log.debug("请求URL: {}", url);
|
||||
|
||||
// 构建请求体
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("ids", List.of(datasetId));
|
||||
log.debug("请求体: {}", requestBody);
|
||||
|
||||
// 设置请求头
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.set("Authorization", "Bearer " + apiKey);
|
||||
|
||||
HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(requestBody, headers);
|
||||
|
||||
// 发送DELETE请求
|
||||
log.info("发送DELETE请求到RAGFlow API...");
|
||||
ResponseEntity<String> response;
|
||||
try {
|
||||
response = restTemplate.exchange(url, HttpMethod.DELETE, requestEntity, String.class);
|
||||
// 从RAG配置中提取适配器类型
|
||||
String adapterType = extractAdapterType(ragConfig);
|
||||
|
||||
// 使用适配器工厂获取适配器实例
|
||||
KnowledgeBaseAdapter adapter = KnowledgeBaseAdapterFactory.getAdapter(adapterType, ragConfig);
|
||||
|
||||
// 调用适配器的删除数据集方法
|
||||
adapter.deleteDataset(datasetId);
|
||||
|
||||
log.info("数据集删除成功,datasetId: {}", datasetId);
|
||||
|
||||
} catch (Exception e) {
|
||||
String errorMessage = url + e.getMessage();
|
||||
log.error(errorMessage, e);
|
||||
throw new RenException(ErrorCode.RAG_API_ERROR, errorMessage);
|
||||
}
|
||||
|
||||
log.info("RAGFlow API响应状态码: {}", response.getStatusCode());
|
||||
log.debug("RAGFlow API响应内容: {}", response.getBody());
|
||||
|
||||
if (!response.getStatusCode().is2xxSuccessful()) {
|
||||
String errorMessage = response.getStatusCode() + response.getBody();
|
||||
log.error(errorMessage);
|
||||
throw new RenException(ErrorCode.RAG_API_ERROR, errorMessage);
|
||||
}
|
||||
|
||||
// 解析响应体,验证操作是否真正成功
|
||||
String responseBody = response.getBody();
|
||||
if (responseBody != null) {
|
||||
try {
|
||||
Map<String, Object> responseMap = objectMapper.readValue(responseBody, Map.class);
|
||||
Integer code = (Integer) responseMap.get("code");
|
||||
String message = (String) responseMap.get("message");
|
||||
|
||||
if (code != null && code != 0) {
|
||||
String errorMessage = code + (message != null ? message : "null");
|
||||
log.error(errorMessage);
|
||||
throw new RenException(ErrorCode.RAG_API_ERROR, errorMessage);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// 构建详细的错误信息,包含异常类型和消息
|
||||
String baseErrorMessage = e.getClass().getSimpleName();
|
||||
String errorMessage = baseErrorMessage + (e.getMessage() != null ? ": " + e.getMessage() : "");
|
||||
log.error(errorMessage, e);
|
||||
throw new RenException(ErrorCode.RAG_API_ERROR, errorMessage);
|
||||
} catch (Exception e) {
|
||||
// 构建详细的错误信息,包含异常类型和消息
|
||||
String baseErrorMessage = e.getClass().getSimpleName();
|
||||
String errorMessage = baseErrorMessage + (e.getMessage() != null ? ": " + e.getMessage() : "");
|
||||
log.error(errorMessage, e);
|
||||
// 如果解析失败,但响应体不为空,尝试直接使用响应体作为错误信息
|
||||
String finalErrorMessage = responseBody;
|
||||
if (e.getMessage() != null) {
|
||||
finalErrorMessage += errorMessage;
|
||||
}
|
||||
throw new RenException(ErrorCode.RAG_API_ERROR, finalErrorMessage);
|
||||
// 直接传递底层适配器的详细错误信息
|
||||
log.error("删除数据集失败", e);
|
||||
if (e instanceof RenException) {
|
||||
throw (RenException) e;
|
||||
}
|
||||
throw new RenException(ErrorCode.RAG_API_ERROR, e.getMessage());
|
||||
}
|
||||
|
||||
log.info("RAGFlow数据集删除成功,datasetId: {}", datasetId);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -760,9 +609,9 @@ public class KnowledgeBaseServiceImpl extends BaseServiceImpl<KnowledgeBaseDao,
|
||||
}
|
||||
|
||||
/**
|
||||
* 从RAGFlow API获取知识库的文档数量
|
||||
* 从适配器获取知识库的文档数量
|
||||
*/
|
||||
private Integer getDocumentCountFromRAGFlow(String datasetId, String ragModelId) {
|
||||
private Integer getDocumentCountFromRAG(String datasetId, String ragModelId) {
|
||||
if (StringUtils.isBlank(datasetId) || StringUtils.isBlank(ragModelId)) {
|
||||
log.warn("datasetId或ragModelId为空,无法获取文档数量");
|
||||
return 0;
|
||||
@@ -770,79 +619,29 @@ public class KnowledgeBaseServiceImpl extends BaseServiceImpl<KnowledgeBaseDao,
|
||||
|
||||
log.info("开始获取知识库 {} 的文档数量", datasetId);
|
||||
|
||||
// 获取RAG配置
|
||||
Map<String, Object> ragConfig = getValidatedRAGConfig(ragModelId);
|
||||
String baseUrl = (String) ragConfig.get("base_url");
|
||||
String apiKey = (String) ragConfig.get("api_key");
|
||||
|
||||
// 构建请求URL - 调用RAGFlow API获取文档列表,但不返回文档详情,只获取总数
|
||||
String url = baseUrl + "/api/v1/datasets/" + datasetId + "/documents?page=1&size=1";
|
||||
log.debug("请求URL: {}", url);
|
||||
|
||||
// 设置请求头
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.set("Authorization", "Bearer " + apiKey);
|
||||
|
||||
HttpEntity<String> requestEntity = new HttpEntity<>(headers);
|
||||
|
||||
// 发送GET请求
|
||||
log.info("发送GET请求到RAGFlow API获取文档数量...");
|
||||
ResponseEntity<String> response;
|
||||
try {
|
||||
response = restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class);
|
||||
// 获取RAG配置
|
||||
Map<String, Object> ragConfig = getValidatedRAGConfig(ragModelId);
|
||||
|
||||
// 从RAG配置中提取适配器类型
|
||||
String adapterType = extractAdapterType(ragConfig);
|
||||
|
||||
// 使用适配器工厂获取适配器实例
|
||||
KnowledgeBaseAdapter adapter = KnowledgeBaseAdapterFactory.getAdapter(adapterType, ragConfig);
|
||||
|
||||
// 调用适配器的获取文档数量方法
|
||||
Integer documentCount = adapter.getDocumentCount(datasetId);
|
||||
|
||||
log.info("获取知识库 {} 的文档数量成功: {}", datasetId, documentCount);
|
||||
return documentCount;
|
||||
|
||||
} catch (Exception e) {
|
||||
String errorMessage = url + e.getMessage();
|
||||
// 构建详细的错误信息,包含异常类型和消息
|
||||
String baseErrorMessage = e.getClass().getSimpleName() + " - 获取知识库文档数量失败";
|
||||
String errorMessage = baseErrorMessage + (e.getMessage() != null ? ": " + e.getMessage() : "");
|
||||
log.error(errorMessage, e);
|
||||
return 0;
|
||||
}
|
||||
|
||||
log.info("RAGFlow API响应状态码: {}", response.getStatusCode());
|
||||
|
||||
if (!response.getStatusCode().is2xxSuccessful()) {
|
||||
log.error("RAGFlow API调用失败,状态码: {}, 响应内容: {}", response.getStatusCode(), response.getBody());
|
||||
return 0;
|
||||
}
|
||||
|
||||
String responseBody = response.getBody();
|
||||
log.debug("RAGFlow API响应内容: {}", responseBody);
|
||||
|
||||
// 解析响应
|
||||
try {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
Map<String, Object> responseMap = objectMapper.readValue(responseBody, Map.class);
|
||||
Integer code = (Integer) responseMap.get("code");
|
||||
|
||||
if (code != null && code == 0) {
|
||||
Object dataObj = responseMap.get("data");
|
||||
if (dataObj instanceof Map) {
|
||||
Map<String, Object> dataMap = (Map<String, Object>) dataObj;
|
||||
Object totalObj = dataMap.get("total");
|
||||
if (totalObj instanceof Integer) {
|
||||
Integer documentCount = (Integer) totalObj;
|
||||
log.info("获取知识库 {} 的文档数量成功: {}", datasetId, documentCount);
|
||||
return documentCount;
|
||||
} else if (totalObj instanceof Long) {
|
||||
Long documentCount = (Long) totalObj;
|
||||
log.info("获取知识库 {} 的文档数量成功: {}", datasetId, documentCount);
|
||||
return documentCount.intValue();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.error("RAGFlow API调用失败,响应码: {}, 响应内容: {}", code, responseBody);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// 构建详细的错误信息,包含异常类型和消息
|
||||
String baseErrorMessage = e.getClass().getSimpleName() + " - 解析RAGFlow API响应时发生IO异常";
|
||||
String errorMessage = baseErrorMessage + (e.getMessage() != null ? ": " + e.getMessage() : "");
|
||||
log.error(errorMessage, e);
|
||||
} catch (Exception e) {
|
||||
// 构建详细的错误信息,包含异常类型和消息
|
||||
String baseErrorMessage = e.getClass().getSimpleName() + " - 解析RAGFlow API响应失败";
|
||||
String errorMessage = baseErrorMessage + (e.getMessage() != null ? ": " + e.getMessage() : "");
|
||||
log.error(errorMessage, e);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
+235
-633
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,70 @@
|
||||
package xiaozhi.modules.llm.service;
|
||||
|
||||
/**
|
||||
* LLM服务接口
|
||||
* 支持多种大模型调用
|
||||
*/
|
||||
public interface LLMService {
|
||||
|
||||
/**
|
||||
* 生成聊天记录总结
|
||||
*
|
||||
* @param conversation 对话内容
|
||||
* @param promptTemplate 提示词模板
|
||||
* @return 总结结果
|
||||
*/
|
||||
String generateSummary(String conversation, String promptTemplate);
|
||||
|
||||
/**
|
||||
* 生成聊天记录总结(使用默认提示词)
|
||||
*
|
||||
* @param conversation 对话内容
|
||||
* @return 总结结果
|
||||
*/
|
||||
String generateSummary(String conversation);
|
||||
|
||||
/**
|
||||
* 生成聊天记录总结(指定模型ID)
|
||||
*
|
||||
* @param conversation 对话内容
|
||||
* @param modelId 模型ID
|
||||
* @return 总结结果
|
||||
*/
|
||||
String generateSummaryWithModel(String conversation, String modelId);
|
||||
|
||||
/**
|
||||
* 生成聊天记录总结(指定模型ID和提示词模板)
|
||||
*
|
||||
* @param conversation 对话内容
|
||||
* @param promptTemplate 提示词模板
|
||||
* @param modelId 模型ID
|
||||
* @return 总结结果
|
||||
*/
|
||||
String generateSummary(String conversation, String promptTemplate, String modelId);
|
||||
|
||||
/**
|
||||
* 生成聊天记录总结(包含历史记忆合并)
|
||||
*
|
||||
* @param conversation 对话内容
|
||||
* @param historyMemory 历史记忆
|
||||
* @param promptTemplate 提示词模板
|
||||
* @param modelId 模型ID
|
||||
* @return 总结结果
|
||||
*/
|
||||
String generateSummaryWithHistory(String conversation, String historyMemory, String promptTemplate, String modelId);
|
||||
|
||||
/**
|
||||
* 检查服务是否可用
|
||||
*
|
||||
* @return 是否可用
|
||||
*/
|
||||
boolean isAvailable();
|
||||
|
||||
/**
|
||||
* 检查指定模型的服务是否可用
|
||||
*
|
||||
* @param modelId 模型ID
|
||||
* @return 是否可用
|
||||
*/
|
||||
boolean isAvailable(String modelId);
|
||||
}
|
||||
+305
@@ -0,0 +1,305 @@
|
||||
package xiaozhi.modules.llm.service.impl;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import cn.hutool.json.JSONArray;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import xiaozhi.modules.llm.service.LLMService;
|
||||
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
||||
import xiaozhi.modules.model.service.ModelConfigService;
|
||||
|
||||
/**
|
||||
* OpenAI风格API的LLM服务实现
|
||||
* 支持阿里云、DeepSeek、ChatGLM等兼容OpenAI API的模型
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class OpenAIStyleLLMServiceImpl implements LLMService {
|
||||
|
||||
@Autowired
|
||||
private ModelConfigService modelConfigService;
|
||||
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
private static final String DEFAULT_SUMMARY_PROMPT = "你是一个经验丰富的记忆总结者,擅长将对话内容进行总结摘要,遵循以下规则:\n1、总结用户的重要信息,以便在未来的对话中提供更个性化的服务\n2、不要重复总结,不要遗忘之前记忆,除非原来的记忆超过了1800字,否则不要遗忘、不要压缩用户的历史记忆\n3、用户操控的设备音量、播放音乐、天气、退出、不想对话等和用户本身无关的内容,这些信息不需要加入到总结中\n4、聊天内容中的今天的日期时间、今天的天气情况与用户事件无关的数据,这些信息如果当成记忆存储会影响后续对话,这些信息不需要加入到总结中\n5、不要把设备操控的成果结果和失败结果加入到总结中,也不要把用户的一些废话加入到总结中\n6、不要为了总结而总结,如果用户的聊天没有意义,请返回原来的历史记录也是可以的\n7、只需要返回总结摘要,严格控制在1800字内\n8、不要包含代码、xml,不需要解释、注释和说明,保存记忆时仅从对话提取信息,不要混入示例内容\n9、如果提供了历史记忆,请将新对话内容与历史记忆进行智能合并,保留有价值的历史信息,同时添加新的重要信息\n\n历史记忆:\n{history_memory}\n\n新对话内容:\n{conversation}";
|
||||
|
||||
@Override
|
||||
public String generateSummary(String conversation) {
|
||||
return generateSummary(conversation, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateSummaryWithModel(String conversation, String modelId) {
|
||||
return generateSummary(conversation, null, modelId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateSummary(String conversation, String promptTemplate, String modelId) {
|
||||
if (!isAvailable()) {
|
||||
log.warn("LLM服务不可用,无法生成总结");
|
||||
return "LLM服务不可用,无法生成总结";
|
||||
}
|
||||
|
||||
try {
|
||||
// 从智控台获取LLM模型配置
|
||||
ModelConfigEntity llmConfig;
|
||||
if (modelId != null && !modelId.trim().isEmpty()) {
|
||||
// 通过具体模型ID获取配置
|
||||
llmConfig = modelConfigService.getModelByIdFromCache(modelId);
|
||||
} else {
|
||||
// 保持向后兼容,使用默认配置
|
||||
llmConfig = getDefaultLLMConfig();
|
||||
}
|
||||
|
||||
if (llmConfig == null || llmConfig.getConfigJson() == null) {
|
||||
log.error("未找到可用的LLM模型配置,modelId: {}", modelId);
|
||||
return "未找到可用的LLM模型配置";
|
||||
}
|
||||
|
||||
JSONObject configJson = llmConfig.getConfigJson();
|
||||
String baseUrl = configJson.getStr("base_url");
|
||||
String model = configJson.getStr("model_name");
|
||||
String apiKey = configJson.getStr("api_key");
|
||||
Double temperature = configJson.getDouble("temperature");
|
||||
Integer maxTokens = configJson.getInt("max_tokens");
|
||||
|
||||
if (StringUtils.isBlank(baseUrl) || StringUtils.isBlank(apiKey)) {
|
||||
log.error("LLM配置不完整,baseUrl或apiKey为空");
|
||||
return "LLM配置不完整,无法生成总结";
|
||||
}
|
||||
|
||||
// 构建提示词
|
||||
String prompt = (promptTemplate != null ? promptTemplate : DEFAULT_SUMMARY_PROMPT).replace("{conversation}",
|
||||
conversation);
|
||||
|
||||
// 构建请求体
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("model", model != null ? model : "gpt-3.5-turbo");
|
||||
|
||||
Map<String, Object>[] messages = new Map[1];
|
||||
Map<String, Object> message = new HashMap<>();
|
||||
message.put("role", "user");
|
||||
message.put("content", prompt);
|
||||
messages[0] = message;
|
||||
|
||||
requestBody.put("messages", messages);
|
||||
requestBody.put("temperature", temperature != null ? temperature : 0.7);
|
||||
requestBody.put("max_tokens", maxTokens != null ? maxTokens : 2000);
|
||||
|
||||
// 发送HTTP请求
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.set("Authorization", "Bearer " + apiKey);
|
||||
|
||||
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
|
||||
|
||||
// 构建完整的API URL
|
||||
String apiUrl = baseUrl;
|
||||
if (!apiUrl.endsWith("/chat/completions")) {
|
||||
if (!apiUrl.endsWith("/")) {
|
||||
apiUrl += "/";
|
||||
}
|
||||
apiUrl += "chat/completions";
|
||||
}
|
||||
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
apiUrl, HttpMethod.POST, entity, String.class);
|
||||
|
||||
if (response.getStatusCode().is2xxSuccessful()) {
|
||||
JSONObject responseJson = JSONUtil.parseObj(response.getBody());
|
||||
JSONArray choices = responseJson.getJSONArray("choices");
|
||||
if (choices != null && choices.size() > 0) {
|
||||
JSONObject choice = choices.getJSONObject(0);
|
||||
JSONObject messageObj = choice.getJSONObject("message");
|
||||
return messageObj.getStr("content");
|
||||
}
|
||||
} else {
|
||||
log.error("LLM API调用失败,状态码:{},响应:{}", response.getStatusCode(), response.getBody());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("调用LLM服务生成总结时发生异常,modelId: {}", modelId, e);
|
||||
}
|
||||
|
||||
return "生成总结失败,请稍后重试";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateSummary(String conversation, String promptTemplate) {
|
||||
return generateSummary(conversation, promptTemplate, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateSummaryWithHistory(String conversation, String historyMemory, String promptTemplate,
|
||||
String modelId) {
|
||||
if (!isAvailable()) {
|
||||
log.warn("LLM服务不可用,无法生成总结");
|
||||
return "LLM服务不可用,无法生成总结";
|
||||
}
|
||||
|
||||
try {
|
||||
// 从智控台获取LLM模型配置
|
||||
ModelConfigEntity llmConfig;
|
||||
if (modelId != null && !modelId.trim().isEmpty()) {
|
||||
// 通过具体模型ID获取配置
|
||||
llmConfig = modelConfigService.getModelByIdFromCache(modelId);
|
||||
} else {
|
||||
// 保持向后兼容,使用默认配置
|
||||
llmConfig = getDefaultLLMConfig();
|
||||
}
|
||||
|
||||
if (llmConfig == null || llmConfig.getConfigJson() == null) {
|
||||
log.error("未找到可用的LLM模型配置,modelId: {}", modelId);
|
||||
return "未找到可用的LLM模型配置";
|
||||
}
|
||||
|
||||
JSONObject configJson = llmConfig.getConfigJson();
|
||||
String baseUrl = configJson.getStr("base_url");
|
||||
String model = configJson.getStr("model_name");
|
||||
String apiKey = configJson.getStr("api_key");
|
||||
|
||||
if (StringUtils.isBlank(baseUrl) || StringUtils.isBlank(apiKey)) {
|
||||
log.error("LLM配置不完整,baseUrl或apiKey为空");
|
||||
return "LLM配置不完整,无法生成总结";
|
||||
}
|
||||
|
||||
// 构建提示词,包含历史记忆
|
||||
String prompt = (promptTemplate != null ? promptTemplate : DEFAULT_SUMMARY_PROMPT)
|
||||
.replace("{history_memory}", historyMemory != null ? historyMemory : "无历史记忆")
|
||||
.replace("{conversation}", conversation);
|
||||
|
||||
// 构建请求体
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("model", model != null ? model : "gpt-3.5-turbo");
|
||||
|
||||
Map<String, Object>[] messages = new Map[1];
|
||||
Map<String, Object> message = new HashMap<>();
|
||||
message.put("role", "user");
|
||||
message.put("content", prompt);
|
||||
messages[0] = message;
|
||||
|
||||
requestBody.put("messages", messages);
|
||||
requestBody.put("temperature", 0.2);
|
||||
requestBody.put("max_tokens", 2000);
|
||||
|
||||
// 发送HTTP请求
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.set("Authorization", "Bearer " + apiKey);
|
||||
|
||||
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
|
||||
|
||||
// 构建完整的API URL
|
||||
String apiUrl = baseUrl;
|
||||
if (!apiUrl.endsWith("/chat/completions")) {
|
||||
if (!apiUrl.endsWith("/")) {
|
||||
apiUrl += "/";
|
||||
}
|
||||
apiUrl += "chat/completions";
|
||||
}
|
||||
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
apiUrl, HttpMethod.POST, entity, String.class);
|
||||
|
||||
if (response.getStatusCode().is2xxSuccessful()) {
|
||||
JSONObject responseJson = JSONUtil.parseObj(response.getBody());
|
||||
JSONArray choices = responseJson.getJSONArray("choices");
|
||||
if (choices != null && choices.size() > 0) {
|
||||
JSONObject choice = choices.getJSONObject(0);
|
||||
JSONObject messageObj = choice.getJSONObject("message");
|
||||
return messageObj.getStr("content");
|
||||
}
|
||||
} else {
|
||||
log.error("LLM API调用失败,状态码:{},响应:{}", response.getStatusCode(), response.getBody());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("调用LLM服务生成总结时发生异常,modelId: {}", modelId, e);
|
||||
}
|
||||
|
||||
return "生成总结失败,请稍后重试";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAvailable() {
|
||||
try {
|
||||
ModelConfigEntity defaultLLMConfig = getDefaultLLMConfig();
|
||||
if (defaultLLMConfig == null || defaultLLMConfig.getConfigJson() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
JSONObject configJson = defaultLLMConfig.getConfigJson();
|
||||
String baseUrl = configJson.getStr("base_url");
|
||||
String apiKey = configJson.getStr("api_key");
|
||||
|
||||
return baseUrl != null && !baseUrl.trim().isEmpty() &&
|
||||
apiKey != null && !apiKey.trim().isEmpty();
|
||||
} catch (Exception e) {
|
||||
log.error("检查LLM服务可用性时发生异常:", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAvailable(String modelId) {
|
||||
try {
|
||||
if (modelId == null || modelId.trim().isEmpty()) {
|
||||
return isAvailable();
|
||||
}
|
||||
|
||||
// 通过具体模型ID获取配置
|
||||
ModelConfigEntity modelConfig = modelConfigService.getModelByIdFromCache(modelId);
|
||||
if (modelConfig == null || modelConfig.getConfigJson() == null) {
|
||||
log.warn("未找到指定的LLM模型配置,modelId: {}", modelId);
|
||||
return false;
|
||||
}
|
||||
|
||||
JSONObject configJson = modelConfig.getConfigJson();
|
||||
String baseUrl = configJson.getStr("base_url");
|
||||
String apiKey = configJson.getStr("api_key");
|
||||
|
||||
return baseUrl != null && !baseUrl.trim().isEmpty() &&
|
||||
apiKey != null && !apiKey.trim().isEmpty();
|
||||
} catch (Exception e) {
|
||||
log.error("检查LLM服务可用性时发生异常,modelId: {}", modelId, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从智控台获取默认的LLM模型配置
|
||||
*/
|
||||
private ModelConfigEntity getDefaultLLMConfig() {
|
||||
try {
|
||||
// 获取所有启用的LLM模型配置
|
||||
List<ModelConfigEntity> llmConfigs = modelConfigService.getEnabledModelsByType("LLM");
|
||||
if (llmConfigs == null || llmConfigs.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 优先返回默认配置,如果没有默认配置则返回第一个启用的配置
|
||||
for (ModelConfigEntity config : llmConfigs) {
|
||||
if (config.getIsDefault() != null && config.getIsDefault() == 1) {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
return llmConfigs.get(0);
|
||||
} catch (Exception e) {
|
||||
log.error("获取LLM模型配置时发生异常:", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,4 +19,27 @@ public class VoiceDTO implements Serializable {
|
||||
|
||||
@Schema(description = "音色名称")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "音频播放地址")
|
||||
private String voiceDemo;
|
||||
|
||||
@Schema(description = "是否为克隆音色")
|
||||
private Boolean isClone;
|
||||
|
||||
// 添加双参数构造函数,保持向后兼容
|
||||
public VoiceDTO(String id, String name) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.voiceDemo = null;
|
||||
this.isClone = false; // 默认不是克隆音色
|
||||
}
|
||||
|
||||
// 添加三参数构造函数,用于普通音色
|
||||
public VoiceDTO(String id, String name, String voiceDemo) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.voiceDemo = voiceDemo;
|
||||
this.isClone = false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -55,4 +55,12 @@ public interface ModelConfigService extends BaseService<ModelConfigEntity> {
|
||||
* @return TTS平台列表(id和modelName)
|
||||
*/
|
||||
List<Map<String, Object>> getTtsPlatformList();
|
||||
|
||||
/**
|
||||
* 根据模型类型获取所有启用的模型配置
|
||||
*
|
||||
* @param modelType 模型类型(如:LLM, TTS, ASR等)
|
||||
* @return 启用的模型配置列表
|
||||
*/
|
||||
List<ModelConfigEntity> getEnabledModelsByType(String modelType);
|
||||
}
|
||||
|
||||
+25
-7
@@ -56,7 +56,7 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
||||
new QueryWrapper<ModelConfigEntity>()
|
||||
.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"));
|
||||
return ConvertUtils.sourceToTarget(entities, ModelBasicInfoDTO.class);
|
||||
}
|
||||
@@ -67,14 +67,14 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
||||
new QueryWrapper<ModelConfigEntity>()
|
||||
.eq("model_type", "llm")
|
||||
.eq("is_enabled", 1)
|
||||
.like(StringUtils.isNotBlank(modelName), "model_name", "%" + modelName + "%")
|
||||
.like(StringUtils.isNotBlank(modelName), "model_name", modelName)
|
||||
.select("id", "model_name", "config_json"));
|
||||
|
||||
return entities.stream().map(item -> {
|
||||
LlmModelBasicInfoDTO dto = new LlmModelBasicInfoDTO();
|
||||
dto.setId(item.getId());
|
||||
dto.setModelName(item.getModelName());
|
||||
String type = item.getConfigJson().get("type").toString();
|
||||
String type = item.getConfigJson().getOrDefault("type", "").toString();
|
||||
dto.setType(type);
|
||||
return dto;
|
||||
}).toList();
|
||||
@@ -91,14 +91,13 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
||||
Page<ModelConfigEntity> pageInfo = new Page<>(curPage, pageSize);
|
||||
|
||||
// 添加排序规则:先按is_enabled降序,再按sort升序
|
||||
pageInfo.addOrder(OrderItem.desc("is_enabled"));
|
||||
pageInfo.addOrder(OrderItem.asc("sort"));
|
||||
pageInfo.addOrder(OrderItem.desc("is_enabled"), OrderItem.asc("sort"));
|
||||
|
||||
IPage<ModelConfigEntity> modelConfigEntityIPage = modelConfigDao.selectPage(
|
||||
pageInfo,
|
||||
new QueryWrapper<ModelConfigEntity>()
|
||||
.eq("model_type", modelType)
|
||||
.like(StringUtils.isNotBlank(modelName), "model_name", "%" + modelName + "%"));
|
||||
.like(StringUtils.isNotBlank(modelName), "model_name", modelName));
|
||||
|
||||
return getPageData(modelConfigEntityIPage, ModelConfigDTO.class);
|
||||
}
|
||||
@@ -346,6 +345,7 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
||||
modelConfigEntity.setModelName(modelConfigBodyDTO.getModelName());
|
||||
modelConfigEntity.setSort(modelConfigBodyDTO.getSort());
|
||||
modelConfigEntity.setIsEnabled(modelConfigBodyDTO.getIsEnabled());
|
||||
modelConfigEntity.setRemark(modelConfigBodyDTO.getRemark());
|
||||
// 3. 处理配置JSON,仅更新非敏感字段和明确修改的敏感字段
|
||||
if (modelConfigBodyDTO.getConfigJson() != null && originalEntity.getConfigJson() != null) {
|
||||
JSONObject originalJson = originalEntity.getConfigJson();
|
||||
@@ -488,7 +488,7 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
||||
List<ModelConfigEntity> intentConfigs = modelConfigDao.selectList(
|
||||
new QueryWrapper<ModelConfigEntity>()
|
||||
.eq("model_type", "Intent")
|
||||
.like("config_json", "%" + modelId + "%"));
|
||||
.like("config_json", modelId));
|
||||
if (!intentConfigs.isEmpty()) {
|
||||
throw new RenException(ErrorCode.LLM_REFERENCED_BY_INTENT);
|
||||
}
|
||||
@@ -502,4 +502,22 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
||||
public List<Map<String, Object>> getTtsPlatformList() {
|
||||
return modelConfigDao.getTtsPlatformList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据模型类型获取所有启用的模型配置
|
||||
*/
|
||||
@Override
|
||||
public List<ModelConfigEntity> getEnabledModelsByType(String modelType) {
|
||||
if (StringUtils.isBlank(modelType)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<ModelConfigEntity> entities = modelConfigDao.selectList(
|
||||
new QueryWrapper<ModelConfigEntity>()
|
||||
.eq("model_type", modelType)
|
||||
.eq("is_enabled", 1)
|
||||
.orderByAsc("sort"));
|
||||
|
||||
return entities;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-8
@@ -124,12 +124,6 @@ public class ModelProviderServiceImpl extends BaseServiceImpl<ModelProviderDao,
|
||||
return getPageData(modelProviderDao.selectPage(pageParam, wrapper), ModelProviderDTO.class);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
String jsonString = "\"[]\"";
|
||||
JSONArray jsonArray = new JSONArray(jsonString);
|
||||
System.out.println("字符串转 JSONArray: " + jsonArray.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModelProviderDTO add(ModelProviderDTO modelProviderDTO) {
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
@@ -153,8 +147,7 @@ public class ModelProviderServiceImpl extends BaseServiceImpl<ModelProviderDao,
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
modelProviderDTO.setUpdater(user.getId());
|
||||
modelProviderDTO.setUpdateDate(new Date());
|
||||
if (modelProviderDao
|
||||
.updateById(ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderEntity.class)) == 0) {
|
||||
if (modelProviderDao.updateById(ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderEntity.class)) == 0) {
|
||||
throw new RenException(ErrorCode.UPDATE_DATA_FAILED);
|
||||
}
|
||||
return ConvertUtils.sourceToTarget(modelProviderDTO, ModelProviderDTO.class);
|
||||
|
||||
@@ -89,7 +89,7 @@ public class ShiroConfig {
|
||||
filterMap.put("/config/**", "server");
|
||||
filterMap.put("/agent/chat-history/report", "server");
|
||||
filterMap.put("/agent/chat-history/download/**", "anon");
|
||||
filterMap.put("/agent/saveMemory/**", "server");
|
||||
filterMap.put("/agent/chat-summary/**", "server");
|
||||
filterMap.put("/agent/play/**", "anon");
|
||||
filterMap.put("/voiceClone/play/**", "anon");
|
||||
filterMap.put("/**", "oauth2");
|
||||
|
||||
+19
-14
@@ -6,6 +6,7 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
@@ -23,7 +24,9 @@ import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.page.TokenDTO;
|
||||
import xiaozhi.common.user.UserDetail;
|
||||
import xiaozhi.common.utils.JsonUtils;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.common.utils.Sm2DecryptUtil;
|
||||
import xiaozhi.common.validator.AssertUtils;
|
||||
import xiaozhi.common.validator.ValidatorUtils;
|
||||
import xiaozhi.modules.security.dto.LoginDTO;
|
||||
@@ -32,8 +35,6 @@ import xiaozhi.modules.security.password.PasswordUtils;
|
||||
import xiaozhi.modules.security.service.CaptchaService;
|
||||
import xiaozhi.modules.security.service.SysUserTokenService;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
import xiaozhi.common.utils.Sm2DecryptUtil;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import xiaozhi.modules.sys.dto.PasswordDTO;
|
||||
import xiaozhi.modules.sys.dto.RetrievePasswordDTO;
|
||||
import xiaozhi.modules.sys.dto.SysUserDTO;
|
||||
@@ -89,13 +90,13 @@ public class LoginController {
|
||||
@Operation(summary = "登录")
|
||||
public Result<TokenDTO> login(@RequestBody LoginDTO login) {
|
||||
String password = login.getPassword();
|
||||
|
||||
|
||||
// 使用工具类解密并验证验证码
|
||||
String actualPassword = Sm2DecryptUtil.decryptAndValidateCaptcha(
|
||||
password, login.getCaptchaId(), captchaService, sysParamsService);
|
||||
|
||||
|
||||
login.setPassword(actualPassword);
|
||||
|
||||
|
||||
// 按照用户名获取用户
|
||||
SysUserDTO userDTO = sysUserService.getByUsername(login.getUsername());
|
||||
// 判断用户是否存在
|
||||
@@ -108,8 +109,6 @@ public class LoginController {
|
||||
}
|
||||
return sysUserTokenService.createToken(userDTO.getId());
|
||||
}
|
||||
|
||||
|
||||
|
||||
@PostMapping("/register")
|
||||
@Operation(summary = "注册")
|
||||
@@ -117,15 +116,15 @@ public class LoginController {
|
||||
if (!sysUserService.getAllowUserRegister()) {
|
||||
throw new RenException(ErrorCode.USER_REGISTER_DISABLED);
|
||||
}
|
||||
|
||||
|
||||
String password = login.getPassword();
|
||||
|
||||
|
||||
// 使用工具类解密并验证验证码
|
||||
String actualPassword = Sm2DecryptUtil.decryptAndValidateCaptcha(
|
||||
password, login.getCaptchaId(), captchaService, sysParamsService);
|
||||
|
||||
|
||||
login.setPassword(actualPassword);
|
||||
|
||||
|
||||
// 是否开启手机注册
|
||||
Boolean isMobileRegister = sysParamsService
|
||||
.getValueObject(Constant.SysMSMParam.SERVER_ENABLE_MOBILE_REGISTER.getValue(), Boolean.class);
|
||||
@@ -204,11 +203,11 @@ public class LoginController {
|
||||
}
|
||||
|
||||
String password = dto.getPassword();
|
||||
|
||||
|
||||
// 使用工具类解密并验证验证码
|
||||
String actualPassword = Sm2DecryptUtil.decryptAndValidateCaptcha(
|
||||
password, dto.getCaptchaId(), captchaService, sysParamsService);
|
||||
|
||||
|
||||
dto.setPassword(actualPassword);
|
||||
|
||||
sysUserService.changePasswordDirectly(userDTO.getId(), dto.getPassword());
|
||||
@@ -229,7 +228,7 @@ public class LoginController {
|
||||
config.put("beianIcpNum", sysParamsService.getValue(Constant.SysBaseParam.BEIAN_ICP_NUM.getValue(), true));
|
||||
config.put("beianGaNum", sysParamsService.getValue(Constant.SysBaseParam.BEIAN_GA_NUM.getValue(), true));
|
||||
config.put("name", sysParamsService.getValue(Constant.SysBaseParam.SERVER_NAME.getValue(), true));
|
||||
|
||||
|
||||
// SM2公钥
|
||||
String publicKey = sysParamsService.getValue(Constant.SM2_PUBLIC_KEY, true);
|
||||
if (StringUtils.isBlank(publicKey)) {
|
||||
@@ -237,6 +236,12 @@ public class LoginController {
|
||||
}
|
||||
config.put("sm2PublicKey", publicKey);
|
||||
|
||||
// 获取system-web.menu参数配置
|
||||
String menuConfig = sysParamsService.getValue("system-web.menu", true);
|
||||
if (StringUtils.isNotBlank(menuConfig)) {
|
||||
config.put("systemWebMenu", JsonUtils.parseObject(menuConfig, Object.class));
|
||||
}
|
||||
|
||||
return new Result<Map<String, Object>>().ok(config);
|
||||
}
|
||||
}
|
||||
+19
-2
@@ -31,6 +31,8 @@ import xiaozhi.modules.sys.dto.ServerActionResponseDTO;
|
||||
import xiaozhi.modules.sys.enums.ServerActionEnum;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
import xiaozhi.modules.sys.utils.WebSocketClientManager;
|
||||
import xiaozhi.modules.device.service.DeviceService;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
|
||||
/**
|
||||
* 服务端管理控制器
|
||||
@@ -41,6 +43,8 @@ import xiaozhi.modules.sys.utils.WebSocketClientManager;
|
||||
@AllArgsConstructor
|
||||
public class ServerSideManageController {
|
||||
private final SysParamsService sysParamsService;
|
||||
private final DeviceService deviceService;
|
||||
private final RedisUtils redisUtils;
|
||||
private static final ObjectMapper objectMapper;
|
||||
static {
|
||||
objectMapper = new ObjectMapper();
|
||||
@@ -85,9 +89,22 @@ public class ServerSideManageController {
|
||||
return false;
|
||||
}
|
||||
String serverSK = sysParamsService.getValue(Constant.SERVER_SECRET, true);
|
||||
|
||||
String deviceId = UUID.randomUUID().toString();
|
||||
String clientId = UUID.randomUUID().toString();
|
||||
|
||||
String redisKey = xiaozhi.common.redis.RedisKeys.getTmpRegisterMacKey(deviceId);
|
||||
redisUtils.set(redisKey, "true", 300); // 5分钟有效期
|
||||
|
||||
WebSocketHttpHeaders headers = new WebSocketHttpHeaders();
|
||||
headers.add("device-id", UUID.randomUUID().toString());
|
||||
headers.add("client-id", UUID.randomUUID().toString());
|
||||
headers.add("device-id", deviceId);
|
||||
headers.add("client-id", clientId);
|
||||
try {
|
||||
String token = deviceService.generateWebSocketToken(clientId, deviceId);
|
||||
headers.add("authorization", "Bearer " + token);
|
||||
} catch (Exception e) {
|
||||
throw new RenException(ErrorCode.WEB_SOCKET_CONNECT_FAILED);
|
||||
}
|
||||
|
||||
try (WebSocketClientManager client = new WebSocketClientManager.Builder()
|
||||
.connectTimeout(3, TimeUnit.SECONDS)
|
||||
|
||||
+3
-3
@@ -174,7 +174,7 @@ public class SysParamsController {
|
||||
return;
|
||||
}
|
||||
if (StringUtils.isBlank(url) || url.equals("null")) {
|
||||
throw new RenException(ErrorCode.OTA_URL_EMPTY);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否包含localhost或127.0.0.1
|
||||
@@ -211,7 +211,7 @@ public class SysParamsController {
|
||||
return;
|
||||
}
|
||||
if (StringUtils.isBlank(url) || url.equals("null")) {
|
||||
throw new RenException(ErrorCode.MCP_URL_EMPTY);
|
||||
return;
|
||||
}
|
||||
if (url.contains("localhost") || url.contains("127.0.0.1")) {
|
||||
throw new RenException(ErrorCode.MCP_URL_LOCALHOST);
|
||||
@@ -242,7 +242,7 @@ public class SysParamsController {
|
||||
return;
|
||||
}
|
||||
if (StringUtils.isBlank(url) || url.equals("null")) {
|
||||
throw new RenException(ErrorCode.VOICEPRINT_URL_EMPTY);
|
||||
return;
|
||||
}
|
||||
if (url.contains("localhost") || url.contains("127.0.0.1")) {
|
||||
throw new RenException(ErrorCode.VOICEPRINT_URL_LOCALHOST);
|
||||
|
||||
@@ -4,6 +4,7 @@ import java.util.List;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import xiaozhi.common.dao.BaseDao;
|
||||
import xiaozhi.modules.sys.entity.SysDictDataEntity;
|
||||
import xiaozhi.modules.sys.vo.SysDictDataItem;
|
||||
@@ -23,4 +24,9 @@ public interface SysDictDataDao extends BaseDao<SysDictDataEntity> {
|
||||
* @return 字典类型编码
|
||||
*/
|
||||
String getTypeByTypeId(Long dictTypeId);
|
||||
|
||||
/**
|
||||
* 根据字典数据ID集合获取字典类型编码集合
|
||||
*/
|
||||
List<String> getDictTypesByIdList(@Param("dictDataIdList") List<Long> dictDataIdList);
|
||||
}
|
||||
|
||||
+15
-11
@@ -1,9 +1,6 @@
|
||||
package xiaozhi.modules.sys.service.impl;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -23,6 +20,7 @@ import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.common.utils.ToolUtil;
|
||||
import xiaozhi.modules.sys.dao.SysDictDataDao;
|
||||
import xiaozhi.modules.sys.dao.SysUserDao;
|
||||
import xiaozhi.modules.sys.dto.SysDictDataDTO;
|
||||
@@ -104,13 +102,19 @@ public class SysDictDataServiceImpl extends BaseServiceImpl<SysDictDataDao, SysD
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void delete(Long[] ids) {
|
||||
for (Long id : ids) {
|
||||
SysDictDataEntity entity = baseDao.selectById(id);
|
||||
// 删除Redis缓存
|
||||
String dictType = baseDao.getTypeByTypeId(entity.getDictTypeId());
|
||||
redisUtils.delete(RedisKeys.getDictDataByTypeKey(dictType));
|
||||
// 删除
|
||||
deleteById(id);
|
||||
List<Long> idList = Arrays.asList(ids);
|
||||
if (ToolUtil.isNotEmpty(idList)) {
|
||||
//批量删除redis字典
|
||||
List<String> redisKeyList = new ArrayList<>();
|
||||
//批量获取字典类型
|
||||
List<String> dictTypeList = Optional.ofNullable(baseDao.getDictTypesByIdList(idList)).orElseGet(ArrayList::new);
|
||||
dictTypeList.forEach(dictType -> redisKeyList.add(RedisKeys.getDictDataByTypeKey(dictType)));
|
||||
if (ToolUtil.isNotEmpty(redisKeyList)) {
|
||||
//清除缓存
|
||||
redisUtils.delete(redisKeyList);
|
||||
}
|
||||
//批量删除字典数据
|
||||
deleteBatchIds(Arrays.asList(ids));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+16
-11
@@ -1,10 +1,6 @@
|
||||
package xiaozhi.modules.timbre.service.impl;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
@@ -128,12 +124,14 @@ public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity>
|
||||
if (StringUtils.isNotBlank(voiceName)) {
|
||||
queryWrapper.like("name", voiceName);
|
||||
}
|
||||
List<TimbreEntity> timbreEntities = timbreDao.selectList(queryWrapper);
|
||||
if (timbreEntities == null) {
|
||||
timbreEntities = new ArrayList<>();
|
||||
}
|
||||
List<TimbreEntity> timbreEntities = Optional.ofNullable(timbreDao.selectList(queryWrapper)).orElseGet(ArrayList::new);
|
||||
List<VoiceDTO> voiceDTOs = timbreEntities.stream()
|
||||
.map(entity -> new VoiceDTO(entity.getId(), entity.getName()))
|
||||
.map(entity -> {
|
||||
VoiceDTO dto = new VoiceDTO(entity.getId(), entity.getName());
|
||||
dto.setVoiceDemo(entity.getVoiceDemo());
|
||||
dto.setIsClone(false); // 设置为普通音色
|
||||
return dto;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 获取当前登录用户ID
|
||||
@@ -146,6 +144,9 @@ public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity>
|
||||
VoiceDTO voiceDTO = new VoiceDTO();
|
||||
voiceDTO.setId(entity.getId());
|
||||
voiceDTO.setName(MessageUtils.getMessage(ErrorCode.VOICE_CLONE_PREFIX) + entity.getName());
|
||||
// 保留从数据库查询到的voiceDemo字段
|
||||
voiceDTO.setVoiceDemo(entity.getVoiceDemo());
|
||||
voiceDTO.setIsClone(true); // 设置为克隆音色
|
||||
redisUtils.set(RedisKeys.getTimbreNameById(voiceDTO.getId()), voiceDTO.getName(),
|
||||
RedisUtils.NOT_EXPIRE);
|
||||
voiceDTOs.add(0, voiceDTO);
|
||||
@@ -205,6 +206,10 @@ public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity>
|
||||
if (list.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return new VoiceDTO(list.get(0).getId(), list.get(0).getName());
|
||||
TimbreEntity entity = list.get(0);
|
||||
VoiceDTO dto = new VoiceDTO(entity.getId(), entity.getName());
|
||||
dto.setVoiceDemo(entity.getVoiceDemo());
|
||||
dto.setIsClone(false); // 设置为普通音色
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
+7
@@ -83,6 +83,13 @@ public class VoiceCloneController {
|
||||
return new Result<String>().error(ErrorCode.VOICE_CLONE_NOT_AUDIO_FILE);
|
||||
}
|
||||
|
||||
// 加强验证文件扩展名
|
||||
String originalFilename = voiceFile.getOriginalFilename();
|
||||
String extension = originalFilename.substring(originalFilename.lastIndexOf(".")).toLowerCase();
|
||||
if (!extension.equals(".mp3") && !extension.equals(".wav")) {
|
||||
return new Result<String>().error("只允许上传.mp3和.wav格式的文件");
|
||||
}
|
||||
|
||||
// 验证文件大小 (最大10MB)
|
||||
if (voiceFile.getSize() > 10 * 1024 * 1024) {
|
||||
return new Result<String>().error(ErrorCode.VOICE_CLONE_AUDIO_TOO_LARGE);
|
||||
|
||||
+25
-10
@@ -4,12 +4,8 @@ import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -30,8 +26,11 @@ import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.common.utils.DateUtils;
|
||||
import xiaozhi.common.utils.ToolUtil;
|
||||
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
||||
import xiaozhi.modules.model.service.ModelConfigService;
|
||||
import xiaozhi.modules.sys.dao.SysUserDao;
|
||||
import xiaozhi.modules.sys.entity.SysUserEntity;
|
||||
import xiaozhi.modules.sys.service.SysUserService;
|
||||
import xiaozhi.modules.voiceclone.dao.VoiceCloneDao;
|
||||
import xiaozhi.modules.voiceclone.dto.VoiceCloneDTO;
|
||||
@@ -47,6 +46,7 @@ public class VoiceCloneServiceImpl extends BaseServiceImpl<VoiceCloneDao, VoiceC
|
||||
|
||||
private final ModelConfigService modelConfigService;
|
||||
private final SysUserService sysUserService;
|
||||
private final SysUserDao sysUserDao;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Override
|
||||
@@ -104,9 +104,11 @@ public class VoiceCloneServiceImpl extends BaseServiceImpl<VoiceCloneDao, VoiceC
|
||||
}
|
||||
}
|
||||
|
||||
// 批量保存
|
||||
List<VoiceCloneEntity> batchInsertList = new ArrayList<>();
|
||||
// 遍历选择的音色ID,为每个音色ID创建一条记录
|
||||
int index = 0;
|
||||
String namePrefix = DateUtils.format(new java.util.Date(), "MMddHHmm");
|
||||
String namePrefix = DateUtils.format(new Date(), "MMddHHmm");
|
||||
for (String voiceId : dto.getVoiceIds()) {
|
||||
index++;
|
||||
VoiceCloneEntity entity = new VoiceCloneEntity();
|
||||
@@ -115,8 +117,10 @@ public class VoiceCloneServiceImpl extends BaseServiceImpl<VoiceCloneDao, VoiceC
|
||||
entity.setName(namePrefix + "_" + index);
|
||||
entity.setUserId(dto.getUserId());
|
||||
entity.setTrainStatus(0); // 默认训练中
|
||||
|
||||
baseDao.insert(entity);
|
||||
batchInsertList.add(entity);
|
||||
}
|
||||
if (ToolUtil.isNotEmpty(batchInsertList)) {
|
||||
insertBatch(batchInsertList);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,6 +168,9 @@ public class VoiceCloneServiceImpl extends BaseServiceImpl<VoiceCloneDao, VoiceC
|
||||
if (entity.getUserId() != null) {
|
||||
dto.setUserName(sysUserService.getByUserId(entity.getUserId()).getUsername());
|
||||
}
|
||||
|
||||
// 确保trainStatus字段被正确设置,前端需要这个字段来判断是否为克隆音频
|
||||
dto.setTrainStatus(entity.getTrainStatus());
|
||||
|
||||
return dto;
|
||||
}
|
||||
@@ -184,6 +191,11 @@ public class VoiceCloneServiceImpl extends BaseServiceImpl<VoiceCloneDao, VoiceC
|
||||
|
||||
List<VoiceCloneResponseDTO> dtoList = new ArrayList<>(entityList.size());
|
||||
|
||||
// 获取用户名称ID集合
|
||||
Set<Long> userIdList = entityList.stream().map(VoiceCloneEntity::getUserId).collect(Collectors.toSet());
|
||||
List<SysUserEntity> userList = sysUserDao.selectList(new QueryWrapper<SysUserEntity>().in("id", userIdList));
|
||||
Map<Long, String> userMap = userList.stream().collect(Collectors.toMap(SysUserEntity::getId, SysUserEntity::getUsername));
|
||||
|
||||
// 转换每个实体为DTO
|
||||
for (VoiceCloneEntity entity : entityList) {
|
||||
VoiceCloneResponseDTO dto = ConvertUtils.sourceToTarget(entity, VoiceCloneResponseDTO.class);
|
||||
@@ -195,8 +207,11 @@ public class VoiceCloneServiceImpl extends BaseServiceImpl<VoiceCloneDao, VoiceC
|
||||
|
||||
// 设置用户名称
|
||||
if (entity.getUserId() != null) {
|
||||
dto.setUserName(sysUserService.getByUserId(entity.getUserId()).getUsername());
|
||||
dto.setUserName(userMap.get(entity.getUserId()));
|
||||
}
|
||||
|
||||
// 确保trainStatus字段被正确设置,前端需要这个字段来判断是否为克隆音频
|
||||
dto.setTrainStatus(entity.getTrainStatus());
|
||||
|
||||
// 设置是否有音频数据
|
||||
dto.setHasVoice(entity.getVoice() != null);
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
-- 更新HuoshanDoubleStreamTTS供应器配置,增加开启链接复用选项
|
||||
UPDATE `ai_model_provider`
|
||||
SET fields = '[{"key": "ws_url", "type": "string", "label": "WebSocket地址"}, {"key": "appid", "type": "string", "label": "应用ID"}, {"key": "access_token", "type": "string", "label": "访问令牌"}, {"key": "resource_id", "type": "string", "label": "资源ID"}, {"key": "speaker", "type": "string", "label": "默认音色"}, {"key": "enable_ws_reuse", "type": "boolean", "label": "是否开启链接复用", "default": true}, {"key": "speech_rate", "type": "number", "label": "语速(-50~100)"}, {"key": "loudness_rate", "type": "number", "label": "音量(-50~100)"}, {"key": "pitch", "type": "number", "label": "音高(-12~12)"}]'
|
||||
WHERE id = 'SYSTEM_TTS_HSDSTTS';
|
||||
|
||||
UPDATE `ai_model_config` SET
|
||||
`doc_link` = 'https://console.volcengine.com/speech/service/10007',
|
||||
`remark` = '火山引擎语音合成服务配置说明:
|
||||
1. 访问 https://www.volcengine.com/ 注册并开通火山引擎账号
|
||||
2. 访问 https://console.volcengine.com/speech/service/10007 开通语音合成大模型,购买音色
|
||||
3. 在页面底部获取appid和access_token
|
||||
5. 资源ID固定为:volc.service_type.10029(大模型语音合成及混音)
|
||||
6. 链接复用:开启WebSocket连接复用,默认true减少链接损耗(注意:复用后设备处于聆听状态时空闲链接会占并发数)
|
||||
7. 语速:-50~100,可不填,正常默认值0,可填-50~100
|
||||
8. 音量:-50~100,可不填,正常默认值0,可填-50~100
|
||||
9. 音高:-12~12,可不填,正常默认值0,可填-12~12
|
||||
10. 填入配置文件中' WHERE `id` = 'TTS_HuoshanDoubleStreamTTS';
|
||||
@@ -0,0 +1,6 @@
|
||||
-- 添加系统功能菜单配置参数
|
||||
delete from `sys_params` where param_code = 'system-web.menu';
|
||||
|
||||
-- 添加系统功能菜单配置参数
|
||||
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES
|
||||
(600, 'system-web.menu', '{"features":{"voiceprintRecognition":{"name":"feature.voiceprintRecognition.name","enabled":false,"description":"feature.voiceprintRecognition.description"},"voiceClone":{"name":"feature.voiceClone.name","enabled":false,"description":"feature.voiceClone.description"},"knowledgeBase":{"name":"feature.knowledgeBase.name","enabled":false,"description":"feature.knowledgeBase.description"},"mcpAccessPoint":{"name":"feature.mcpAccessPoint.name","enabled":false,"description":"feature.mcpAccessPoint.description"},"vad":{"name":"feature.vad.name","enabled":true,"description":"feature.vad.description"},"asr":{"name":"feature.asr.name","enabled":true,"description":"feature.asr.description"}},"groups":{"featureManagement":["voiceprintRecognition","voiceClone","knowledgeBase","mcpAccessPoint"],"voiceManagement":["vad","asr"]}}', 'json', 1, '系统功能菜单配置');
|
||||
@@ -0,0 +1,14 @@
|
||||
-- liquibase formatted sql
|
||||
|
||||
-- changeset xiaozhi:202512041515
|
||||
CREATE TABLE ai_agent_context_provider (
|
||||
id VARCHAR(32) NOT NULL COMMENT '主键',
|
||||
agent_id VARCHAR(32) NOT NULL COMMENT '智能体ID',
|
||||
context_providers JSON COMMENT '上下文源配置',
|
||||
creator BIGINT COMMENT '创建者',
|
||||
created_at DATETIME COMMENT '创建时间',
|
||||
updater BIGINT COMMENT '更新者',
|
||||
updated_at DATETIME COMMENT '更新时间',
|
||||
PRIMARY KEY (id),
|
||||
INDEX idx_agent_id (agent_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='智能体上下文源配置表';
|
||||
@@ -0,0 +1,6 @@
|
||||
-- 删除server模块是否开启token认证参数
|
||||
delete from `sys_params` where param_code = 'server.auth.enabled';
|
||||
|
||||
-- 添加server模块是否开启token认证参数
|
||||
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES
|
||||
(122, 'server.auth.enabled', 'true', 'boolean', 1, 'server模块是否开启token认证');
|
||||
@@ -0,0 +1 @@
|
||||
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (311, 'enable_websocket_ping', 'false', 'boolean', 1, '是否启用WebSocket心跳保活机制');
|
||||
@@ -0,0 +1,2 @@
|
||||
-- 为智能体聊天历史记录添加音频ID索引
|
||||
ALTER TABLE ai_agent_chat_history ADD INDEX idx_ai_agent_chat_history_audio_id (audio_id);
|
||||
@@ -0,0 +1,10 @@
|
||||
-- 更新豆包流式ASR供应器,增加end_window_size配置
|
||||
delete from `ai_model_provider` where id = 'SYSTEM_ASR_DoubaoStreamASR';
|
||||
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
|
||||
('SYSTEM_ASR_DoubaoStreamASR', 'ASR', 'doubao_stream', '火山引擎语音识别(流式)', '[{"key":"appid","label":"应用ID","type":"string"},{"key":"access_token","label":"访问令牌","type":"string"},{"key":"cluster","label":"集群","type":"string"},{"key":"boosting_table_name","label":"热词文件名称","type":"string"},{"key":"correct_table_name","label":"替换词文件名称","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"},{"key":"end_window_size","label":"静音判定时长(ms)","type":"number"}]', 3, 1, NOW(), 1, NOW());
|
||||
|
||||
|
||||
-- 更新豆包流式ASR模型配置,增加end_window_size默认值
|
||||
UPDATE `ai_model_config` SET
|
||||
`config_json` = JSON_SET(`config_json`, '$.end_window_size', 200)
|
||||
WHERE `id` = 'ASR_DoubaoStreamASR' AND JSON_EXTRACT(`config_json`, '$.end_window_size') IS NULL;
|
||||
@@ -0,0 +1,94 @@
|
||||
-- 添加阿里百炼Paraformer实时语音识别服务配置
|
||||
delete from `ai_model_provider` where id = 'SYSTEM_ASR_AliyunBLStream';
|
||||
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
|
||||
('SYSTEM_ASR_AliyunBLStream', 'ASR', 'aliyunbl_stream', '阿里百炼Paraformer实时语音识别', '[{"key":"api_key","label":"API密钥","type":"password"},{"key":"model","label":"模型名称","type":"string"},{"key":"format","label":"音频格式","type":"string"},{"key":"sample_rate","label":"采样率","type":"number"},{"key":"output_dir","label":"输出目录","type":"string"}]', 18, 1, NOW(), 1, NOW());
|
||||
|
||||
delete from `ai_model_config` where id = 'ASR_AliyunBLStream';
|
||||
INSERT INTO `ai_model_config` VALUES ('ASR_AliyunBLStream', 'ASR', 'AliyunBLStream', '阿里百炼Paraformer实时语音识别', 0, 1, '{"type": "aliyunbl_stream", "api_key": "", "model": "paraformer-realtime-v2", "format": "pcm", "sample_rate": 16000, "disfluency_removal_enabled": false, "semantic_punctuation_enabled": false, "max_sentence_silence": 200, "multi_threshold_mode_enabled": false, "punctuation_prediction_enabled": true, "inverse_text_normalization_enabled": true, "output_dir": "tmp/"}', 'https://help.aliyun.com/zh/model-studio/websocket-for-paraformer-real-time-service', '支持多语言、热词定制、语义断句等高级功能', 21, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- 更新阿里百炼Paraformer模型配置的说明文档
|
||||
UPDATE `ai_model_config` SET
|
||||
`doc_link` = 'https://help.aliyun.com/zh/model-studio/websocket-for-paraformer-real-time-service',
|
||||
`remark` = '阿里百炼Paraformer实时语音识别配置说明:
|
||||
1. 登录阿里云百炼平台 https://bailian.console.aliyun.com/
|
||||
2. 创建API-KEY https://bailian.console.aliyun.com/#/api-key
|
||||
3. 支持模型:paraformer-realtime-v2(推荐)、paraformer-realtime-8k-v2、paraformer-realtime-v1、paraformer-realtime-8k-v1
|
||||
4. 功能特性:
|
||||
- 多语言支持(中文含方言、英文、日语、韩语、德语、法语、俄语)
|
||||
- 热词定制(vocabulary_id参数),详细说明请参考:https://help.aliyun.com/zh/model-studio/custom-hot-words?
|
||||
- 语义断句/VAD断句(semantic_punctuation_enabled参数)
|
||||
- 自动标点符号、ITN、过滤语气词等
|
||||
5. 参数说明:
|
||||
- model: 模型名称,推荐paraformer-realtime-v2
|
||||
- sample_rate: 采样率(Hz),v2支持任意采样率,v1仅支持16000,8k版本仅支持8000
|
||||
- semantic_punctuation_enabled: false为VAD断句(低延迟),true为语义断句(高准确)
|
||||
- max_sentence_silence: VAD断句静音时长阈值(200-6000ms)
|
||||
' WHERE `id` = 'ASR_AliyunBLStream';
|
||||
|
||||
|
||||
-- 更新豆包流式ASR供应器,增加配置
|
||||
delete from `ai_model_provider` where id = 'SYSTEM_ASR_DoubaoStreamASR';
|
||||
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
|
||||
('SYSTEM_ASR_DoubaoStreamASR', 'ASR', 'doubao_stream', '火山引擎语音识别(流式)', '[{"key":"appid","label":"应用ID","type":"string"},{"key":"access_token","label":"访问令牌","type":"string"},{"key":"cluster","label":"集群","type":"string"},{"key":"boosting_table_name","label":"热词文件名称","type":"string"},{"key":"correct_table_name","label":"替换词文件名称","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"},{"key":"end_window_size","label":"静音判定时长(ms)","type":"number"},{"key":"enable_multilingual","label":"是否开启多语种识别模式","type":"boolean"},{"key":"language","label":"指定语言编码","type":"string"}]', 3, 1, NOW(), 1, NOW());
|
||||
UPDATE `ai_model_config` SET
|
||||
`remark` = '豆包ASR配置说明:
|
||||
1. 豆包ASR和豆包(流式)ASR的区别是:豆包ASR是按次收费,豆包(流式)ASR是按时收费
|
||||
2. 一般来说按次收费的更便宜,但是豆包(流式)ASR使用了大模型技术,效果更好
|
||||
3. 需要在火山引擎控制台创建应用并获取appid和access_token
|
||||
4. 支持中文语音识别
|
||||
5. 需要网络连接
|
||||
6. 输出文件保存在tmp/目录
|
||||
申请步骤:
|
||||
1. 访问 https://console.volcengine.com/speech/app
|
||||
2. 创建新应用
|
||||
3. 获取appid和access_token
|
||||
4. 填入配置文件中
|
||||
如需设置热词,请参考:https://www.volcengine.com/docs/6561/155738
|
||||
如开启多语种识别模式,请设置language当该键为空时,该模型支持中英文、上海话、闽南语,四川、陕西、粤语识别。其他语种请参考:https://www.volcengine.com/docs/6561/1354869
|
||||
' WHERE `id` = 'ASR_DoubaoStreamASR';
|
||||
|
||||
-- 更新豆包流式ASR模型配置,增加enable_multilingual默认值
|
||||
UPDATE `ai_model_config` SET
|
||||
`config_json` = JSON_SET(
|
||||
`config_json`,
|
||||
'$.enable_multilingual', false,
|
||||
'$.language', 'zh-CN'
|
||||
)
|
||||
WHERE `id` = 'ASR_DoubaoStreamASR'
|
||||
AND JSON_EXTRACT(`config_json`, '$.enable_multilingual') IS NULL
|
||||
AND JSON_EXTRACT(`config_json`, '$.language') IS NULL;
|
||||
|
||||
|
||||
-- 更新HuoshanDoubleStreamTTS供应器配置,增加多情感音色参数
|
||||
UPDATE `ai_model_provider`
|
||||
SET `fields` = '[{"key": "ws_url", "type": "string", "label": "WebSocket地址"}, {"key": "appid", "type": "string", "label": "应用ID"}, {"key": "access_token", "type": "string", "label": "访问令牌"}, {"key": "resource_id", "type": "string", "label": "资源ID"}, {"key": "speaker", "type": "string", "label": "默认音色"}, {"key": "enable_ws_reuse", "type": "boolean", "label": "是否开启链接复用", "default": true}, {"key": "speech_rate", "type": "number", "label": "语速(-50~100)"}, {"key": "loudness_rate", "type": "number", "label": "音量(-50~100)"}, {"key": "pitch", "type": "number", "label": "音高(-12~12)"}, {"key": "emotion_scale", "type": "number", "label": "情感强度(1-5)"}, {"key": "emotion", "type": "string", "label": "情感类型"}]'
|
||||
WHERE `id` = 'SYSTEM_TTS_HSDSTTS';
|
||||
|
||||
-- 更新默认值
|
||||
UPDATE `ai_model_config` SET
|
||||
`config_json` = JSON_SET(
|
||||
`config_json`,
|
||||
'$.emotion', 'neutral',
|
||||
'$.emotion_scale', 4
|
||||
)
|
||||
WHERE `id` = 'TTS_HuoshanDoubleStreamTTS'
|
||||
AND JSON_EXTRACT(`config_json`, '$.emotion') IS NULL
|
||||
AND JSON_EXTRACT(`config_json`, '$.emotion_scale') IS NULL;
|
||||
|
||||
-- 增加文档链接和备注
|
||||
UPDATE `ai_model_config` SET
|
||||
`doc_link` = 'https://console.volcengine.com/speech/service/10007',
|
||||
`remark` = '火山引擎语音合成服务配置说明:
|
||||
1. 访问 https://www.volcengine.com/ 注册并开通火山引擎账号
|
||||
2. 访问 https://console.volcengine.com/speech/service/10007 开通语音合成大模型,购买音色
|
||||
3. 在页面底部获取appid和access_token
|
||||
5. 资源ID固定为:volc.service_type.10029(大模型语音合成及混音)
|
||||
6. 链接复用:开启WebSocket连接复用,默认true减少链接损耗(注意:复用后设备处于聆听状态时空闲链接会占并发数)
|
||||
7. 语速:-50~100,可不填,正常默认值0,可填-50~100
|
||||
8. 音量:-50~100,可不填,正常默认值0,可填-50~100
|
||||
9. 音高:-12~12,可不填,正常默认值0,可填-12~12
|
||||
10. 多情感参数(当前仅部分音色支持设置情感):
|
||||
相关音色列表:https://www.volcengine.com/docs/6561/1257544
|
||||
- emotion_scale:情感强度,可选值为:1~5,默认值为4
|
||||
- emotion:情感类型,可选值为:neutral、happy、sad、angry、fearful、disgusted、surprised
|
||||
' WHERE `id` = 'TTS_HuoshanDoubleStreamTTS';
|
||||
@@ -0,0 +1,11 @@
|
||||
-- 更改FunASRServer说明文档模型为SenseVoiceSmall
|
||||
UPDATE `ai_model_config` SET
|
||||
`doc_link` = 'https://github.com/modelscope/FunASR/blob/main/runtime/docs/SDK_advanced_guide_online_zh.md',
|
||||
`remark` = '独立部署FunASR,使用FunASR的API服务,只需要五句话
|
||||
第一句:mkdir -p ./funasr-runtime-resources/models
|
||||
第二句:sudo docker run -d -p 10096:10095 --privileged=true -v $PWD/funasr-runtime-resources/models:/workspace/models registry.cn-hangzhou.aliyuncs.com/funasr_repo/funasr:funasr-runtime-sdk-online-cpu-0.1.12
|
||||
上一句话执行后会进入到容器,继续第三句:cd FunASR/runtime
|
||||
不要退出容器,继续在容器中执行第四句:nohup bash run_server_2pass.sh --download-model-dir /workspace/models --vad-dir damo/speech_fsmn_vad_zh-cn-16k-common-onnx --model-dir iic/SenseVoiceSmall-onnx --online-model-dir damo/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-online-onnx --punc-dir damo/punc_ct-transformer_zh-cn-common-vad_realtime-vocab272727-onnx --lm-dir damo/speech_ngram_lm_zh-cn-ai-wesp-fst --itn-dir thuduj12/fst_itn_zh --hotword /workspace/models/hotwords.txt > log.txt 2>&1 &
|
||||
上一句话执行后会进入到容器,继续第五句:tail -f log.txt
|
||||
第五句话执行完后,会看到模型下载日志,下载完后就可以连接使用了
|
||||
以上是使用CPU推理,如果有GPU,详细参考:https://github.com/modelscope/FunASR/blob/main/runtime/docs/SDK_advanced_guide_online_zh.md' WHERE `id` = 'ASR_FunASRServer';
|
||||
@@ -0,0 +1,31 @@
|
||||
-- 批量清理 ai_model_provider 中的 sample_rate 字段定义
|
||||
UPDATE `ai_model_provider` ap
|
||||
JOIN (
|
||||
SELECT
|
||||
id,
|
||||
JSON_ARRAYAGG(
|
||||
JSON_OBJECT('key', jt.k, 'label', jt.l, 'type', jt.t)
|
||||
) AS new_fields
|
||||
FROM `ai_model_provider`,
|
||||
JSON_TABLE(`fields`, '$[*]' COLUMNS (
|
||||
k VARCHAR(50) PATH '$.key',
|
||||
l VARCHAR(100) PATH '$.label',
|
||||
t VARCHAR(20) PATH '$.type'
|
||||
)) AS jt
|
||||
WHERE `model_type` = 'TTS'
|
||||
AND jt.k != 'sample_rate'
|
||||
GROUP BY id
|
||||
) filtered ON ap.id = filtered.id
|
||||
SET ap.fields = filtered.new_fields;
|
||||
|
||||
-- 清理 config_json 顶层的 sample_rate
|
||||
UPDATE `ai_model_config`
|
||||
SET `config_json` = JSON_REMOVE(`config_json`, '$.sample_rate')
|
||||
WHERE `model_type` = 'TTS'
|
||||
AND JSON_EXTRACT(`config_json`, '$.sample_rate') IS NOT NULL;
|
||||
|
||||
-- 清理Minimax流式TTS的sample_rate参数(位于audio_setting内部)
|
||||
UPDATE `ai_model_config` SET
|
||||
`config_json` = JSON_SET(`config_json`, '$.audio_setting', JSON_REMOVE(JSON_EXTRACT(`config_json`, '$.audio_setting'), '$.sample_rate'))
|
||||
WHERE `id` = 'TTS_MinimaxStreamTTS'
|
||||
AND JSON_EXTRACT(`config_json`, '$.audio_setting.sample_rate') IS NOT NULL;
|
||||
@@ -0,0 +1,87 @@
|
||||
-- 更新HuoshanDoubleStreamTTS供应器配置,将分散的参数改为JSON字典配置
|
||||
-- 将 speech_rate, loudness_rate, pitch, emotion, emotion_scale 等参数整合为 audio_params, additions, mix_speaker 三个JSON字典
|
||||
|
||||
UPDATE `ai_model_provider`
|
||||
SET `fields` = '[
|
||||
{"key": "ws_url", "type": "string", "label": "WebSocket地址"},
|
||||
{"key": "appid", "type": "string", "label": "应用ID"},
|
||||
{"key": "access_token", "type": "string", "label": "访问令牌"},
|
||||
{"key": "resource_id", "type": "string", "label": "资源ID"},
|
||||
{"key": "speaker", "type": "string", "label": "默认音色"},
|
||||
{"key": "enable_ws_reuse", "type": "boolean", "label": "是否开启链接复用", "default": true},
|
||||
{"key": "audio_params", "type": "dict", "label": "音频输出配置"},
|
||||
{"key": "additions", "type": "dict", "label": "高级文本处理配置"},
|
||||
{"key": "mix_speaker", "type": "dict", "label": "混音控制配置"}
|
||||
]'
|
||||
WHERE `id` = 'SYSTEM_TTS_HSDSTTS';
|
||||
|
||||
-- 更新现有配置,将旧的分散参数迁移到新的JSON字典结构
|
||||
UPDATE `ai_model_config`
|
||||
SET `config_json` = JSON_SET(
|
||||
`config_json`,
|
||||
'$.audio_params', JSON_OBJECT(
|
||||
'speech_rate', CAST(COALESCE(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(`config_json`, '$.speech_rate')), ''), '0') AS SIGNED),
|
||||
'loudness_rate', CAST(COALESCE(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(`config_json`, '$.loudness_rate')), ''), '0') AS SIGNED)
|
||||
),
|
||||
'$.additions', JSON_OBJECT(
|
||||
'aigc_metadata', JSON_OBJECT(),
|
||||
'cache_config', JSON_OBJECT(),
|
||||
'post_process', JSON_OBJECT(
|
||||
'pitch', CAST(COALESCE(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(`config_json`, '$.pitch')), ''), '0') AS SIGNED)
|
||||
)
|
||||
),
|
||||
'$.mix_speaker', JSON_OBJECT()
|
||||
)
|
||||
WHERE `id` = 'TTS_HuoshanDoubleStreamTTS';
|
||||
|
||||
-- 删除旧的分散参数字段
|
||||
UPDATE `ai_model_config`
|
||||
SET `config_json` = JSON_REMOVE(
|
||||
`config_json`,
|
||||
'$.speech_rate',
|
||||
'$.loudness_rate',
|
||||
'$.pitch',
|
||||
'$.emotion',
|
||||
'$.emotion_scale'
|
||||
)
|
||||
WHERE `id` = 'TTS_HuoshanDoubleStreamTTS';
|
||||
|
||||
-- 更新文档链接和备注说明
|
||||
UPDATE `ai_model_config` SET
|
||||
`doc_link` = 'https://www.volcengine.com/docs/6561/1329505',
|
||||
`remark` = '火山引擎双向流式TTS配置说明:
|
||||
1. 访问 https://www.volcengine.com/ 注册并开通火山引擎账号
|
||||
2. 访问 https://console.volcengine.com/speech/service/10007 开通语音合成大模型,购买音色
|
||||
3. 在页面底部获取appid和access_token
|
||||
4. 资源ID固定为:volc.service_type.10029(大模型语音合成及混音)
|
||||
5. 链接复用:开启WebSocket连接复用,默认true减少链接损耗(注意:复用后设备处于聆听状态时空闲链接会占并发数)
|
||||
|
||||
详细参数文档:https://www.volcengine.com/docs/6561/1329505
|
||||
【audio_params】音频输出配置 - 用户可自定义添加火山引擎支持的任何音频参数
|
||||
- speech_rate: 语速(-50~100),默认0
|
||||
- loudness_rate: 音量(-50~100),默认0
|
||||
- emotion: 情感类型(仅部分音色支持),可选值:neutral、happy、sad、angry、fearful、disgusted、surprised
|
||||
- emotion_scale: 情感强度(1~5),默认4
|
||||
示例:{"speech_rate": 10, "loudness_rate": 5, "emotion": "happy", "emotion_scale": 4}
|
||||
|
||||
【additions】高级文本处理配置 - 用户可自定义添加火山引擎支持的任何高级参数
|
||||
- post_process.pitch: 音高(-12~12),默认0
|
||||
- aigc_metadata: AIGC元数据配置
|
||||
- cache_config: 缓存配置
|
||||
示例:{"post_process": {"pitch": 2}, "aigc_metadata": {}, "cache_config": {}}
|
||||
|
||||
【mix_speaker】混音控制配置 - 多音色混合(仅 TTS 1.0)
|
||||
示例:
|
||||
{"speakers": [
|
||||
{"source_speaker": "zh_male_bvlazysheep","mix_factor": 0.3},
|
||||
{"source_speaker": "BV120_streaming","mix_factor": 0.3},
|
||||
{"source_speaker": "zh_male_ahu_conversation_wvae_bigtts","mix_factor": 0.4}
|
||||
]}
|
||||
|
||||
注意:
|
||||
- 多情感音色参数(emotion、emotion_scale)仅部分音色支持
|
||||
- 相关音色列表:https://www.volcengine.com/docs/6561/1257544
|
||||
- 用户可根据火山引擎API文档自行添加更多参数
|
||||
- 混音功能主要适用于豆包语音合成模型1.0的音色,使用时需要将req_params.speaker设置为custom_mix_bigtts
|
||||
'
|
||||
WHERE `id` = 'TTS_HuoshanDoubleStreamTTS';
|
||||
@@ -0,0 +1,14 @@
|
||||
-- 更新小智参数中的默认采样率从 16000 改为 24000
|
||||
UPDATE `sys_params`
|
||||
SET `param_value` = '{
|
||||
"type": "hello",
|
||||
"version": 1,
|
||||
"transport": "websocket",
|
||||
"audio_params": {
|
||||
"format": "opus",
|
||||
"sample_rate": 24000,
|
||||
"channels": 1,
|
||||
"frame_duration": 60
|
||||
}
|
||||
}'
|
||||
WHERE `id` = 309 AND `param_code` = 'xiaozhi';
|
||||
@@ -423,3 +423,88 @@ databaseChangeLog:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202511131023.sql
|
||||
- changeSet:
|
||||
id: 202511221450
|
||||
author: RanChen
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202511221450.sql
|
||||
- changeSet:
|
||||
id: 202512031517
|
||||
author: rainv123
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202512031517.sql
|
||||
|
||||
- changeSet:
|
||||
id: 202512041515
|
||||
author: cgd
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202512041515.sql
|
||||
- changeSet:
|
||||
id: 202512131453
|
||||
author: hrz
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202512131453.sql
|
||||
- changeSet:
|
||||
id: 202512161529
|
||||
author: RanChen
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202512161529.sql
|
||||
- changeSet:
|
||||
id: 202512192245
|
||||
author: hrz
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202512192245.sql
|
||||
- changeSet:
|
||||
id: 202512221117
|
||||
author: RanChen
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202512221117.sql
|
||||
- changeSet:
|
||||
id: 202512301430
|
||||
author: RanChen
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202512301430.sql
|
||||
- changeSet:
|
||||
id: 202601051433
|
||||
author: RanChen
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202601051433.sql
|
||||
- changeSet:
|
||||
id: 202601141645
|
||||
author: RanChen
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202601141645.sql
|
||||
- changeSet:
|
||||
id: 202601231530
|
||||
author: RanChen
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202601231530.sql
|
||||
- changeSet:
|
||||
id: 202601261730
|
||||
author: RanChen
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202601261730.sql
|
||||
|
||||
@@ -189,4 +189,16 @@
|
||||
10180=\u6587\u4EF6\u5185\u5BB9\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10181=\u97F3\u8272\u514B\u9686\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10182=\u97F3\u8272\u514B\u9686\u97F3\u9891\u4E0D\u5B58\u5728
|
||||
10183=\u9ED8\u8BA4\u667A\u80FD\u4F53\u672A\u627E\u5230
|
||||
10183=\u9ED8\u8BA4\u667A\u80FD\u4F53\u672A\u627E\u5230
|
||||
10184=\u4E0D\u652F\u6301\u7684\u9002\u914D\u5668\u7C7B\u578B
|
||||
10185=RAG\u914D\u7F6E\u9A8C\u8BC1\u5931\u8D25
|
||||
10186=\u9002\u914D\u5668\u521B\u5EFA\u5931\u8D25
|
||||
10187=\u9002\u914D\u5668\u521D\u59CB\u5316\u5931\u8D25
|
||||
10188=\u9002\u914D\u5668\u8FDE\u63A5\u6D4B\u8BD5\u5931\u8D25
|
||||
10189=\u9002\u914D\u5668\u64CD\u4F5C\u5931\u8D25
|
||||
10190=\u9002\u914D\u5668\u672A\u627E\u5230
|
||||
10191=\u9002\u914D\u5668\u7F13\u5B58\u9519\u8BEF
|
||||
10192=\u9002\u914D\u5668\u7C7B\u578B\u672A\u627E\u5230
|
||||
10193=\u8BBE\u5907ID\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10194=\u8BBE\u5907\u4E0D\u5B58\u5728\u6216\u4E0D\u5728\u7EBF
|
||||
10195=OTA\u4E0A\u4F20\u6B21\u6570\u8D85\u8FC7\u9650\u5236
|
||||
@@ -8,23 +8,23 @@
|
||||
10004=Die Kontonummer oder das Passwort ist falsch
|
||||
10005=Konto wurde deaktiviert
|
||||
10006=Eindeutige ID darf nicht leer sein
|
||||
10007=Der Bestätigungscode ist falsch
|
||||
10008=Zuerst Untermenü oder Schaltfläche löschen
|
||||
10009=Das ursprüngliche Passwort ist falsch
|
||||
10010=Das Konto oder Passwort ist falsch, Sie können es noch {0} Mal versuchen
|
||||
10007=Der Best\u00E4tigungscode ist falsch
|
||||
10008=Zuerst Untermen\u00FC oder Schaltfl\u00E4che l\u00F6schen
|
||||
10009=Das urspr\u00FCngliche Passwort ist falsch
|
||||
10010=Das Konto oder Passwort ist falsch, Sie k\u00F6nnen es noch {0} Mal versuchen
|
||||
|
||||
10011=Die übergeordnete Abteilung hat eine falsche Wahl getroffen
|
||||
10012=Oberes Menü kann nicht sich selbst sein
|
||||
10011=Die \u00FCbergeordnete Abteilung hat eine falsche Wahl getroffen
|
||||
10012=Oberes Men\u00FC kann nicht sich selbst sein
|
||||
10013=Datenberechtigungsschnittstelle, die nur ein Map-Typ-Parameter sein kann
|
||||
10014=Bitte löschen Sie zuerst die untergeordnete Abteilung
|
||||
10015=Bitte löschen Sie zuerst den Benutzer unter der Abteilung
|
||||
10014=Bitte l\u00F6schen Sie zuerst die untergeordnete Abteilung
|
||||
10015=Bitte l\u00F6schen Sie zuerst den Benutzer unter der Abteilung
|
||||
10016=Bereitstellung fehlgeschlagen, kein Prozess
|
||||
10017=Das ModellDiagramm ist falsch, bitte überprüfen
|
||||
10017=Das ModellDiagramm ist falsch, bitte \u00FCberpr\u00FCfen
|
||||
10018=Export fehlgeschlagen, Modell-ID ist {0}
|
||||
|
||||
10019=Bitte laden Sie eine Datei hoch
|
||||
10020=Token darf nicht leer sein
|
||||
10021=Token ist ungültig, bitte melden Sie sich erneut an
|
||||
10021=Token ist ung\u00FCltig, bitte melden Sie sich erneut an
|
||||
10022=Das Konto wurde gesperrt
|
||||
10023=Bitte laden Sie Dateien im Format zip, bar, bpmn, bpmn20.xml hoch
|
||||
|
||||
@@ -37,25 +37,25 @@
|
||||
10029=Darf keine illegalen Zeichen enthalten
|
||||
|
||||
10030=Das Passwort hat weniger als {0} Ziffern
|
||||
10031=Das Passwort muss gleichzeitig aus Zahlen, Groß- und Kleinbuchstaben sowie Sonderzeichen bestehen
|
||||
10032=Ausnahme beim Löschen dieser Daten
|
||||
10033=Gerätebestätigungscode Fehler
|
||||
10031=Das Passwort muss gleichzeitig aus Zahlen, Gro\u00DF- und Kleinbuchstaben sowie Sonderzeichen bestehen
|
||||
10032=Ausnahme beim L\u00F6schen dieser Daten
|
||||
10033=Ger\u00E4tebest\u00E4tigungscode Fehler
|
||||
|
||||
10034=Parameterwert darf nicht leer sein
|
||||
10035=Parametertyp darf nicht leer sein
|
||||
10036=Nicht unterstützter Parametertyp
|
||||
10037=Parameterwert muss eine gültige Zahl sein
|
||||
10036=Nicht unterst\u00FCtzter Parametertyp
|
||||
10037=Parameterwert muss eine g\u00FCltige Zahl sein
|
||||
10038=Parameterwert muss true oder false sein
|
||||
10039=Parameterwert muss ein gültiges JSON-Array-Format sein
|
||||
10040=Parameterwert muss ein gültiges JSON-Format sein
|
||||
10039=Parameterwert muss ein g\u00FCltiges JSON-Array-Format sein
|
||||
10040=Parameterwert muss ein g\u00FCltiges JSON-Format sein
|
||||
|
||||
10041=Gerät nicht gefunden
|
||||
10041=Ger\u00E4t nicht gefunden
|
||||
10042={0}
|
||||
10043=Löschen der Daten fehlgeschlagen
|
||||
10043=L\u00F6schen der Daten fehlgeschlagen
|
||||
10044=Benutzer nicht angemeldet
|
||||
10045=WebSocket-Verbindung fehlgeschlagen oder Zeitüberschreitung
|
||||
10045=WebSocket-Verbindung fehlgeschlagen oder Zeit\u00FCberschreitung
|
||||
10046=Stimmabdruck speichern fehlgeschlagen, bitte Administrator kontaktieren
|
||||
10047=Tägliches SendeLimit erreicht
|
||||
10047=T\u00E4gliches SendeLimit erreicht
|
||||
10048=Altes Passwort ist falsch
|
||||
10049=Das eingestellte LLM ist nicht openai oder ollama
|
||||
10050=Token-Generierung fehlgeschlagen
|
||||
@@ -65,45 +65,45 @@
|
||||
10054=Stimmabdruck-Schnittstelle nicht konfiguriert, bitte konfigurieren Sie zuerst die Stimmabdruck-Schnittstellenadresse (server.voice_print) in der Parameterkonfiguration
|
||||
10055=SMS-Sendung fehlgeschlagen
|
||||
10056=SMS-Verbindung konnte nicht hergestellt werden
|
||||
10057=Stimmabdruck für Agent konnte nicht erstellt werden
|
||||
10058=Stimmabdruck für Agent konnte nicht aktualisiert werden
|
||||
10059=Stimmabdruck für Agent konnte nicht gelöscht werden
|
||||
10060=Sendung zu häufig, bitte versuchen Sie es nach {0} Sekunden erneut
|
||||
10057=Stimmabdruck f\u00FCr Agent konnte nicht erstellt werden
|
||||
10058=Stimmabdruck f\u00FCr Agent konnte nicht aktualisiert werden
|
||||
10059=Stimmabdruck f\u00FCr Agent konnte nicht gel\u00F6scht werden
|
||||
10060=Sendung zu h\u00E4ufig, bitte versuchen Sie es nach {0} Sekunden erneut
|
||||
10061=Aktivierungscode darf nicht leer sein
|
||||
10062=Aktivierungscode Fehler
|
||||
10063=Gerät bereits aktiviert
|
||||
10063=Ger\u00E4t bereits aktiviert
|
||||
10064=Dieses Modell ist das Standardmodell, bitte setzen Sie zuerst ein anderes Modell als Standard
|
||||
10065=Hinzufügen der Daten fehlgeschlagen
|
||||
10065=Hinzuf\u00FCgen der Daten fehlgeschlagen
|
||||
10066=Aktualisieren der Daten fehlgeschlagen
|
||||
10067=Grafischer Bestätigungscode Fehler
|
||||
10068=Mobilregistrierung nicht aktiviert, SMS-Bestätigungscode-Funktion kann nicht verwendet werden
|
||||
10067=Grafischer Best\u00E4tigungscode Fehler
|
||||
10068=Mobilregistrierung nicht aktiviert, SMS-Best\u00E4tigungscode-Funktion kann nicht verwendet werden
|
||||
10069=Benutzername ist keine Handynummer, bitte erneut eingeben
|
||||
10070=Diese Handynummer wurde bereits registriert
|
||||
10071=Die eingegebene Handynummer ist nicht registriert
|
||||
10072=Normale Benutzerregistrierung derzeit nicht erlaubt
|
||||
10073=Mobilregistrierung nicht aktiviert, Passwort-Wiederherstellungsfunktion kann nicht verwendet werden
|
||||
10074=Das Format der eingegebenen Handynummer ist falsch
|
||||
10075=Der eingegebene SMS-Bestätigungscode ist falsch
|
||||
10076=Wörterbuchtyp existiert nicht
|
||||
10077=Wörterbuchtypcode ist dupliziert
|
||||
10075=Der eingegebene SMS-Best\u00E4tigungscode ist falsch
|
||||
10076=W\u00F6rterbuchtyp existiert nicht
|
||||
10077=W\u00F6rterbuchtypcode ist dupliziert
|
||||
10078=Lesen der Ressource fehlgeschlagen
|
||||
10079=LLM-Modell und Absichtserkennung, Parameterauswahl stimmt nicht überein
|
||||
10080=Dieser Stimmabdruck gehört zu {0}, der bereits registriert ist, bitte wählen Sie eine andere Stimme
|
||||
10081=Fehler beim Löschen des Stimmabdrucks aufgetreten
|
||||
10082=Änderung nicht erlaubt, diese Stimme wurde bereits als Stimmabdruck registriert ({0})
|
||||
10083=Fehler beim Ändern des Stimmabdrucks, bitte Administrator kontaktieren
|
||||
10084=Stimmabdruck-Schnittstellenadresse Fehler, bitte gehen Sie zur Parameterverwaltung, um die Stimmabdruck-Schnittstellenadresse zu ändern
|
||||
10085=Audiodaten gehören nicht zu diesem Agent
|
||||
10086=Audiodaten sind leer, bitte überprüfen Sie hochgeladene Daten
|
||||
10079=LLM-Modell und Absichtserkennung, Parameterauswahl stimmt nicht \u00FCberein
|
||||
10080=Dieser Stimmabdruck geh\u00F6rt zu {0}, der bereits registriert ist, bitte w\u00E4hlen Sie eine andere Stimme
|
||||
10081=Fehler beim L\u00F6schen des Stimmabdrucks aufgetreten
|
||||
10082=\u00C4nderung nicht erlaubt, diese Stimme wurde bereits als Stimmabdruck registriert ({0})
|
||||
10083=Fehler beim \u00C4ndern des Stimmabdrucks, bitte Administrator kontaktieren
|
||||
10084=Stimmabdruck-Schnittstellenadresse Fehler, bitte gehen Sie zur Parameterverwaltung, um die Stimmabdruck-Schnittstellenadresse zu \u00E4ndern
|
||||
10085=Audiodaten geh\u00F6ren nicht zu diesem Agent
|
||||
10086=Audiodaten sind leer, bitte \u00FCberpr\u00FCfen Sie hochgeladene Daten
|
||||
10087=Stimmabdruck-Registrierung fehlgeschlagen, Anfrage nicht erfolgreich
|
||||
10088=Stimmabdruck-Registrierung fehlgeschlagen, Anfrageverarbeitung fehlgeschlagen
|
||||
10089=Stimmabdruck-Stornierung fehlgeschlagen, Anfrage nicht erfolgreich
|
||||
10090=Stimmabdruck-Stornierung fehlgeschlagen, Anfrageverarbeitung fehlgeschlagen
|
||||
10091=Modellanbieter existiert nicht
|
||||
10092=Das konfigurierte LLM existiert nicht
|
||||
10093=Diese Modellkonfiguration wird von Agent {0} referenziert und kann nicht gelöscht werden
|
||||
10094=Dieses LLM-Modell wird von der Absichtserkennungskonfiguration referenziert und kann nicht gelöscht werden
|
||||
10095=Ungültige Serveroperation
|
||||
10093=Diese Modellkonfiguration wird von Agent {0} referenziert und kann nicht gel\u00F6scht werden
|
||||
10094=Dieses LLM-Modell wird von der Absichtserkennungskonfiguration referenziert und kann nicht gel\u00F6scht werden
|
||||
10095=Ung\u00FCltige Serveroperation
|
||||
10096=Server WebSocket-Adresse nicht konfiguriert
|
||||
10097=Ziel-WebSocket-Adresse existiert nicht
|
||||
10098=WebSocket-Adressliste darf nicht leer sein
|
||||
@@ -115,47 +115,47 @@
|
||||
10104=OTA-Adresse muss mit http oder https beginnen
|
||||
10105=OTA-Adresse muss mit /ota/ enden
|
||||
10106=OTA-Schnittstellenzugriff fehlgeschlagen
|
||||
10107=OTA-Schnittstelle Rückgabeinhaltformat ist falsch
|
||||
10107=OTA-Schnittstelle R\u00FCckgabeinhaltformat ist falsch
|
||||
10108=OTA-Schnittstellenvalidierung fehlgeschlagen
|
||||
10109=MCP-Adresse darf nicht leer sein
|
||||
10110=MCP-Adresse darf nicht localhost oder 127.0.0.1 verwenden
|
||||
10111=Keine gültige MCP-Adresse
|
||||
10111=Keine g\u00FCltige MCP-Adresse
|
||||
10112=MCP-Schnittstellenzugriff fehlgeschlagen
|
||||
10113=MCP-Schnittstelle Rückgabeinhaltformat ist falsch
|
||||
10113=MCP-Schnittstelle R\u00FCckgabeinhaltformat ist falsch
|
||||
10114=MCP-Schnittstellenvalidierung fehlgeschlagen
|
||||
10115=Stimmabdruck-Schnittstellenadresse darf nicht leer sein
|
||||
10116=Stimmabdruck-Schnittstellenadresse darf nicht localhost oder 127.0.0.1 verwenden
|
||||
10117=Keine gültige Stimmabdruck-Schnittstellenadresse
|
||||
10117=Keine g\u00FCltige Stimmabdruck-Schnittstellenadresse
|
||||
10118=Stimmabdruck-Schnittstellenadresse muss mit http oder https beginnen
|
||||
10119=Stimmabdruck-Schnittstellenzugriff fehlgeschlagen
|
||||
10120=Stimmabdruck-Schnittstelle Rückgabeinhaltformat ist falsch
|
||||
10120=Stimmabdruck-Schnittstelle R\u00FCckgabeinhaltformat ist falsch
|
||||
10121=Stimmabdruck-Schnittstellenvalidierung fehlgeschlagen
|
||||
10122=MQTT-Geheimnis darf nicht leer sein
|
||||
10123=Ihr MQTT-Geheimnis ist nicht sicher, es muss mindestens 8 Zeichen lang sein und muss sowohl Groß- als auch Kleinbuchstaben enthalten
|
||||
10124=Ihr MQTT-Geheimnis ist nicht sicher, MQTT-Geheimnis muss sowohl Groß- als auch Kleinbuchstaben enthalten
|
||||
10125=Ihr MQTT-Geheimnis enthält schwaches Passwort
|
||||
10128=Wörterbuchlabel ist dupliziert
|
||||
10129=SM2-Schlüssel nicht konfiguriert
|
||||
10130=SM2-Entschlüsselung fehlgeschlagen
|
||||
10131=modelType und provideCode dürfen nicht leer sein
|
||||
10123=Ihr MQTT-Geheimnis ist nicht sicher, es muss mindestens 8 Zeichen lang sein und muss sowohl Gro\u00DF- als auch Kleinbuchstaben enthalten
|
||||
10124=Ihr MQTT-Geheimnis ist nicht sicher, MQTT-Geheimnis muss sowohl Gro\u00DF- als auch Kleinbuchstaben enthalten
|
||||
10125=Ihr MQTT-Geheimnis enth\u00E4lt schwaches Passwort
|
||||
10128=W\u00F6rterbuchlabel ist dupliziert
|
||||
10129=SM2-Schl\u00FCssel nicht konfiguriert
|
||||
10130=SM2-Entschl\u00FCsselung fehlgeschlagen
|
||||
10131=modelType und provideCode d\u00FCrfen nicht leer sein
|
||||
10132=Keine Berechtigung, den Chat-Verlauf dieses Agents anzuzeigen
|
||||
10133=Sitzungs-ID darf nicht leer sein
|
||||
10134=Agent-ID darf nicht leer sein
|
||||
10135=Chat-Verlauf-Download fehlgeschlagen
|
||||
10136=Download-Link abgelaufen oder ungültig
|
||||
10137=Download-Link ungültig
|
||||
10136=Download-Link abgelaufen oder ung\u00FCltig
|
||||
10137=Download-Link ung\u00FCltig
|
||||
10138=Benutzer
|
||||
10139=Agent
|
||||
10140=Audio-Datei darf nicht leer sein
|
||||
10141=Nur Audio-Dateien werden unterstützt
|
||||
10142=Audio-Dateigröße darf 10MB nicht überschreiten
|
||||
10141=Nur Audio-Dateien werden unterst\u00FCtzt
|
||||
10142=Audio-Dateigr\u00F6\u00DFe darf 10MB nicht \u00FCberschreiten
|
||||
10143=Upload fehlgeschlagen
|
||||
10144=Stimmenklon-Datensatz existiert nicht
|
||||
10145=Stimmressourceninformationen dürfen nicht leer sein
|
||||
10145=Stimmressourceninformationen d\u00FCrfen nicht leer sein
|
||||
10146=TTS-Plattformname darf nicht leer sein
|
||||
10147=Stimmen-ID darf nicht leer sein
|
||||
10148=Kontoinhaber darf nicht leer sein
|
||||
10149=Zu löschende Stimmressourcen-ID darf nicht leer sein
|
||||
10149=Zu l\u00F6schende Stimmressourcen-ID darf nicht leer sein
|
||||
10150=Sie haben keine Berechtigung, diesen Datensatz zu bearbeiten
|
||||
10151=Bitte laden Sie zuerst eine Audio-Datei hoch
|
||||
10152=Modellkonfiguration nicht gefunden
|
||||
@@ -179,14 +179,26 @@
|
||||
10170=Wissensbasis-Name existiert bereits
|
||||
10171=RAG-Konfiguration base_url darf nicht leer sein
|
||||
10172=RAG-Konfiguration api_key darf nicht leer sein
|
||||
10173=RAG-Konfiguration api_key darf keinen Platzhalter enthalten, bitte ersetzen Sie mit tatsächlichem API-Schlüssel
|
||||
10173=RAG-Konfiguration api_key darf keinen Platzhalter enthalten, bitte ersetzen Sie mit tats\u00E4chlichem API-Schl\u00FCssel
|
||||
10174=RAG-Konfiguration base_url-Formatfehler, muss mit http oder https beginnen
|
||||
10175=Mac-Adresse darf nicht leer sein
|
||||
10176=RAG-Konfiguration dataset_id darf nicht leer sein
|
||||
10177=RAG-Konfiguration model_id darf nicht leer sein
|
||||
10178=RAG-Konfiguration dataset_id und model_id dürfen nicht leer sein
|
||||
10178=RAG-Konfiguration dataset_id und model_id d\u00FCrfen nicht leer sein
|
||||
10179=Dateiname darf nicht leer sein
|
||||
10180=Dateiinhalt darf nicht leer sein
|
||||
10181=Stimmenklon-Name darf nicht leer sein
|
||||
10182=Stimmenklon-Audio nicht gefunden
|
||||
10183=Standard-Agent-Vorlage nicht gefunden
|
||||
10183=Standard-Agent-Vorlage nicht gefunden
|
||||
10184=Nicht unterst\u00FCtzter Adaptertyp
|
||||
10185=RAG-Konfigurationsvalidierung fehlgeschlagen
|
||||
10186=Adapter-Erstellung fehlgeschlagen
|
||||
10187=Adapter-Initialisierung fehlgeschlagen
|
||||
10188=Adapter-Verbindungstest fehlgeschlagen
|
||||
10189=Adapter-Operation fehlgeschlagen
|
||||
10190=Adapter nicht gefunden
|
||||
10191=Adapter-Cache-Fehler
|
||||
10192=Adaptertyp nicht gefunden
|
||||
10193=Adapter-ID darf nicht leer sein
|
||||
10194=Adapter nicht gefunden oder nicht online
|
||||
10195=OTA-Upload-Anzahl \u00FCberschreitet das Limit
|
||||
@@ -189,4 +189,16 @@
|
||||
10180=File content cannot be empty
|
||||
10181=Voice clone name cannot be empty
|
||||
10182=Voice clone audio not found
|
||||
10183=Default agent template not found
|
||||
10183=Default agent template not found
|
||||
10184=Unsupported adapter type
|
||||
10185=RAG configuration validation failed
|
||||
10186=Adapter creation failed
|
||||
10187=Adapter initialization failed
|
||||
10188=Adapter connection test failed
|
||||
10189=Adapter operation failed
|
||||
10190=Adapter not found
|
||||
10191=Adapter cache error
|
||||
10192=Adapter type not found
|
||||
10193=Device ID cannot be empty
|
||||
10194=Device not found or not online
|
||||
10195=OTA upload times exceed the limit
|
||||
@@ -1,192 +1,204 @@
|
||||
#Tiếng Việt
|
||||
500=Ngoại lệ nội bộ máy chủ
|
||||
401=Không được ủy quyền
|
||||
403=Truy cập bị từ chối, không có quyền
|
||||
10001={0} không thể để trống
|
||||
10002=Bản ghi đã tồn tại trong cơ sở dữ liệu
|
||||
10003=Không thể lấy tham số
|
||||
10004=Số tài khoản hoặc mật khẩu không chính xác
|
||||
10005=Tài khoản đã bị vô hiệu hóa
|
||||
10006=ID duy nhất không thể để trống
|
||||
10007=Mã xác minh không chính xác
|
||||
10008=Trước tiên hãy xóa menu con hoặc nút
|
||||
10009=Mật khẩu gốc không chính xác
|
||||
10010=Tài khoản hoặc mật khẩu không chính xác, bạn có thể thử thêm {0} lần nữa
|
||||
#Ti\u1EBFng Vi\u1EC7t
|
||||
500=Ngo\u1EA1i l\u1EC7 n\u1ED9i b\u1ED9 m\u00E1y ch\u1EE7
|
||||
401=Kh\u00F4ng \u0111\u01B0\u1EE3c \u1EE7y quy\u1EC1n
|
||||
403=Truy c\u1EADp b\u1ECB t\u1EEB ch\u1ED1i, kh\u00F4ng c\u00F3 quy\u1EC1n
|
||||
10001={0} kh\u00F4ng th\u1EC3 \u0111\u1EC3 tr\u1ED1ng
|
||||
10002=B\u1EA3n ghi \u0111\u00E3 t\u1ED3n t\u1EA1i trong c\u01A1 s\u1EDF d\u1EEF li\u1EC7u
|
||||
10003=Kh\u00F4ng th\u1EC3 l\u1EA5y tham s\u1ED1
|
||||
10004=S\u1ED1 t\u00E0i kho\u1EA3n ho\u1EB7c m\u1EADt kh\u1EA9u kh\u00F4ng ch\u00EDnh x\u00E1c
|
||||
10005=T\u00E0i kho\u1EA3n \u0111\u00E3 b\u1ECB v\u00F4 hi\u1EC7u h\u00F3a
|
||||
10006=ID duy nh\u1EA5t kh\u00F4ng th\u1EC3 \u0111\u1EC3 tr\u1ED1ng
|
||||
10007=M\u00E3 x\u00E1c minh kh\u00F4ng ch\u00EDnh x\u00E1c
|
||||
10008=Tr\u01B0\u1EDBc ti\u00EAn h\u00E3y x\u00F3a menu con ho\u1EB7c n\u00FAt
|
||||
10009=M\u1EADt kh\u1EA9u g\u1ED1c kh\u00F4ng ch\u00EDnh x\u00E1c
|
||||
10010=T\u00E0i kho\u1EA3n ho\u1EB7c m\u1EADt kh\u1EA9u kh\u00F4ng ch\u00EDnh x\u00E1c, b\u1EA1n c\u00F3 th\u1EC3 th\u1EED th\u00EAm {0} l\u1EA7n n\u1EEFa
|
||||
|
||||
10011=Phòng ban cấp trên đã lựa chọn sai
|
||||
10012=Menu trên không thể là chính nó
|
||||
10013=Giao diện quyền dữ liệu, chỉ có thể là tham số kiểu Map
|
||||
10014=Vui lòng xóa phòng ban cấp dưới trước
|
||||
10015=Vui lòng xóa người dùng thuộc phòng ban trước
|
||||
10016=Triển khai thất bại, không có quy trình
|
||||
10017=Biểu đồ mô hình không chính xác, vui lòng kiểm tra
|
||||
10018=Xuất thất bại, ID mô hình là {0}
|
||||
10011=Ph\u00F2ng ban c\u1EA5p tr\u00EAn \u0111\u00E3 l\u1EF1a ch\u1ECDn sai
|
||||
10012=Menu tr\u00EAn kh\u00F4ng th\u1EC3 l\u00E0 ch\u00EDnh n\u00F3
|
||||
10013=Giao di\u1EC7n quy\u1EC1n d\u1EEF li\u1EC7u, ch\u1EC9 c\u00F3 th\u1EC3 l\u00E0 tham s\u1ED1 ki\u1EC3u Map
|
||||
10014=Vui l\u00F2ng x\u00F3a ph\u00F2ng ban c\u1EA5p d\u01B0\u1EDBi tr\u01B0\u1EDBc
|
||||
10015=Vui l\u00F2ng x\u00F3a ng\u01B0\u1EDDi d\u00F9ng thu\u1ED9c ph\u00F2ng ban tr\u01B0\u1EDBc
|
||||
10016=Tri\u1EC3n khai th\u1EA5t b\u1EA1i, kh\u00F4ng c\u00F3 quy tr\u00ECnh
|
||||
10017=Bi\u1EC3u \u0111\u1ED3 m\u00F4 h\u00ECnh kh\u00F4ng ch\u00EDnh x\u00E1c, vui l\u00F2ng ki\u1EC3m tra
|
||||
10018=Xu\u1EA5t th\u1EA5t b\u1EA1i, ID m\u00F4 h\u00ECnh l\u00E0 {0}
|
||||
|
||||
10019=Vui lòng tải lên tệp
|
||||
10020=Token không thể để trống
|
||||
10021=Token không hợp lệ, vui lòng đăng nhập lại
|
||||
10022=Tài khoản đã bị khóa
|
||||
10023=Vui lòng tải lên tệp ở định dạng zip, bar, bpmn, bpmn20.xml
|
||||
10019=Vui l\u00F2ng t\u1EA3i l\u00EAn t\u1EC7p
|
||||
10020=Token kh\u00F4ng th\u1EC3 \u0111\u1EC3 tr\u1ED1ng
|
||||
10021=Token kh\u00F4ng h\u1EE3p l\u1EC7, vui l\u00F2ng \u0111\u0103ng nh\u1EADp l\u1EA1i
|
||||
10022=T\u00E0i kho\u1EA3n \u0111\u00E3 b\u1ECB kh\u00F3a
|
||||
10023=Vui l\u00F2ng t\u1EA3i l\u00EAn t\u1EC7p \u1EDF \u0111\u1ECBnh d\u1EA1ng zip, bar, bpmn, bpmn20.xml
|
||||
|
||||
10024=Tải lên tệp {0} thất bại
|
||||
10025=Gửi SMS {0} thất bại
|
||||
10026=Mẫu email không tồn tại
|
||||
10024=T\u1EA3i l\u00EAn t\u1EC7p {0} th\u1EA5t b\u1EA1i
|
||||
10025=G\u1EEDi SMS {0} th\u1EA5t b\u1EA1i
|
||||
10026=M\u1EABu email kh\u00F4ng t\u1ED3n t\u1EA1i
|
||||
|
||||
10027=Ngoại lệ dịch vụ Redis
|
||||
10028=Tác vụ hẹn giờ thất bại
|
||||
10029=Không thể chứa ký tự không hợp lệ
|
||||
10027=Ngo\u1EA1i l\u1EC7 d\u1ECBch v\u1EE5 Redis
|
||||
10028=T\u00E1c v\u1EE5 h\u1EB9n gi\u1EDD th\u1EA5t b\u1EA1i
|
||||
10029=Kh\u00F4ng th\u1EC3 ch\u1EE9a k\u00FD t\u1EF1 kh\u00F4ng h\u1EE3p l\u1EC7
|
||||
|
||||
10030=Mật khẩu ít hơn {0} chữ số
|
||||
10031=Mật khẩu phải bao gồm số, chữ hoa, chữ thường và ký tự đặc biệt cùng một lúc
|
||||
10032=Ngoại lệ khi xóa dữ liệu này
|
||||
10033=Lỗi mã xác minh thiết bị
|
||||
10030=M\u1EADt kh\u1EA9u \u00EDt h\u01A1n {0} ch\u1EEF s\u1ED1
|
||||
10031=M\u1EADt kh\u1EA9u ph\u1EA3i bao g\u1ED3m s\u1ED1, ch\u1EEF hoa, ch\u1EEF th\u01B0\u1EDDng v\u00E0 k\u00FD t\u1EF1 \u0111\u1EB7c bi\u1EC7t c\u00F9ng m\u1ED9t l\u00FAc
|
||||
10032=Ngo\u1EA1i l\u1EC7 khi x\u00F3a d\u1EEF li\u1EC7u n\u00E0y
|
||||
10033=L\u1ED7i m\u00E3 x\u00E1c minh thi\u1EBFt b\u1ECB
|
||||
|
||||
10034=Giá trị tham số không thể để trống
|
||||
10035=Loại tham số không thể để trống
|
||||
10036=Loại tham số không được hỗ trợ
|
||||
10037=Giá trị tham số phải là số hợp lệ
|
||||
10038=Giá trị tham số phải là true hoặc false
|
||||
10039=Giá trị tham số phải là định dạng mảng JSON hợp lệ
|
||||
10040=Giá trị tham số phải là định dạng JSON hợp lệ
|
||||
10034=Gi\u00E1 tr\u1ECB tham s\u1ED1 kh\u00F4ng th\u1EC3 \u0111\u1EC3 tr\u1ED1ng
|
||||
10035=Lo\u1EA1i tham s\u1ED1 kh\u00F4ng th\u1EC3 \u0111\u1EC3 tr\u1ED1ng
|
||||
10036=Lo\u1EA1i tham s\u1ED1 kh\u00F4ng \u0111\u01B0\u1EE3c h\u1ED7 tr\u1EE3
|
||||
10037=Gi\u00E1 tr\u1ECB tham s\u1ED1 ph\u1EA3i l\u00E0 s\u1ED1 h\u1EE3p l\u1EC7
|
||||
10038=Gi\u00E1 tr\u1ECB tham s\u1ED1 ph\u1EA3i l\u00E0 true ho\u1EB7c false
|
||||
10039=Gi\u00E1 tr\u1ECB tham s\u1ED1 ph\u1EA3i l\u00E0 \u0111\u1ECBnh d\u1EA1ng m\u1EA3ng JSON h\u1EE3p l\u1EC7
|
||||
10040=Gi\u00E1 tr\u1ECB tham s\u1ED1 ph\u1EA3i l\u00E0 \u0111\u1ECBnh d\u1EA1ng JSON h\u1EE3p l\u1EC7
|
||||
|
||||
10041=Không tìm thấy thiết bị
|
||||
10041=Kh\u00F4ng t\u00ECm th\u1EA5y thi\u1EBFt b\u1ECB
|
||||
10042={0}
|
||||
10043=Xóa dữ liệu thất bại
|
||||
10044=Người dùng chưa đăng nhập
|
||||
10045=Kết nối WebSocket thất bại hoặc hết thời gian chờ
|
||||
10046=Không thể lưu dấu giọng nói, vui lòng liên hệ quản trị viên
|
||||
10047=Đạt đến giới hạn gửi hàng ngày
|
||||
10048=Mật khẩu cũ không chính xác
|
||||
10049=LLM được đặt không phải là openai hoặc ollama
|
||||
10050=Không thể tạo token
|
||||
10051=Không tìm thấy tài nguyên
|
||||
10052=Không tìm thấy agent mặc định
|
||||
10053=Không tìm thấy agent
|
||||
10054=Giao diện dấu giọng nói chưa được cấu hình, vui lòng cấu hình địa chỉ giao diện dấu giọng nói (server.voice_print) trong cấu hình tham số trước
|
||||
10055=Gửi SMS thất bại
|
||||
10056=Không thể thiết lập kết nối SMS
|
||||
10057=Không thể tạo dấu giọng nói cho agent
|
||||
10058=Không thể cập nhật dấu giọng nói cho agent
|
||||
10059=Không thể xóa dấu giọng nói cho agent
|
||||
10060=Gửi quá thường xuyên, vui lòng thử lại sau {0} giây
|
||||
10061=Mã kích hoạt không thể để trống
|
||||
10062=Lỗi mã kích hoạt
|
||||
10063=Thiết bị đã được kích hoạt
|
||||
10064=Mô hình này là mô hình mặc định, vui lòng đặt mô hình khác làm mặc định trước
|
||||
10065=Thêm dữ liệu thất bại
|
||||
10066=Cập nhật dữ liệu thất bại
|
||||
10067=Lỗi mã xác minh đồ họa
|
||||
10068=Đăng ký di động không được bật, không thể sử dụng chức năng mã xác minh SMS
|
||||
10069=Tên người dùng không phải là số điện thoại di động, vui lòng nhập lại
|
||||
10070=Số điện thoại di động này đã được đăng ký
|
||||
10071=Số điện thoại di động đã nhập không được đăng ký
|
||||
10072=Hiện không cho phép đăng ký người dùng thông thường
|
||||
10073=Đăng ký di động không được bật, không thể sử dụng chức năng truy xuất mật khẩu
|
||||
10074=Định dạng số điện thoại di động đã nhập không chính xác
|
||||
10075=Mã xác minh SMS đã nhập không chính xác
|
||||
10076=Loại từ điển không tồn tại
|
||||
10077=Mã loại từ điển bị trùng lặp
|
||||
10078=Đọc tài nguyên thất bại
|
||||
10079=Mô hình LLM và nhận dạng ý định, lựa chọn tham số không khớp
|
||||
10080=Dấu giọng nói này thuộc về {0} đã đăng ký, vui lòng chọn giọng nói khác
|
||||
10081=Đã xảy ra lỗi khi xóa dấu giọng nói
|
||||
10082=Không cho phép sửa đổi, giọng nói này đã được đăng ký làm dấu giọng nói ({0})
|
||||
10083=Lỗi sửa đổi dấu giọng nói, vui lòng liên hệ quản trị viên
|
||||
10084=Lỗi địa chỉ giao diện dấu giọng nói, vui lòng vào quản lý tham số để sửa đổi địa chỉ giao diện dấu giọng nói
|
||||
10085=Dữ liệu âm thanh không thuộc về agent này
|
||||
10086=Dữ liệu âm thanh trống, vui lòng kiểm tra dữ liệu đã tải lên
|
||||
10087=Đăng ký dấu giọng nói thất bại, yêu cầu không thành công
|
||||
10088=Đăng ký dấu giọng nói thất bại, xử lý yêu cầu thất bại
|
||||
10089=Hủy dấu giọng nói thất bại, yêu cầu không thành công
|
||||
10090=Hủy dấu giọng nói thất bại, xử lý yêu cầu thất bại
|
||||
10091=Nhà cung cấp mô hình không tồn tại
|
||||
10092=LLM được cấu hình không tồn tại
|
||||
10093=Cấu hình mô hình này được tham chiếu bởi agent {0} và không thể xóa
|
||||
10094=Mô hình LLM này được tham chiếu bởi cấu hình nhận dạng ý định và không thể xóa
|
||||
10095=Thao tác máy chủ không hợp lệ
|
||||
10096=Địa chỉ WebSocket máy chủ chưa được cấu hình
|
||||
10097=Địa chỉ WebSocket mục tiêu không tồn tại
|
||||
10098=Danh sách địa chỉ WebSocket không thể để trống
|
||||
10099=Địa chỉ WebSocket không thể sử dụng localhost hoặc 127.0.0.1
|
||||
10100=Định dạng địa chỉ WebSocket không chính xác
|
||||
10101=Kiểm tra kết nối WebSocket thất bại
|
||||
10102=Địa chỉ OTA không thể để trống
|
||||
10103=Địa chỉ OTA không thể sử dụng localhost hoặc 127.0.0.1
|
||||
10104=Địa chỉ OTA phải bắt đầu bằng http hoặc https
|
||||
10105=Địa chỉ OTA phải kết thúc bằng /ota/
|
||||
10106=Truy cập giao diện OTA thất bại
|
||||
10107=Định dạng nội dung trả về giao diện OTA không chính xác
|
||||
10108=Xác thực giao diện OTA thất bại
|
||||
10109=Địa chỉ MCP không thể để trống
|
||||
10110=Địa chỉ MCP không thể sử dụng localhost hoặc 127.0.0.1
|
||||
10111=Không phải địa chỉ MCP hợp lệ
|
||||
10112=Truy cập giao diện MCP thất bại
|
||||
10113=Định dạng nội dung trả về giao diện MCP không chính xác
|
||||
10114=Xác thực giao diện MCP thất bại
|
||||
10115=Địa chỉ giao diện dấu giọng nói không thể để trống
|
||||
10116=Địa chỉ giao diện dấu giọng nói không thể sử dụng localhost hoặc 127.0.0.1
|
||||
10117=Không phải địa chỉ giao diện dấu giọng nói hợp lệ
|
||||
10118=Địa chỉ giao diện dấu giọng nói phải bắt đầu bằng http hoặc https
|
||||
10119=Truy cập giao diện dấu giọng nói thất bại
|
||||
10120=Định dạng nội dung trả về giao diện dấu giọng nói không chính xác
|
||||
10121=Xác thực giao diện dấu giọng nói thất bại
|
||||
10122=Bí mật MQTT không thể để trống
|
||||
10123=Bí mật MQTT của bạn không an toàn, cần ít nhất 8 ký tự và phải chứa cả chữ hoa và chữ thường
|
||||
10124=Bí mật MQTT của bạn không an toàn, bí mật MQTT phải chứa cả chữ hoa và chữ thường
|
||||
10125=Bí mật MQTT của bạn chứa mật khẩu yếu
|
||||
10128=Nhãn từ điển bị trùng lặp
|
||||
10129=Khóa SM2 chưa được cấu hình
|
||||
10130=Giải mã SM2 thất bại
|
||||
10131=modelType và provideCode không thể để trống
|
||||
10132=Không có quyền xem lịch sử trò chuyện của agent này
|
||||
10133=ID phiên không thể để trống
|
||||
10134=ID agent không thể để trống
|
||||
10135=Tải xuống lịch sử trò chuyện thất bại
|
||||
10136=Liên kết tải xuống đã hết hạn hoặc không hợp lệ
|
||||
10137=Liên kết tải xuống không hợp lệ
|
||||
10138=Người dùng
|
||||
10043=X\u00F3a d\u1EEF li\u1EC7u th\u1EA5t b\u1EA1i
|
||||
10044=Ng\u01B0\u1EDDi d\u00F9ng ch\u01B0a \u0111\u0103ng nh\u1EADp
|
||||
10045=K\u1EBFt n\u1ED1i WebSocket th\u1EA5t b\u1EA1i ho\u1EB7c h\u1EBFt th\u1EDDi gian ch\u1EDD
|
||||
10046=Kh\u00F4ng th\u1EC3 l\u01B0u d\u1EA5u gi\u1ECDng n\u00F3i, vui l\u00F2ng li\u00EAn h\u1EC7 qu\u1EA3n tr\u1ECB vi\u00EAn
|
||||
10047=\u0110\u1EA1t \u0111\u1EBFn gi\u1EDBi h\u1EA1n g\u1EEDi h\u00E0ng ng\u00E0y
|
||||
10048=M\u1EADt kh\u1EA9u c\u0169 kh\u00F4ng ch\u00EDnh x\u00E1c
|
||||
10049=LLM \u0111\u01B0\u1EE3c \u0111\u1EB7t kh\u00F4ng ph\u1EA3i l\u00E0 openai ho\u1EB7c ollama
|
||||
10050=Kh\u00F4ng th\u1EC3 t\u1EA1o token
|
||||
10051=Kh\u00F4ng t\u00ECm th\u1EA5y t\u00E0i nguy\u00EAn
|
||||
10052=Kh\u00F4ng t\u00ECm th\u1EA5y agent m\u1EB7c \u0111\u1ECBnh
|
||||
10053=Kh\u00F4ng t\u00ECm th\u1EA5y agent
|
||||
10054=Giao di\u1EC7n d\u1EA5u gi\u1ECDng n\u00F3i ch\u01B0a \u0111\u01B0\u1EE3c c\u1EA5u h\u00ECnh, vui l\u00F2ng c\u1EA5u h\u00ECnh \u0111\u1ECBa ch\u1EC9 giao di\u1EC7n d\u1EA5u gi\u1ECDng n\u00F3i (server.voice_print) trong c\u1EA5u h\u00ECnh tham s\u1ED1 tr\u01B0\u1EDBc
|
||||
10055=G\u1EEDi SMS th\u1EA5t b\u1EA1i
|
||||
10056=Kh\u00F4ng th\u1EC3 thi\u1EBFt l\u1EADp k\u1EBFt n\u1ED1i SMS
|
||||
10057=Kh\u00F4ng th\u1EC3 t\u1EA1o d\u1EA5u gi\u1ECDng n\u00F3i cho agent
|
||||
10058=Kh\u00F4ng th\u1EC3 c\u1EADp nh\u1EADt d\u1EA5u gi\u1ECDng n\u00F3i cho agent
|
||||
10059=Kh\u00F4ng th\u1EC3 x\u00F3a d\u1EA5u gi\u1ECDng n\u00F3i cho agent
|
||||
10060=G\u1EEDi qu\u00E1 th\u01B0\u1EDDng xuy\u00EAn, vui l\u00F2ng th\u1EED l\u1EA1i sau {0} gi\u00E2y
|
||||
10061=M\u00E3 k\u00EDch ho\u1EA1t kh\u00F4ng th\u1EC3 \u0111\u1EC3 tr\u1ED1ng
|
||||
10062=L\u1ED7i m\u00E3 k\u00EDch ho\u1EA1t
|
||||
10063=Thi\u1EBFt b\u1ECB \u0111\u00E3 \u0111\u01B0\u1EE3c k\u00EDch ho\u1EA1t
|
||||
10064=M\u00F4 h\u00ECnh n\u00E0y l\u00E0 m\u00F4 h\u00ECnh m\u1EB7c \u0111\u1ECBnh, vui l\u00F2ng \u0111\u1EB7t m\u00F4 h\u00ECnh kh\u00E1c l\u00E0m m\u1EB7c \u0111\u1ECBnh tr\u01B0\u1EDBc
|
||||
10065=Th\u00EAm d\u1EEF li\u1EC7u th\u1EA5t b\u1EA1i
|
||||
10066=C\u1EADp nh\u1EADt d\u1EEF li\u1EC7u th\u1EA5t b\u1EA1i
|
||||
10067=L\u1ED7i m\u00E3 x\u00E1c minh \u0111\u1ED3 h\u1ECDa
|
||||
10068=\u0110\u0103ng k\u00FD di \u0111\u1ED9ng kh\u00F4ng \u0111\u01B0\u1EE3c b\u1EADt, kh\u00F4ng th\u1EC3 s\u1EED d\u1EE5ng ch\u1EE9c n\u0103ng m\u00E3 x\u00E1c minh SMS
|
||||
10069=T\u00EAn ng\u01B0\u1EDDi d\u00F9ng kh\u00F4ng ph\u1EA3i l\u00E0 s\u1ED1 \u0111i\u1EC7n tho\u1EA1i di \u0111\u1ED9ng, vui l\u00F2ng nh\u1EADp l\u1EA1i
|
||||
10070=S\u1ED1 \u0111i\u1EC7n tho\u1EA1i di \u0111\u1ED9ng n\u00E0y \u0111\u00E3 \u0111\u01B0\u1EE3c \u0111\u0103ng k\u00FD
|
||||
10071=S\u1ED1 \u0111i\u1EC7n tho\u1EA1i di \u0111\u1ED9ng \u0111\u00E3 nh\u1EADp kh\u00F4ng \u0111\u01B0\u1EE3c \u0111\u0103ng k\u00FD
|
||||
10072=Hi\u1EC7n kh\u00F4ng cho ph\u00E9p \u0111\u0103ng k\u00FD ng\u01B0\u1EDDi d\u00F9ng th\u00F4ng th\u01B0\u1EDDng
|
||||
10073=\u0110\u0103ng k\u00FD di \u0111\u1ED9ng kh\u00F4ng \u0111\u01B0\u1EE3c b\u1EADt, kh\u00F4ng th\u1EC3 s\u1EED d\u1EE5ng ch\u1EE9c n\u0103ng truy xu\u1EA5t m\u1EADt kh\u1EA9u
|
||||
10074=\u0110\u1ECBnh d\u1EA1ng s\u1ED1 \u0111i\u1EC7n tho\u1EA1i di \u0111\u1ED9ng \u0111\u00E3 nh\u1EADp kh\u00F4ng ch\u00EDnh x\u00E1c
|
||||
10075=M\u00E3 x\u00E1c minh SMS \u0111\u00E3 nh\u1EADp kh\u00F4ng ch\u00EDnh x\u00E1c
|
||||
10076=Lo\u1EA1i t\u1EEB \u0111i\u1EC3n kh\u00F4ng t\u1ED3n t\u1EA1i
|
||||
10077=M\u00E3 lo\u1EA1i t\u1EEB \u0111i\u1EC3n b\u1ECB tr\u00F9ng l\u1EB7p
|
||||
10078=\u0110\u1ECDc t\u00E0i nguy\u00EAn th\u1EA5t b\u1EA1i
|
||||
10079=M\u00F4 h\u00ECnh LLM v\u00E0 nh\u1EADn d\u1EA1ng \u00FD \u0111\u1ECBnh, l\u1EF1a ch\u1ECDn tham s\u1ED1 kh\u00F4ng kh\u1EDBp
|
||||
10080=D\u1EA5u gi\u1ECDng n\u00F3i n\u00E0y thu\u1ED9c v\u1EC1 {0} \u0111\u00E3 \u0111\u0103ng k\u00FD, vui l\u00F2ng ch\u1ECDn gi\u1ECDng n\u00F3i kh\u00E1c
|
||||
10081=\u0110\u00E3 x\u1EA3y ra l\u1ED7i khi x\u00F3a d\u1EA5u gi\u1ECDng n\u00F3i
|
||||
10082=Kh\u00F4ng cho ph\u00E9p s\u1EEDa \u0111\u1ED5i, gi\u1ECDng n\u00F3i n\u00E0y \u0111\u00E3 \u0111\u01B0\u1EE3c \u0111\u0103ng k\u00FD l\u00E0m d\u1EA5u gi\u1ECDng n\u00F3i ({0})
|
||||
10083=L\u1ED7i s\u1EEDa \u0111\u1ED5i d\u1EA5u gi\u1ECDng n\u00F3i, vui l\u00F2ng li\u00EAn h\u1EC7 qu\u1EA3n tr\u1ECB vi\u00EAn
|
||||
10084=L\u1ED7i \u0111\u1ECBa ch\u1EC9 giao di\u1EC7n d\u1EA5u gi\u1ECDng n\u00F3i, vui l\u00F2ng v\u00E0o qu\u1EA3n l\u00FD tham s\u1ED1 \u0111\u1EC3 s\u1EEDa \u0111\u1ED5i \u0111\u1ECBa ch\u1EC9 giao di\u1EC7n d\u1EA5u gi\u1ECDng n\u00F3i
|
||||
10085=D\u1EEF li\u1EC7u \u00E2m thanh kh\u00F4ng thu\u1ED9c v\u1EC1 agent n\u00E0y
|
||||
10086=D\u1EEF li\u1EC7u \u00E2m thanh tr\u1ED1ng, vui l\u00F2ng ki\u1EC3m tra d\u1EEF li\u1EC7u \u0111\u00E3 t\u1EA3i l\u00EAn
|
||||
10087=\u0110\u0103ng k\u00FD d\u1EA5u gi\u1ECDng n\u00F3i th\u1EA5t b\u1EA1i, y\u00EAu c\u1EA7u kh\u00F4ng th\u00E0nh c\u00F4ng
|
||||
10088=\u0110\u0103ng k\u00FD d\u1EA5u gi\u1ECDng n\u00F3i th\u1EA5t b\u1EA1i, x\u1EED l\u00FD y\u00EAu c\u1EA7u th\u1EA5t b\u1EA1i
|
||||
10089=H\u1EE7y d\u1EA5u gi\u1ECDng n\u00F3i th\u1EA5t b\u1EA1i, y\u00EAu c\u1EA7u kh\u00F4ng th\u00E0nh c\u00F4ng
|
||||
10090=H\u1EE7y d\u1EA5u gi\u1ECDng n\u00F3i th\u1EA5t b\u1EA1i, x\u1EED l\u00FD y\u00EAu c\u1EA7u th\u1EA5t b\u1EA1i
|
||||
10091=Nh\u00E0 cung c\u1EA5p m\u00F4 h\u00ECnh kh\u00F4ng t\u1ED3n t\u1EA1i
|
||||
10092=LLM \u0111\u01B0\u1EE3c c\u1EA5u h\u00ECnh kh\u00F4ng t\u1ED3n t\u1EA1i
|
||||
10093=C\u1EA5u h\u00ECnh m\u00F4 h\u00ECnh n\u00E0y \u0111\u01B0\u1EE3c tham chi\u1EBFu b\u1EDFi agent {0} v\u00E0 kh\u00F4ng th\u1EC3 x\u00F3a
|
||||
10094=M\u00F4 h\u00ECnh LLM n\u00E0y \u0111\u01B0\u1EE3c tham chi\u1EBFu b\u1EDFi c\u1EA5u h\u00ECnh nh\u1EADn d\u1EA1ng \u00FD \u0111\u1ECBnh v\u00E0 kh\u00F4ng th\u1EC3 x\u00F3a
|
||||
10095=Thao t\u00E1c m\u00E1y ch\u1EE7 kh\u00F4ng h\u1EE3p l\u1EC7
|
||||
10096=\u0110\u1ECBa ch\u1EC9 WebSocket m\u00E1y ch\u1EE7 ch\u01B0a \u0111\u01B0\u1EE3c c\u1EA5u h\u00ECnh
|
||||
10097=\u0110\u1ECBa ch\u1EC9 WebSocket m\u1EE5c ti\u00EAu kh\u00F4ng t\u1ED3n t\u1EA1i
|
||||
10098=Danh s\u00E1ch \u0111\u1ECBa ch\u1EC9 WebSocket kh\u00F4ng th\u1EC3 \u0111\u1EC3 tr\u1ED1ng
|
||||
10099=\u0110\u1ECBa ch\u1EC9 WebSocket kh\u00F4ng th\u1EC3 s\u1EED d\u1EE5ng localhost ho\u1EB7c 127.0.0.1
|
||||
10100=\u0110\u1ECBnh d\u1EA1ng \u0111\u1ECBa ch\u1EC9 WebSocket kh\u00F4ng ch\u00EDnh x\u00E1c
|
||||
10101=Ki\u1EC3m tra k\u1EBFt n\u1ED1i WebSocket th\u1EA5t b\u1EA1i
|
||||
10102=\u0110\u1ECBa ch\u1EC9 OTA kh\u00F4ng th\u1EC3 \u0111\u1EC3 tr\u1ED1ng
|
||||
10103=\u0110\u1ECBa ch\u1EC9 OTA kh\u00F4ng th\u1EC3 s\u1EED d\u1EE5ng localhost ho\u1EB7c 127.0.0.1
|
||||
10104=\u0110\u1ECBa ch\u1EC9 OTA ph\u1EA3i b\u1EAFt \u0111\u1EA7u b\u1EB1ng http ho\u1EB7c https
|
||||
10105=\u0110\u1ECBa ch\u1EC9 OTA ph\u1EA3i k\u1EBFt th\u00FAc b\u1EB1ng /ota/
|
||||
10106=Truy c\u1EADp giao di\u1EC7n OTA th\u1EA5t b\u1EA1i
|
||||
10107=\u0110\u1ECBnh d\u1EA1ng n\u1ED9i dung tr\u1EA3 v\u1EC1 giao di\u1EC7n OTA kh\u00F4ng ch\u00EDnh x\u00E1c
|
||||
10108=X\u00E1c th\u1EF1c giao di\u1EC7n OTA th\u1EA5t b\u1EA1i
|
||||
10109=\u0110\u1ECBa ch\u1EC9 MCP kh\u00F4ng th\u1EC3 \u0111\u1EC3 tr\u1ED1ng
|
||||
10110=\u0110\u1ECBa ch\u1EC9 MCP kh\u00F4ng th\u1EC3 s\u1EED d\u1EE5ng localhost ho\u1EB7c 127.0.0.1
|
||||
10111=Kh\u00F4ng ph\u1EA3i \u0111\u1ECBa ch\u1EC9 MCP h\u1EE3p l\u1EC7
|
||||
10112=Truy c\u1EADp giao di\u1EC7n MCP th\u1EA5t b\u1EA1i
|
||||
10113=\u0110\u1ECBnh d\u1EA1ng n\u1ED9i dung tr\u1EA3 v\u1EC1 giao di\u1EC7n MCP kh\u00F4ng ch\u00EDnh x\u00E1c
|
||||
10114=X\u00E1c th\u1EF1c giao di\u1EC7n MCP th\u1EA5t b\u1EA1i
|
||||
10115=\u0110\u1ECBa ch\u1EC9 giao di\u1EC7n d\u1EA5u gi\u1ECDng n\u00F3i kh\u00F4ng th\u1EC3 \u0111\u1EC3 tr\u1ED1ng
|
||||
10116=\u0110\u1ECBa ch\u1EC9 giao di\u1EC7n d\u1EA5u gi\u1ECDng n\u00F3i kh\u00F4ng th\u1EC3 s\u1EED d\u1EE5ng localhost ho\u1EB7c 127.0.0.1
|
||||
10117=Kh\u00F4ng ph\u1EA3i \u0111\u1ECBa ch\u1EC9 giao di\u1EC7n d\u1EA5u gi\u1ECDng n\u00F3i h\u1EE3p l\u1EC7
|
||||
10118=\u0110\u1ECBa ch\u1EC9 giao di\u1EC7n d\u1EA5u gi\u1ECDng n\u00F3i ph\u1EA3i b\u1EAFt \u0111\u1EA7u b\u1EB1ng http ho\u1EB7c https
|
||||
10119=Truy c\u1EADp giao di\u1EC7n d\u1EA5u gi\u1ECDng n\u00F3i th\u1EA5t b\u1EA1i
|
||||
10120=\u0110\u1ECBnh d\u1EA1ng n\u1ED9i dung tr\u1EA3 v\u1EC1 giao di\u1EC7n d\u1EA5u gi\u1ECDng n\u00F3i kh\u00F4ng ch\u00EDnh x\u00E1c
|
||||
10121=X\u00E1c th\u1EF1c giao di\u1EC7n d\u1EA5u gi\u1ECDng n\u00F3i th\u1EA5t b\u1EA1i
|
||||
10122=B\u00ED m\u1EADt MQTT kh\u00F4ng th\u1EC3 \u0111\u1EC3 tr\u1ED1ng
|
||||
10123=B\u00ED m\u1EADt MQTT c\u1EE7a b\u1EA1n kh\u00F4ng an to\u00E0n, c\u1EA7n \u00EDt nh\u1EA5t 8 k\u00FD t\u1EF1 v\u00E0 ph\u1EA3i ch\u1EE9a c\u1EA3 ch\u1EEF hoa v\u00E0 ch\u1EEF th\u01B0\u1EDDng
|
||||
10124=B\u00ED m\u1EADt MQTT c\u1EE7a b\u1EA1n kh\u00F4ng an to\u00E0n, b\u00ED m\u1EADt MQTT ph\u1EA3i ch\u1EE9a c\u1EA3 ch\u1EEF hoa v\u00E0 ch\u1EEF th\u01B0\u1EDDng
|
||||
10125=B\u00ED m\u1EADt MQTT c\u1EE7a b\u1EA1n ch\u1EE9a m\u1EADt kh\u1EA9u y\u1EBFu
|
||||
10128=Nh\u00E3n t\u1EEB \u0111i\u1EC3n b\u1ECB tr\u00F9ng l\u1EB7p
|
||||
10129=Kh\u00F3a SM2 ch\u01B0a \u0111\u01B0\u1EE3c c\u1EA5u h\u00ECnh
|
||||
10130=Gi\u1EA3i m\u00E3 SM2 th\u1EA5t b\u1EA1i
|
||||
10131=modelType v\u00E0 provideCode kh\u00F4ng th\u1EC3 \u0111\u1EC3 tr\u1ED1ng
|
||||
10132=Kh\u00F4ng c\u00F3 quy\u1EC1n xem l\u1ECBch s\u1EED tr\u00F2 chuy\u1EC7n c\u1EE7a agent n\u00E0y
|
||||
10133=ID phi\u00EAn kh\u00F4ng th\u1EC3 \u0111\u1EC3 tr\u1ED1ng
|
||||
10134=ID agent kh\u00F4ng th\u1EC3 \u0111\u1EC3 tr\u1ED1ng
|
||||
10135=T\u1EA3i xu\u1ED1ng l\u1ECBch s\u1EED tr\u00F2 chuy\u1EC7n th\u1EA5t b\u1EA1i
|
||||
10136=Li\u00EAn k\u1EBFt t\u1EA3i xu\u1ED1ng \u0111\u00E3 h\u1EBFt h\u1EA1n ho\u1EB7c kh\u00F4ng h\u1EE3p l\u1EC7
|
||||
10137=Li\u00EAn k\u1EBFt t\u1EA3i xu\u1ED1ng kh\u00F4ng h\u1EE3p l\u1EC7
|
||||
10138=Ng\u01B0\u1EDDi d\u00F9ng
|
||||
10139=Agent
|
||||
10140=Tệp âm thanh không thể để trống
|
||||
10141=Chỉ hỗ trợ tệp âm thanh
|
||||
10142=Kích thước tệp âm thanh không thể vượt quá 10MB
|
||||
10143=Tải lên thất bại
|
||||
10144=Bản ghi nhân bản giọng nói không tồn tại
|
||||
10145=Thông tin tài nguyên giọng nói không thể để trống
|
||||
10146=Tên nền tảng TTS không thể để trống
|
||||
10147=ID giọng nói không thể để trống
|
||||
10148=Tài khoản chủ sở hữu không thể để trống
|
||||
10149=ID tài nguyên giọng nói cần xóa không thể để trống
|
||||
10150=Bạn không có quyền thao tác bản ghi này
|
||||
10151=Vui lòng tải lên tệp âm thanh trước
|
||||
10152=Không tìm thấy cấu hình mô hình
|
||||
10153=Không tìm thấy loại mô hình
|
||||
10154=Đào tạo thất bại: {0}
|
||||
10155=Thiếu cấu hình Huoshan Engine
|
||||
10156=Lỗi định dạng phản hồi, thiếu trường BaseResp
|
||||
10157=Yêu cầu thất bại
|
||||
10158=Nhân bản giọng nói:
|
||||
10159=ID giọng nói đã tồn tại
|
||||
10160=Lỗi định dạng ID giọng nói Huoshan Engine, phải bắt đầu bằng S_
|
||||
10161=Địa chỉ Mac đã tồn tại
|
||||
10162=Nhà cung cấp mô hình không tồn tại
|
||||
10163=Bản ghi cơ sở kiến thức không tồn tại
|
||||
10164=Không tìm thấy cấu hình RAG
|
||||
10165=Lỗi loại cấu hình RAG
|
||||
10166=Không tìm thấy cấu hình RAG mặc định
|
||||
10167=Gọi API RAG thất bại: {0}
|
||||
10168=Tải lên tệp thất bại
|
||||
10169=Không có quyền thao tác cơ sở kiến thức này
|
||||
10170=Tên cơ sở kiến thức đã tồn tại
|
||||
10171=Cấu hình RAG base_url không thể để trống
|
||||
10172=Cấu hình RAG api_key không thể để trống
|
||||
10173=Cấu hình RAG api_key không thể chứa trình giữ chỗ, vui lòng thay thế bằng khóa API thực tế
|
||||
10174=Lỗi định dạng cấu hình RAG base_url, phải bắt đầu bằng http hoặc https
|
||||
10175=Địa chỉ Mac không thể để trống
|
||||
10176=Cấu hình RAG dataset_id không thể để trống
|
||||
10177=Cấu hình RAG model_id không thể để trống
|
||||
10178=Cấu hình RAG dataset_id và model_id không thể để trống
|
||||
10179=Tên tệp không thể để trống
|
||||
10180=Nội dung tệp không thể để trống
|
||||
10181=Tên nhân bản giọng nói không thể để trống
|
||||
10182=Không tìm thấy âm thanh nhân bản giọng nói
|
||||
10183=Không tìm thấy mẫu agent mặc định
|
||||
10140=T\u1EC7p \u00E2m thanh kh\u00F4ng th\u1EC3 \u0111\u1EC3 tr\u1ED1ng
|
||||
10141=Ch\u1EC9 h\u1ED7 tr\u1EE3 t\u1EC7p \u00E2m thanh
|
||||
10142=K\u00EDch th\u01B0\u1EDBc t\u1EC7p \u00E2m thanh kh\u00F4ng th\u1EC3 v\u01B0\u1EE3t qu\u00E1 10MB
|
||||
10143=T\u1EA3i l\u00EAn th\u1EA5t b\u1EA1i
|
||||
10144=B\u1EA3n ghi nh\u00E2n b\u1EA3n gi\u1ECDng n\u00F3i kh\u00F4ng t\u1ED3n t\u1EA1i
|
||||
10145=Th\u00F4ng tin t\u00E0i nguy\u00EAn gi\u1ECDng n\u00F3i kh\u00F4ng th\u1EC3 \u0111\u1EC3 tr\u1ED1ng
|
||||
10146=T\u00EAn n\u1EC1n t\u1EA3ng TTS kh\u00F4ng th\u1EC3 \u0111\u1EC3 tr\u1ED1ng
|
||||
10147=ID gi\u1ECDng n\u00F3i kh\u00F4ng th\u1EC3 \u0111\u1EC3 tr\u1ED1ng
|
||||
10148=T\u00E0i kho\u1EA3n ch\u1EE7 s\u1EDF h\u1EEFu kh\u00F4ng th\u1EC3 \u0111\u1EC3 tr\u1ED1ng
|
||||
10149=ID t\u00E0i nguy\u00EAn gi\u1ECDng n\u00F3i c\u1EA7n x\u00F3a kh\u00F4ng th\u1EC3 \u0111\u1EC3 tr\u1ED1ng
|
||||
10150=B\u1EA1n kh\u00F4ng c\u00F3 quy\u1EC1n thao t\u00E1c b\u1EA3n ghi n\u00E0y
|
||||
10151=Vui l\u00F2ng t\u1EA3i l\u00EAn t\u1EC7p \u00E2m thanh tr\u01B0\u1EDBc
|
||||
10152=Kh\u00F4ng t\u00ECm th\u1EA5y c\u1EA5u h\u00ECnh m\u00F4 h\u00ECnh
|
||||
10153=Kh\u00F4ng t\u00ECm th\u1EA5y lo\u1EA1i m\u00F4 h\u00ECnh
|
||||
10154=\u0110\u00E0o t\u1EA1o th\u1EA5t b\u1EA1i: {0}
|
||||
10155=Thi\u1EBFu c\u1EA5u h\u00ECnh Huoshan Engine
|
||||
10156=L\u1ED7i \u0111\u1ECBnh d\u1EA1ng ph\u1EA3n h\u1ED3i, thi\u1EBFu tr\u01B0\u1EDDng BaseResp
|
||||
10157=Y\u00EAu c\u1EA7u th\u1EA5t b\u1EA1i
|
||||
10158=Nh\u00E2n b\u1EA3n \u00E2m s\u1EAFc:
|
||||
10159=ID gi\u1ECDng n\u00F3i \u0111\u00E3 t\u1ED3n t\u1EA1i
|
||||
10160=L\u1ED7i \u0111\u1ECBnh d\u1EA1ng ID gi\u1ECDng n\u00F3i Huoshan Engine, ph\u1EA3i b\u1EAFt \u0111\u1EA7u b\u1EB1ng S_
|
||||
10161=\u0110\u1ECBa ch\u1EC9 Mac \u0111\u00E3 t\u1ED3n t\u1EA1i
|
||||
10162=Nh\u00E0 cung c\u1EA5p m\u00F4 h\u00ECnh kh\u00F4ng t\u1ED3n t\u1EA1i
|
||||
10163=B\u1EA3n ghi c\u01A1 s\u1EDF ki\u1EBFn th\u1EE9c kh\u00F4ng t\u1ED3n t\u1EA1i
|
||||
10164=Kh\u00F4ng t\u00ECm th\u1EA5y c\u1EA5u h\u00ECnh RAG
|
||||
10165=L\u1ED7i lo\u1EA1i c\u1EA5u h\u00ECnh RAG
|
||||
10166=Kh\u00F4ng t\u00ECm th\u1EA5y c\u1EA5u h\u00ECnh RAG m\u1EB7c \u0111\u1ECBnh
|
||||
10167=G\u1ECDi API RAG th\u1EA5t b\u1EA1i: {0}
|
||||
10168=T\u1EA3i l\u00EAn t\u1EC7p th\u1EA5t b\u1EA1i
|
||||
10169=Kh\u00F4ng c\u00F3 quy\u1EC1n thao t\u00E1c c\u01A1 s\u1EDF ki\u1EBFn th\u1EE9c n\u00E0y
|
||||
10170=T\u00EAn c\u01A1 s\u1EDF ki\u1EBFn th\u1EE9c \u0111\u00E3 t\u1ED3n t\u1EA1i
|
||||
10171=C\u1EA5u h\u00ECnh RAG base_url kh\u00F4ng th\u1EC3 \u0111\u1EC3 tr\u1ED1ng
|
||||
10172=C\u1EA5u h\u00ECnh RAG api_key kh\u00F4ng th\u1EC3 \u0111\u1EC3 tr\u1ED1ng
|
||||
10173=C\u1EA5u h\u00ECnh RAG api_key kh\u00F4ng th\u1EC3 ch\u1EE9a tr\u00ECnh gi\u1EEF ch\u1ED7, vui l\u00F2ng thay th\u1EBF b\u1EB1ng kh\u00F3a API th\u1EF1c t\u1EBF
|
||||
10174=L\u1ED7i \u0111\u1ECBnh d\u1EA1ng c\u1EA5u h\u00ECnh RAG base_url, ph\u1EA3i b\u1EAFt \u0111\u1EA7u b\u1EB1ng http ho\u1EB7c https
|
||||
10175=\u0110\u1ECBa ch\u1EC9 Mac kh\u00F4ng th\u1EC3 \u0111\u1EC3 tr\u1ED1ng
|
||||
10176=C\u1EA5u h\u00ECnh RAG dataset_id kh\u00F4ng th\u1EC3 \u0111\u1EC3 tr\u1ED1ng
|
||||
10177=C\u1EA5u h\u00ECnh RAG model_id kh\u00F4ng th\u1EC3 \u0111\u1EC3 tr\u1ED1ng
|
||||
10178=C\u1EA5u h\u00ECnh RAG dataset_id v\u00E0 model_id kh\u00F4ng th\u1EC3 \u0111\u1EC3 tr\u1ED1ng
|
||||
10179=T\u00EAn t\u1EC7p kh\u00F4ng th\u1EC3 \u0111\u1EC3 tr\u1ED1ng
|
||||
10180=N\u1ED9i dung t\u1EC7p kh\u00F4ng th\u1EC3 \u0111\u1EC3 tr\u1ED1ng
|
||||
10181=T\u00EAn nh\u00E2n b\u1EA3n gi\u1ECDng n\u00F3i kh\u00F4ng th\u1EC3 \u0111\u1EC3 tr\u1ED1ng
|
||||
10182=Kh\u00F4ng t\u00ECm th\u1EA5y \u00E2m thanh nh\u00E2n b\u1EA3n gi\u1ECDng n\u00F3i
|
||||
10183=Kh\u00F4ng t\u00ECm th\u1EA5y m\u1EABu agent m\u1EB7c \u0111\u1ECBnh
|
||||
10184=Lo\u1EA1i b\u1ED9 chuy\u1EC3n \u0111\u1ED5i kh\u00F4ng \u0111\u01B0\u1EE3c h\u1ED7 tr\u1EE3
|
||||
10185=Ki\u1EC3m tra c\u1EA5u h\u00ECnh RAG th\u1EA5t b\u1EA1i
|
||||
10186=T\u1EA1o b\u1ED9 chuy\u1EC3n \u0111\u1ED5i th\u1EA5t b\u1EA1i
|
||||
10187=Kh\u1EDFi t\u1EA1o b\u1ED9 chuy\u1EC3n \u0111\u1ED5i th\u1EA5t b\u1EA1i
|
||||
10188=Ki\u1EC3m tra k\u1EBFt n\u1ED1i b\u1ED9 chuy\u1EC3n \u0111\u1ED5i th\u1EA5t b\u1EA1i
|
||||
10189=Thao t\u00E1c b\u1ED9 chuy\u1EC3n \u0111\u1ED5i th\u1EA5t b\u1EA1i
|
||||
10190=Kh\u00F4ng t\u00ECm th\u1EA5y b\u1ED9 chuy\u1EC3n \u0111\u1ED5i
|
||||
10191=L\u1ED7i b\u1ED9 nh\u1EDB \u0111\u1EC7m b\u1ED9 chuy\u1EC3n \u0111\u1ED5i
|
||||
10192=Kh\u00F4ng t\u00ECm th\u1EA5y lo\u1EA1i b\u1ED9 chuy\u1EC3n \u0111\u1ED5i
|
||||
10193=ID thi\u1EBFt b\u1ECB kh\u00F4ng th\u1EC3 tr\u1ED1ng
|
||||
10194=Kh\u00F4ng t\u00ECm th\u1EA5y thi\u1EBFt b\u1ECB ho\u1EB7c thi\u1EBFt b\u1ECB kh\u00F4ng tr\u1EF1c tuy\u1EBFn
|
||||
10195=S\u1ED1 l\u1EA7n t\u1EA3i l\u00EAn OTA v\u01B0\u1EE3t qu\u00E1 gi\u1EDBi h\u1EA1n
|
||||
@@ -189,4 +189,16 @@
|
||||
10180=\u6587\u4EF6\u5185\u5BB9\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10181=\u97F3\u8272\u514B\u9686\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10182=\u97F3\u8272\u514B\u9686\u97F3\u9891\u4E0D\u5B58\u5728
|
||||
10183=\u9ED8\u8BA4\u667A\u80FD\u4F53\u672A\u627E\u5230
|
||||
10183=\u9ED8\u8BA4\u667A\u80FD\u4F53\u672A\u627E\u5230
|
||||
10184=\u4E0D\u652F\u6301\u7684\u9002\u914D\u5668\u7C7B\u578B
|
||||
10185=RAG\u914D\u7F6E\u9A8C\u8BC1\u5931\u8D25
|
||||
10186=\u9002\u914D\u5668\u521B\u5EFA\u5931\u8D25
|
||||
10187=\u9002\u914D\u5668\u521D\u59CB\u5316\u5931\u8D25
|
||||
10188=\u9002\u914D\u5668\u8FDE\u63A5\u6D4B\u8BD5\u5931\u8D25
|
||||
10189=\u9002\u914D\u5668\u64CD\u4F5C\u5931\u8D25
|
||||
10190=\u9002\u914D\u5668\u672A\u627E\u5230
|
||||
10191=\u9002\u914D\u5668\u7F13\u5B58\u9519\u8BEF
|
||||
10192=\u9002\u914D\u5668\u7C7B\u578B\u672A\u627E\u5230
|
||||
10193=\u8BBE\u5907ID\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10194=\u8BBE\u5907\u4E0D\u5B58\u5728\u6216\u4E0D\u5728\u7EBF
|
||||
10195=OTA\u4E0A\u4F20\u6B21\u6570\u8D85\u8FC7\u9650\u5236
|
||||
@@ -164,7 +164,7 @@
|
||||
10155=\u706B\u5C71\u5F15\u64CE\u7F3A\u5C11appid\u6216access_token
|
||||
10156=\u97FF\u61C9\u683C\u5F0F\u932F\u8AA4\uFF0C\u7F3A\u5C11BaseResp\u5B57\u6BB5
|
||||
10157=\u8ACB\u6C42\u5931\u6557
|
||||
10158=\u514b\u9686\u97f3\u8272:
|
||||
10158=\u514B\u9686\u97F3\u8272:
|
||||
10159=\u97F3\u8272ID\u5DF2\u5B58\u5728
|
||||
10160=\u706B\u5C71\u5F15\u64CE\u97F3\u8272ID\u683C\u5F0F\u932F\u8AA4\uFF0C\u5FC5\u9808\u4EE5S_\u958B\u982D
|
||||
10161=Mac\u5730\u5740\u5DF2\u5B58\u5728
|
||||
@@ -173,20 +173,32 @@
|
||||
10164=RAG\u914D\u7F6E\u672A\u627E\u5230
|
||||
10165=RAG\u914D\u7F6E\u985E\u578B\u932F\u8AA4
|
||||
10166=\u9810\u8A2DRAG\u914D\u7F6E\u672A\u627E\u5230
|
||||
10167=\u0052\u0041\u0047\u8abf\u7528\u5931\u6557\uFF0C{0}
|
||||
10167=RAG\u8ABF\u7528\u5931\u6557\uFF0C{0}
|
||||
10168=\u4E0A\u50B3\u6587\u4EF6\u5931\u6557
|
||||
10169=\u60A8\u6C92\u6709\u6B0A\u9650\u64CD\u4F5C\u8A72\u8A18\u9304
|
||||
10170=\u77E5\u8B58\u5EAB\u540D\u7A31\u91CD\u8907
|
||||
10171=\u0052\u0041\u0047\u914D\u7F6E\u4F53\u7684base_url\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10172=\u0052\u0041\u0047\u914D\u7F6E\u4F53\u7684api_key\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10173=\u0052\u0041\u0047\u914D\u7F6E\u4F53\u7684api_key\u4E0D\u80FD\u4E3A\u7A7A\uFF0C\u8BF7\u66F4\u6362\u4E3A\u5728\u53D6\u7684API\u53C2\u6570
|
||||
10174=\u0052\u0041\u0047\u914D\u7F6E\u4F53\u7684base_url\u683C\u5F0F\u9519\u8BEF\uFF0C\u5FC5\u987B\u4EE5http\u6216https\u5F00\u5934
|
||||
10171=RAG\u914D\u7F6E\u4F53\u7684base_url\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10172=RAG\u914D\u7F6E\u4F53\u7684api_key\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10173=RAG\u914D\u7F6E\u4F53\u7684api_key\u4E0D\u80FD\u4E3A\u7A7A\uFF0C\u8BF7\u66F4\u6362\u4E3A\u5728\u53D6\u7684API\u53C2\u6570
|
||||
10174=RAG\u914D\u7F6E\u4F53\u7684base_url\u683C\u5F0F\u9519\u8BEF\uFF0C\u5FC5\u987B\u4EE5http\u6216https\u5F00\u5934
|
||||
10175=mac\u5730\u5740\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10176=\u0052\u0041\u0047\u914D\u7F6E\u4F53\u7684dataset_id\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10177=\u0052\u0041\u0047\u914D\u7F6E\u4F53\u7684model_id\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10178=\u0052\u0041\u0047\u914D\u7F6E\u4F53\u7684dataset_id\u548Cmodel_id\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10179=\u6587\u4ef6\u540d\u7a31\u4e0d\u80fd\u70ba\u7a7a
|
||||
10180=\u6587\u4ef6\u5185\u5bb9\u4e0d\u80fd\u70ba\u7a7a
|
||||
10181=\u97f3\u8272\u514b\u9686\u540d\u7a31\u4e0d\u80fd\u70ba\u7a7a
|
||||
10182=\u97f3\u8272\u514b\u9686\u97f3\u983b\u4e0d\u5b58\u5728
|
||||
10183=\u9ed8\u8ba4\u667a\u80fd\u4f53\u672a\u627e\u5230
|
||||
10176=RAG\u914D\u7F6E\u4F53\u7684dataset_id\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10177=RAG\u914D\u7F6E\u4F53\u7684model_id\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10178=RAG\u914D\u7F6E\u4F53\u7684dataset_id\u548Cmodel_id\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10179=\u6587\u4EF6\u540D\u7A31\u4E0D\u80FD\u70BA\u7A7A
|
||||
10180=\u6587\u4EF6\u5185\u5BB9\u4E0D\u80FD\u70BA\u7A7A
|
||||
10181=\u97F3\u8272\u514B\u9686\u540D\u7A31\u4E0D\u80FD\u70BA\u7A7A
|
||||
10182=\u97F3\u8272\u514B\u9686\u97F3\u983B\u4E0D\u5B58\u5728
|
||||
10183=\u9ED8\u8BA4\u667A\u80FD\u4F53\u672A\u627E\u5230
|
||||
10184=\u4E0D\u652F\u6301\u7684\u9002\u914D\u5668\u985E\u578B
|
||||
10185=RAG\u914D\u7F6E\u9A57\u8B49\u5931\u6557
|
||||
10186=\u9002\u914D\u5668\u5275\u5EFA\u5931\u6557
|
||||
10187=\u9002\u914D\u5668\u521D\u59CB\u5316\u5931\u6557
|
||||
10188=\u9002\u914D\u5668\u9023\u63A5\u6E2C\u8A66\u5931\u6557
|
||||
10189=\u9002\u914D\u5668\u64CD\u4F5C\u5931\u6557
|
||||
10190=\u9002\u914D\u5668\u672A\u627E\u5230
|
||||
10191=\u9002\u914D\u5668\u7F13\u5B58\u932F\u8AA4
|
||||
10192=\u9002\u914D\u5668\u985E\u578B\u672A\u627E\u5230
|
||||
10193=\u8A2D\u5099ID\u4E0D\u80FD\u4E3A\u7A7A
|
||||
10194=\u8A2D\u5099\u4E0D\u5B58\u5728\u6216\u672A\u5728\u7DDA
|
||||
10195=OTA\u4E0A\u4F20\u6B21\u6578\u8D85\u904E\u9650\u5236
|
||||
@@ -22,13 +22,18 @@
|
||||
created_at, updated_at
|
||||
</sql>
|
||||
|
||||
<delete id="deleteAudioByAgentId">
|
||||
DELETE FROM ai_agent_chat_audio
|
||||
WHERE id IN (
|
||||
SELECT audio_id
|
||||
FROM ai_agent_chat_history
|
||||
WHERE agent_id = #{agentId}
|
||||
)
|
||||
<select id="getAudioIdsByAgentId" resultType="java.lang.String">
|
||||
SELECT DISTINCT audio_id
|
||||
FROM ai_agent_chat_history
|
||||
WHERE agent_id = #{agentId} AND audio_id IS NOT NULL
|
||||
</select>
|
||||
|
||||
<delete id="deleteAudioByIds">
|
||||
DELETE FROM ai_agent_chat_audio
|
||||
WHERE id IN
|
||||
<foreach collection="audioIds" item="id" open="(" separator="," close=")">
|
||||
#{id}
|
||||
</foreach>
|
||||
</delete>
|
||||
|
||||
<update id="deleteAudioIdByAgentId">
|
||||
|
||||
@@ -15,4 +15,23 @@
|
||||
FROM sys_dict_type
|
||||
WHERE id = #{dictTypeId}
|
||||
</select>
|
||||
|
||||
<select id="getDictTypesByIdList" resultType="java.lang.String">
|
||||
SELECT
|
||||
dict_type
|
||||
FROM
|
||||
sys_dict_type
|
||||
WHERE
|
||||
id IN (
|
||||
SELECT
|
||||
dict_type_id
|
||||
FROM
|
||||
sys_dict_data
|
||||
WHERE
|
||||
id IN
|
||||
<foreach collection="dictDataIdList" item="id" open="(" separator="," close=")">
|
||||
#{id}
|
||||
</foreach>
|
||||
)
|
||||
</select>
|
||||
</mapper>
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
<mapper namespace="xiaozhi.modules.voiceclone.dao.VoiceCloneDao">
|
||||
<select id="getTrainSuccess" resultType="xiaozhi.modules.model.dto.VoiceDTO">
|
||||
select id, name
|
||||
select id, name, voice_id as voiceDemo
|
||||
from ai_voice_clone
|
||||
where model_id = #{modelId} and user_id = #{userId} and train_status = 2
|
||||
</select>
|
||||
|
||||
@@ -235,7 +235,7 @@ function showAbout() {
|
||||
title: t('settings.aboutApp', { appName: import.meta.env.VITE_APP_TITLE }),
|
||||
content: t('settings.aboutContent', {
|
||||
appName: import.meta.env.VITE_APP_TITLE,
|
||||
version: '0.8.8'
|
||||
version: '0.9.1'
|
||||
}),
|
||||
showCancel: false,
|
||||
confirmText: t('common.confirm'),
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
# 语音盒子主题自定义
|
||||
|
||||
## 项目概述
|
||||
|
||||
本目录包含从 [xiaozhi-assets-generator](https://github.com/xinnan-tech/xiaozhi-assets-generator) 项目打包的静态文件,用于语音盒子主题的在线自定义与生成。用户可以通过此工具配置唤醒词、字体、表情和聊天背景等元素,并导出为 `assets.bin` 文件。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
generator/
|
||||
├── assets/ # 构建生成的资源文件
|
||||
│ ├── ft_render-ByO_jG18.js
|
||||
│ ├── index-CYcyz9xb.js
|
||||
│ └── index-NXxBVrod.css
|
||||
├── static/ # 静态资源目录
|
||||
│ ├── charsets/ # 字符集文件
|
||||
│ │ ├── deepseek.txt
|
||||
│ │ ├── gb2312.txt
|
||||
│ │ ├── latin1.txt
|
||||
│ │ └── qwen18409.txt
|
||||
│ ├── fonts/ # 字体资源
|
||||
│ │ ├── font_noto_qwen_14_1.bin
|
||||
│ │ ├── font_noto_qwen_16_4.bin
|
||||
│ │ ├── font_noto_qwen_20_4.bin
|
||||
│ │ ├── font_noto_qwen_30_4.bin
|
||||
│ │ ├── font_puhui_deepseek_14_1.bin
|
||||
│ │ ├── font_puhui_deepseek_16_4.bin
|
||||
│ │ ├── font_puhui_deepseek_20_4.bin
|
||||
│ │ ├── font_puhui_deepseek_30_4.bin
|
||||
│ │ ├── noto_qwen.ttf
|
||||
│ │ └── puhui_deepseek.ttf
|
||||
│ ├── multinet_model/ # 自定义唤醒词模型
|
||||
│ │ ├── fst/
|
||||
│ │ ├── mn6_cn/
|
||||
│ │ ├── mn6_en/
|
||||
│ │ ├── mn7_cn/
|
||||
│ │ └── mn7_en/
|
||||
│ ├── twemoji32/ # 32x32 表情图片
|
||||
│ ├── twemoji64/ # 64x64 表情图片
|
||||
│ ├── wakenet_model/ # 预设唤醒词模型
|
||||
│ └── README.md # 静态资源说明
|
||||
├── index.html # 主页面
|
||||
└── README.md # 项目说明文档
|
||||
```
|
||||
|
||||
## 主要功能
|
||||
|
||||
### 1. 芯片与屏幕配置
|
||||
- 支持多种芯片型号:ESP32-S3、ESP32-C3、ESP32-P4、ESP32-C6
|
||||
- 灵活的屏幕分辨率设置
|
||||
- 支持 RGB565 颜色格式
|
||||
|
||||
### 2. 唤醒词配置
|
||||
- **预设唤醒词**:基于不同芯片支持的 WakeNet 模型
|
||||
- **自定义唤醒词**:支持中文和英文命令词,可配置阈值和超时时间
|
||||
|
||||
### 3. 字体配置
|
||||
- 预设多种字体:阿里巴巴普惠体、Noto Qwen 等
|
||||
- 支持上传自定义 TTF/WOFF 字体文件
|
||||
- 可配置字号和颜色深度(bpp)
|
||||
|
||||
### 4. 表情集合
|
||||
- 提供 21 种基础表情的预设方案(32x32 和 64x64 两种尺寸)
|
||||
- 支持自定义表情上传
|
||||
|
||||
### 5. 聊天背景
|
||||
- 支持浅色/深色模式切换
|
||||
- 可配置纯色背景或图片背景
|
||||
- 自动适配屏幕分辨率
|
||||
|
||||
## 使用方法
|
||||
|
||||
1. 以服务方式启动 `index.html` 文件
|
||||
2. 选择芯片型号和屏幕配置
|
||||
3. 通过不同标签页配置主题元素
|
||||
4. 点击生成按钮查看资源清单
|
||||
5. 确认后生成并下载 `assets.bin` 文件
|
||||
|
||||
## 技术说明
|
||||
|
||||
- 构建后的静态资源位于 `assets/` 目录
|
||||
- 原始模型和资源文件位于 `static/` 目录
|
||||
- 支持离线使用,无需额外依赖
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 本工具为离线使用设计,所有资源已包含在目录中
|
||||
- 生成的 `assets.bin` 文件需要与语音盒子硬件配合使用
|
||||
- 自定义资源需注意文件格式和大小限制,以确保兼容
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" href="./favicon.ico">
|
||||
<title>Xiaozhi AI Customization</title>
|
||||
<script type="module" crossorigin src="./assets/index-B8r0c7xg.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-CrIJdTCK.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user