mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-28 10:03:54 +08:00
@@ -80,6 +80,7 @@ VAD:
|
|||||||
7、[如何开启声纹识别](./voiceprint-integration.md)<br/>
|
7、[如何开启声纹识别](./voiceprint-integration.md)<br/>
|
||||||
8、[新闻插件源配置指南](./newsnow_plugin_config.md)<br/>
|
8、[新闻插件源配置指南](./newsnow_plugin_config.md)<br/>
|
||||||
9、[知识库ragflow集成指南](./ragflow-integration.md)<br/>
|
9、[知识库ragflow集成指南](./ragflow-integration.md)<br/>
|
||||||
|
10、[如何部署上下文源](./context-provider-integration.md)<br/>
|
||||||
|
|
||||||
### 11、语音克隆、本地语音部署相关教程
|
### 11、语音克隆、本地语音部署相关教程
|
||||||
1、[如何在智控台克隆音色](./huoshan-streamTTS-voice-cloning.md)<br/>
|
1、[如何在智控台克隆音色](./huoshan-streamTTS-voice-cloning.md)<br/>
|
||||||
|
|||||||
@@ -0,0 +1,220 @@
|
|||||||
|
# 数据上下文填充功能使用指南
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
数据上下文填充功能(Context Data Provider)允许小智在唤醒那一刻,获取外部系统的数据,并将其动态注入到大模型的系统提示词(System Prompt)中。
|
||||||
|
让其做到唤醒时感知世界某个事物的状态。
|
||||||
|
|
||||||
|
通过这个功能,在小智唤醒的一刹那,“感知”到:
|
||||||
|
- 智能家居的传感器状态(温度、湿度、灯光状态等)
|
||||||
|
- 业务系统的实时数据(服务器负载、健康数据、股票信息等)
|
||||||
|
- 任何可以通过 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服务已停止")
|
||||||
|
```
|
||||||
@@ -44,6 +44,7 @@ import xiaozhi.modules.agent.entity.AgentEntity;
|
|||||||
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
||||||
import xiaozhi.modules.agent.service.AgentChatAudioService;
|
import xiaozhi.modules.agent.service.AgentChatAudioService;
|
||||||
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||||
|
import xiaozhi.modules.agent.service.AgentContextProviderService;
|
||||||
import xiaozhi.modules.agent.service.AgentPluginMappingService;
|
import xiaozhi.modules.agent.service.AgentPluginMappingService;
|
||||||
import xiaozhi.modules.agent.service.AgentService;
|
import xiaozhi.modules.agent.service.AgentService;
|
||||||
import xiaozhi.modules.agent.service.AgentTemplateService;
|
import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||||
@@ -64,6 +65,7 @@ public class AgentController {
|
|||||||
private final AgentChatHistoryService agentChatHistoryService;
|
private final AgentChatHistoryService agentChatHistoryService;
|
||||||
private final AgentChatAudioService agentChatAudioService;
|
private final AgentChatAudioService agentChatAudioService;
|
||||||
private final AgentPluginMappingService agentPluginMappingService;
|
private final AgentPluginMappingService agentPluginMappingService;
|
||||||
|
private final AgentContextProviderService agentContextProviderService;
|
||||||
private final RedisUtils redisUtils;
|
private final RedisUtils redisUtils;
|
||||||
|
|
||||||
@GetMapping("/list")
|
@GetMapping("/list")
|
||||||
@@ -135,6 +137,8 @@ public class AgentController {
|
|||||||
agentChatHistoryService.deleteByAgentId(id, true, true);
|
agentChatHistoryService.deleteByAgentId(id, true, true);
|
||||||
// 删除关联的插件
|
// 删除关联的插件
|
||||||
agentPluginMappingService.deleteByAgentId(id);
|
agentPluginMappingService.deleteByAgentId(id);
|
||||||
|
// 删除关联的上下文源配置
|
||||||
|
agentContextProviderService.deleteByAgentId(id);
|
||||||
// 再删除智能体
|
// 再删除智能体
|
||||||
agentService.deleteById(id);
|
agentService.deleteById(id);
|
||||||
return new Result<>();
|
return new Result<>();
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
}
|
||||||
@@ -69,6 +69,9 @@ public class AgentUpdateDTO implements Serializable {
|
|||||||
@Schema(description = "排序", example = "1", nullable = true)
|
@Schema(description = "排序", example = "1", nullable = true)
|
||||||
private Integer sort;
|
private Integer sort;
|
||||||
|
|
||||||
|
@Schema(description = "上下文源配置", nullable = true)
|
||||||
|
private List<ContextProviderDTO> contextProviders;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@Schema(description = "插件函数信息")
|
@Schema(description = "插件函数信息")
|
||||||
public static class FunctionInfo implements Serializable {
|
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;
|
||||||
|
}
|
||||||
+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);
|
||||||
|
}
|
||||||
+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));
|
||||||
|
}
|
||||||
|
}
|
||||||
+18
@@ -32,10 +32,12 @@ import xiaozhi.modules.agent.dao.AgentDao;
|
|||||||
import xiaozhi.modules.agent.dto.AgentCreateDTO;
|
import xiaozhi.modules.agent.dto.AgentCreateDTO;
|
||||||
import xiaozhi.modules.agent.dto.AgentDTO;
|
import xiaozhi.modules.agent.dto.AgentDTO;
|
||||||
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
|
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
|
||||||
|
import xiaozhi.modules.agent.entity.AgentContextProviderEntity;
|
||||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||||
import xiaozhi.modules.agent.entity.AgentPluginMapping;
|
import xiaozhi.modules.agent.entity.AgentPluginMapping;
|
||||||
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
||||||
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
import xiaozhi.modules.agent.service.AgentChatHistoryService;
|
||||||
|
import xiaozhi.modules.agent.service.AgentContextProviderService;
|
||||||
import xiaozhi.modules.agent.service.AgentPluginMappingService;
|
import xiaozhi.modules.agent.service.AgentPluginMappingService;
|
||||||
import xiaozhi.modules.agent.service.AgentService;
|
import xiaozhi.modules.agent.service.AgentService;
|
||||||
import xiaozhi.modules.agent.service.AgentTemplateService;
|
import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||||
@@ -62,6 +64,7 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
|||||||
private final AgentChatHistoryService agentChatHistoryService;
|
private final AgentChatHistoryService agentChatHistoryService;
|
||||||
private final AgentTemplateService agentTemplateService;
|
private final AgentTemplateService agentTemplateService;
|
||||||
private final ModelProviderService modelProviderService;
|
private final ModelProviderService modelProviderService;
|
||||||
|
private final AgentContextProviderService agentContextProviderService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PageData<AgentEntity> adminAgentList(Map<String, Object> params) {
|
public PageData<AgentEntity> adminAgentList(Map<String, Object> params) {
|
||||||
@@ -85,6 +88,13 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
|||||||
agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.RECORD_TEXT_AUDIO.getCode());
|
agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.RECORD_TEXT_AUDIO.getCode());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 查询上下文源配置
|
||||||
|
AgentContextProviderEntity contextProviderEntity = agentContextProviderService.getByAgentId(id);
|
||||||
|
if (contextProviderEntity != null) {
|
||||||
|
agent.setContextProviders(contextProviderEntity.getContextProviders());
|
||||||
|
}
|
||||||
|
|
||||||
// 无需额外查询插件列表,已通过SQL查询出来
|
// 无需额外查询插件列表,已通过SQL查询出来
|
||||||
return agent;
|
return agent;
|
||||||
}
|
}
|
||||||
@@ -331,6 +341,14 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
|||||||
agentChatHistoryService.deleteByAgentId(existingEntity.getId(), true, false);
|
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());
|
boolean b = validateLLMIntentParams(dto.getLlmModelId(), dto.getIntentModelId());
|
||||||
if (!b) {
|
if (!b) {
|
||||||
throw new RenException(ErrorCode.LLM_INTENT_PARAMS_MISMATCH);
|
throw new RenException(ErrorCode.LLM_INTENT_PARAMS_MISMATCH);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
|||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
import xiaozhi.modules.agent.dto.ContextProviderDTO;
|
||||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||||
import xiaozhi.modules.agent.entity.AgentPluginMapping;
|
import xiaozhi.modules.agent.entity.AgentPluginMapping;
|
||||||
|
|
||||||
@@ -21,4 +22,7 @@ public class AgentInfoVO extends AgentEntity
|
|||||||
@Schema(description = "插件列表Id")
|
@Schema(description = "插件列表Id")
|
||||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||||
private List<AgentPluginMapping> functions;
|
private List<AgentPluginMapping> functions;
|
||||||
|
|
||||||
|
@Schema(description = "上下文源配置")
|
||||||
|
private List<ContextProviderDTO> contextProviders;
|
||||||
}
|
}
|
||||||
|
|||||||
+10
@@ -20,10 +20,12 @@ import xiaozhi.common.redis.RedisUtils;
|
|||||||
import xiaozhi.common.utils.ConvertUtils;
|
import xiaozhi.common.utils.ConvertUtils;
|
||||||
import xiaozhi.common.utils.JsonUtils;
|
import xiaozhi.common.utils.JsonUtils;
|
||||||
import xiaozhi.modules.agent.dao.AgentVoicePrintDao;
|
import xiaozhi.modules.agent.dao.AgentVoicePrintDao;
|
||||||
|
import xiaozhi.modules.agent.entity.AgentContextProviderEntity;
|
||||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||||
import xiaozhi.modules.agent.entity.AgentPluginMapping;
|
import xiaozhi.modules.agent.entity.AgentPluginMapping;
|
||||||
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
||||||
import xiaozhi.modules.agent.entity.AgentVoicePrintEntity;
|
import xiaozhi.modules.agent.entity.AgentVoicePrintEntity;
|
||||||
|
import xiaozhi.modules.agent.service.AgentContextProviderService;
|
||||||
import xiaozhi.modules.agent.service.AgentMcpAccessPointService;
|
import xiaozhi.modules.agent.service.AgentMcpAccessPointService;
|
||||||
import xiaozhi.modules.agent.service.AgentPluginMappingService;
|
import xiaozhi.modules.agent.service.AgentPluginMappingService;
|
||||||
import xiaozhi.modules.agent.service.AgentService;
|
import xiaozhi.modules.agent.service.AgentService;
|
||||||
@@ -53,6 +55,7 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
private final TimbreService timbreService;
|
private final TimbreService timbreService;
|
||||||
private final AgentPluginMappingService agentPluginMappingService;
|
private final AgentPluginMappingService agentPluginMappingService;
|
||||||
private final AgentMcpAccessPointService agentMcpAccessPointService;
|
private final AgentMcpAccessPointService agentMcpAccessPointService;
|
||||||
|
private final AgentContextProviderService agentContextProviderService;
|
||||||
private final VoiceCloneService cloneVoiceService;
|
private final VoiceCloneService cloneVoiceService;
|
||||||
private final AgentVoicePrintDao agentVoicePrintDao;
|
private final AgentVoicePrintDao agentVoicePrintDao;
|
||||||
|
|
||||||
@@ -178,6 +181,13 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
mcpEndpoint = mcpEndpoint.replace("/mcp/", "/call/");
|
mcpEndpoint = mcpEndpoint.replace("/mcp/", "/call/");
|
||||||
result.put("mcp_endpoint", mcpEndpoint);
|
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);
|
buildVoiceprintConfig(agent.getId(), result);
|
||||||
|
|
||||||
|
|||||||
@@ -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='智能体上下文源配置表';
|
||||||
@@ -423,6 +423,13 @@ databaseChangeLog:
|
|||||||
- sqlFile:
|
- sqlFile:
|
||||||
encoding: utf8
|
encoding: utf8
|
||||||
path: classpath:db/changelog/202511131023.sql
|
path: classpath:db/changelog/202511131023.sql
|
||||||
|
- changeSet:
|
||||||
|
id: 202512041515
|
||||||
|
author: cgd
|
||||||
|
changes:
|
||||||
|
- sqlFile:
|
||||||
|
encoding: utf8
|
||||||
|
path: classpath:db/changelog/202512041515.sql
|
||||||
- changeSet:
|
- changeSet:
|
||||||
id: 202512031514
|
id: 202512031514
|
||||||
author: hrz
|
author: hrz
|
||||||
@@ -430,4 +437,3 @@ databaseChangeLog:
|
|||||||
- sqlFile:
|
- sqlFile:
|
||||||
encoding: utf8
|
encoding: utf8
|
||||||
path: classpath:db/changelog/202512031514.sql
|
path: classpath:db/changelog/202512031514.sql
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,329 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
:visible.sync="dialogVisible"
|
||||||
|
width="900px"
|
||||||
|
title="编辑源"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
custom-class="context-provider-dialog"
|
||||||
|
append-to-body
|
||||||
|
>
|
||||||
|
<div class="dialog-content">
|
||||||
|
<el-empty v-if="localProviders.length === 0" description="暂无上下文API">
|
||||||
|
<el-button type="primary" icon="el-icon-plus" @click="addProvider(0)">添加</el-button>
|
||||||
|
</el-empty>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-for="(provider, pIndex) in localProviders"
|
||||||
|
:key="pIndex"
|
||||||
|
class="provider-item"
|
||||||
|
>
|
||||||
|
<el-card class="provider-card" shadow="hover" :body-style="{ padding: '15px 20px' }">
|
||||||
|
<!-- URL Row -->
|
||||||
|
<div class="input-row">
|
||||||
|
<span class="label-text">接口地址</span>
|
||||||
|
<el-input
|
||||||
|
v-model="provider.url"
|
||||||
|
placeholder="http://api.example.com/data"
|
||||||
|
size="small"
|
||||||
|
class="flex-1"
|
||||||
|
></el-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Headers Section -->
|
||||||
|
<div class="headers-section">
|
||||||
|
<div class="label-text" style="margin-top: 6px;">请求头</div>
|
||||||
|
<div class="headers-list">
|
||||||
|
<div
|
||||||
|
v-for="(header, hIndex) in provider.headers"
|
||||||
|
:key="hIndex"
|
||||||
|
class="header-row"
|
||||||
|
>
|
||||||
|
<el-input
|
||||||
|
v-model="header.key"
|
||||||
|
placeholder="Key"
|
||||||
|
size="small"
|
||||||
|
style="width: 180px;"
|
||||||
|
></el-input>
|
||||||
|
<span class="separator">:</span>
|
||||||
|
<el-input
|
||||||
|
v-model="header.value"
|
||||||
|
placeholder="Value"
|
||||||
|
size="small"
|
||||||
|
class="flex-1"
|
||||||
|
></el-input>
|
||||||
|
|
||||||
|
<div class="row-controls">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
icon="el-icon-plus"
|
||||||
|
circle
|
||||||
|
size="mini"
|
||||||
|
plain
|
||||||
|
@click="addHeader(pIndex, hIndex + 1)"
|
||||||
|
></el-button>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
icon="el-icon-minus"
|
||||||
|
circle
|
||||||
|
size="mini"
|
||||||
|
plain
|
||||||
|
@click="removeHeader(pIndex, hIndex)"
|
||||||
|
></el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Empty Headers State -->
|
||||||
|
<div v-if="provider.headers.length === 0" class="header-row empty-header">
|
||||||
|
<span class="no-header-text">暂无 Headers</span>
|
||||||
|
<el-button
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-plus"
|
||||||
|
size="mini"
|
||||||
|
@click="addHeader(pIndex, 0)"
|
||||||
|
>添加 Header</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- Provider Block Controls (Right Side) -->
|
||||||
|
<div class="block-controls">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
icon="el-icon-plus"
|
||||||
|
circle
|
||||||
|
size="medium"
|
||||||
|
@click="addProvider(pIndex + 1)"
|
||||||
|
></el-button>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
icon="el-icon-minus"
|
||||||
|
circle
|
||||||
|
size="medium"
|
||||||
|
@click="removeProvider(pIndex)"
|
||||||
|
></el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleConfirm">确定</el-button>
|
||||||
|
</span>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'ContextProviderDialog',
|
||||||
|
props: {
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
providers: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
localProviders: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
dialogVisible: {
|
||||||
|
get() {
|
||||||
|
return this.visible;
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit('update:visible', val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
visible(val) {
|
||||||
|
if (val) {
|
||||||
|
this.initLocalData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
initLocalData() {
|
||||||
|
// 深拷贝并将 headers 对象转换为数组
|
||||||
|
this.localProviders = this.providers.map(p => {
|
||||||
|
const headers = p.headers || {};
|
||||||
|
return {
|
||||||
|
url: p.url || '',
|
||||||
|
headers: Object.entries(headers).map(([key, value]) => ({ key, value }))
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// 如果为空,添加一个默认块
|
||||||
|
if (this.localProviders.length === 0) {
|
||||||
|
this.localProviders.push({ url: '', headers: [{ key: '', value: '' }] });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addProvider(index) {
|
||||||
|
this.localProviders.splice(index, 0, {
|
||||||
|
url: '',
|
||||||
|
headers: [{ key: '', value: '' }]
|
||||||
|
});
|
||||||
|
},
|
||||||
|
removeProvider(index) {
|
||||||
|
this.localProviders.splice(index, 1);
|
||||||
|
},
|
||||||
|
addHeader(pIndex, hIndex) {
|
||||||
|
this.localProviders[pIndex].headers.splice(hIndex, 0, { key: '', value: '' });
|
||||||
|
},
|
||||||
|
removeHeader(pIndex, hIndex) {
|
||||||
|
this.localProviders[pIndex].headers.splice(hIndex, 1);
|
||||||
|
},
|
||||||
|
handleConfirm() {
|
||||||
|
const result = this.localProviders
|
||||||
|
.filter(p => p.url.trim() !== '')
|
||||||
|
.map(p => {
|
||||||
|
const headersObj = {};
|
||||||
|
p.headers.forEach(h => {
|
||||||
|
if (h.key.trim()) {
|
||||||
|
headersObj[h.key.trim()] = h.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
url: p.url.trim(),
|
||||||
|
headers: headersObj
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$emit('confirm', result);
|
||||||
|
this.dialogVisible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dialog-content {
|
||||||
|
max-height: 60vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 20px 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-content::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
.dialog-content::-webkit-scrollbar-thumb {
|
||||||
|
background: #dcdfe6;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.dialog-content::-webkit-scrollbar-track {
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-card {
|
||||||
|
flex: 1;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
border-left: 4px solid #409EFF; /* 左侧强调色 */
|
||||||
|
background-color: #fff;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.provider-card:hover {
|
||||||
|
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.block-controls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-text {
|
||||||
|
width: 60px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #606266;
|
||||||
|
text-align: right;
|
||||||
|
font-size: 13px;
|
||||||
|
white-space: nowrap;
|
||||||
|
line-height: 32px; /* 垂直居中对齐 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-1 {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headers-section {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headers-list {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
background: #fcfcfc;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px dashed #dcdfe6;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headers-list:hover {
|
||||||
|
border-color: #c0c4cc;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
color: #909399;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
margin-left: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-row:hover .row-controls {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-header {
|
||||||
|
justify-content: center;
|
||||||
|
padding: 10px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-header-text {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -55,10 +55,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="上下文源:" class="context-provider-item">
|
||||||
|
<div style="display: flex; align-items: center; justify-content: space-between;">
|
||||||
|
<span style="color: #606266; font-size: 13px;">
|
||||||
|
已成功添加 {{ currentContextProviders.length }} 个源。<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/master/docs/context-provider-integration.md" target="_blank" class="doc-link">如何部署上下文源</a>
|
||||||
|
</span>
|
||||||
|
<el-button
|
||||||
|
class="edit-function-btn"
|
||||||
|
size="small"
|
||||||
|
@click="openContextProviderDialog"
|
||||||
|
>
|
||||||
|
编辑源
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item :label="$t('roleConfig.roleIntroduction') + ':'">
|
<el-form-item :label="$t('roleConfig.roleIntroduction') + ':'">
|
||||||
<el-input
|
<el-input
|
||||||
type="textarea"
|
type="textarea"
|
||||||
rows="9"
|
rows="8"
|
||||||
resize="none"
|
resize="none"
|
||||||
:placeholder="$t('roleConfig.pleaseEnterContent')"
|
:placeholder="$t('roleConfig.pleaseEnterContent')"
|
||||||
v-model="form.systemPrompt"
|
v-model="form.systemPrompt"
|
||||||
@@ -71,7 +85,7 @@
|
|||||||
<el-form-item :label="$t('roleConfig.memoryHis') + ':'">
|
<el-form-item :label="$t('roleConfig.memoryHis') + ':'">
|
||||||
<el-input
|
<el-input
|
||||||
type="textarea"
|
type="textarea"
|
||||||
rows="6"
|
rows="4"
|
||||||
resize="none"
|
resize="none"
|
||||||
v-model="form.summaryMemory"
|
v-model="form.summaryMemory"
|
||||||
maxlength="2000"
|
maxlength="2000"
|
||||||
@@ -275,6 +289,11 @@
|
|||||||
@update-functions="handleUpdateFunctions"
|
@update-functions="handleUpdateFunctions"
|
||||||
@dialog-closed="handleDialogClosed"
|
@dialog-closed="handleDialogClosed"
|
||||||
/>
|
/>
|
||||||
|
<context-provider-dialog
|
||||||
|
:visible.sync="showContextProviderDialog"
|
||||||
|
:providers="currentContextProviders"
|
||||||
|
@confirm="handleUpdateContext"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -283,15 +302,17 @@ import Api from "@/apis/api";
|
|||||||
import { getServiceUrl } from "@/apis/api";
|
import { getServiceUrl } from "@/apis/api";
|
||||||
import RequestService from "@/apis/httpRequest";
|
import RequestService from "@/apis/httpRequest";
|
||||||
import FunctionDialog from "@/components/FunctionDialog.vue";
|
import FunctionDialog from "@/components/FunctionDialog.vue";
|
||||||
|
import ContextProviderDialog from "@/components/ContextProviderDialog.vue";
|
||||||
import HeaderBar from "@/components/HeaderBar.vue";
|
import HeaderBar from "@/components/HeaderBar.vue";
|
||||||
import i18n from "@/i18n";
|
import i18n from "@/i18n";
|
||||||
import featureManager from "@/utils/featureManager";
|
import featureManager from "@/utils/featureManager";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "RoleConfigPage",
|
name: "RoleConfigPage",
|
||||||
components: { HeaderBar, FunctionDialog },
|
components: { HeaderBar, FunctionDialog, ContextProviderDialog },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
showContextProviderDialog: false,
|
||||||
form: {
|
form: {
|
||||||
agentCode: "",
|
agentCode: "",
|
||||||
agentName: "",
|
agentName: "",
|
||||||
@@ -329,6 +350,7 @@ export default {
|
|||||||
voiceDetails: {}, // 保存完整的音色信息
|
voiceDetails: {}, // 保存完整的音色信息
|
||||||
showFunctionDialog: false,
|
showFunctionDialog: false,
|
||||||
currentFunctions: [],
|
currentFunctions: [],
|
||||||
|
currentContextProviders: [],
|
||||||
allFunctions: [],
|
allFunctions: [],
|
||||||
originalFunctions: [],
|
originalFunctions: [],
|
||||||
playingVoice: false,
|
playingVoice: false,
|
||||||
@@ -370,6 +392,7 @@ export default {
|
|||||||
paramInfo: item.params,
|
paramInfo: item.params,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
contextProviders: this.currentContextProviders,
|
||||||
};
|
};
|
||||||
Api.agent.updateAgentConfig(this.$route.query.agentId, configData, ({ data }) => {
|
Api.agent.updateAgentConfig(this.$route.query.agentId, configData, ({ data }) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
@@ -487,6 +510,9 @@ export default {
|
|||||||
// 后端只给了最小映射:[{ id, agentId, pluginId }, ...]
|
// 后端只给了最小映射:[{ id, agentId, pluginId }, ...]
|
||||||
const savedMappings = data.data.functions || [];
|
const savedMappings = data.data.functions || [];
|
||||||
|
|
||||||
|
// 加载上下文配置
|
||||||
|
this.currentContextProviders = data.data.contextProviders || [];
|
||||||
|
|
||||||
// 先保证 allFunctions 已经加载(如果没有,则先 fetchAllFunctions)
|
// 先保证 allFunctions 已经加载(如果没有,则先 fetchAllFunctions)
|
||||||
const ensureFuncs = this.allFunctions.length
|
const ensureFuncs = this.allFunctions.length
|
||||||
? Promise.resolve()
|
? Promise.resolve()
|
||||||
@@ -660,6 +686,12 @@ export default {
|
|||||||
this.showFunctionDialog = true;
|
this.showFunctionDialog = true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
openContextProviderDialog() {
|
||||||
|
this.showContextProviderDialog = true;
|
||||||
|
},
|
||||||
|
handleUpdateContext(providers) {
|
||||||
|
this.currentContextProviders = providers;
|
||||||
|
},
|
||||||
handleUpdateFunctions(selected) {
|
handleUpdateFunctions(selected) {
|
||||||
this.currentFunctions = selected;
|
this.currentFunctions = selected;
|
||||||
},
|
},
|
||||||
@@ -1374,4 +1406,18 @@ export default {
|
|||||||
height: 32px;
|
height: 32px;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.context-provider-item ::v-deep .el-form-item__label {
|
||||||
|
line-height: 42px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-link {
|
||||||
|
color: #1677ff;
|
||||||
|
text-decoration: none;
|
||||||
|
margin-left: 4px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -74,6 +74,7 @@
|
|||||||
- **今天农历:** {{lunar_date}}
|
- **今天农历:** {{lunar_date}}
|
||||||
- **用户所在城市:** {{local_address}}
|
- **用户所在城市:** {{local_address}}
|
||||||
- **当地未来7天天气:** {{weather_info}}
|
- **当地未来7天天气:** {{weather_info}}
|
||||||
|
{{ dynamic_context }}
|
||||||
</context>
|
</context>
|
||||||
|
|
||||||
<memory>
|
<memory>
|
||||||
|
|||||||
@@ -113,6 +113,14 @@ wakeup_words:
|
|||||||
# MCP接入点地址,地址格式为:ws://你的mcp接入点ip或者域名:端口号/mcp/?token=你的token
|
# MCP接入点地址,地址格式为:ws://你的mcp接入点ip或者域名:端口号/mcp/?token=你的token
|
||||||
# 详细教程 https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/mcp-endpoint-integration.md
|
# 详细教程 https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/mcp-endpoint-integration.md
|
||||||
mcp_endpoint: 你的接入点 websocket地址
|
mcp_endpoint: 你的接入点 websocket地址
|
||||||
|
|
||||||
|
# 数据上下文填充配置
|
||||||
|
# 用于在系统提示词中注入动态数据,如健康数据、股票信息等
|
||||||
|
context_providers:
|
||||||
|
- url: ""
|
||||||
|
headers:
|
||||||
|
Authorization: ""
|
||||||
|
|
||||||
# 插件的基础配置
|
# 插件的基础配置
|
||||||
plugins:
|
plugins:
|
||||||
# 获取天气插件的配置,这里填写你的api_key
|
# 获取天气插件的配置,这里填写你的api_key
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ class ConnectionHandler:
|
|||||||
self.conn_from_mqtt_gateway = False
|
self.conn_from_mqtt_gateway = False
|
||||||
|
|
||||||
# 初始化提示词管理器
|
# 初始化提示词管理器
|
||||||
self.prompt_manager = PromptManager(config, self.logger)
|
self.prompt_manager = PromptManager(self.config, self.logger)
|
||||||
|
|
||||||
async def handle_connection(self, ws):
|
async def handle_connection(self, ws):
|
||||||
try:
|
try:
|
||||||
@@ -630,6 +630,8 @@ class ConnectionHandler:
|
|||||||
self.chat_history_conf = int(private_config["chat_history_conf"])
|
self.chat_history_conf = int(private_config["chat_history_conf"])
|
||||||
if private_config.get("mcp_endpoint", None) is not None:
|
if private_config.get("mcp_endpoint", None) is not None:
|
||||||
self.config["mcp_endpoint"] = private_config["mcp_endpoint"]
|
self.config["mcp_endpoint"] = private_config["mcp_endpoint"]
|
||||||
|
if private_config.get("context_providers", None) is not None:
|
||||||
|
self.config["context_providers"] = private_config["context_providers"]
|
||||||
|
|
||||||
# 使用 run_in_executor 在线程池中执行 initialize_modules,避免阻塞主循环
|
# 使用 run_in_executor 在线程池中执行 initialize_modules,避免阻塞主循环
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import httpx
|
||||||
|
from typing import Dict, Any, List
|
||||||
|
from config.logger import setup_logging
|
||||||
|
|
||||||
|
TAG = __name__
|
||||||
|
|
||||||
|
class ContextDataProvider:
|
||||||
|
"""数据上下文填充,负责从配置的API获取数据"""
|
||||||
|
|
||||||
|
def __init__(self, config: Dict[str, Any], logger=None):
|
||||||
|
self.config = config
|
||||||
|
self.logger = logger or setup_logging()
|
||||||
|
self.context_data = ""
|
||||||
|
|
||||||
|
def fetch_all(self, device_id: str) -> str:
|
||||||
|
"""获取所有配置的上下文数据"""
|
||||||
|
context_providers = self.config.get("context_providers", [])
|
||||||
|
if not context_providers:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
formatted_lines = []
|
||||||
|
for provider in context_providers:
|
||||||
|
url = provider.get("url")
|
||||||
|
headers = provider.get("headers", {})
|
||||||
|
|
||||||
|
if not url:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
headers = headers.copy() if isinstance(headers, dict) else {}
|
||||||
|
# 将 device_id 添加到请求头
|
||||||
|
headers["device_id"] = device_id
|
||||||
|
|
||||||
|
# 发送请求
|
||||||
|
response = httpx.get(url, headers=headers, timeout=3)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
result = response.json()
|
||||||
|
if isinstance(result, dict):
|
||||||
|
if result.get("code") == 0:
|
||||||
|
data = result.get("data")
|
||||||
|
# 格式化数据
|
||||||
|
if isinstance(data, dict):
|
||||||
|
for k, v in data.items():
|
||||||
|
formatted_lines.append(f"- **{k}:** {v}")
|
||||||
|
elif isinstance(data, list):
|
||||||
|
for item in data:
|
||||||
|
formatted_lines.append(f"- {item}")
|
||||||
|
else:
|
||||||
|
formatted_lines.append(f"- {data}")
|
||||||
|
else:
|
||||||
|
self.logger.bind(tag=TAG).warning(f"API {url} 返回错误码: {result.get('msg')}")
|
||||||
|
else:
|
||||||
|
self.logger.bind(tag=TAG).warning(f"API {url} 返回的不是JSON字典")
|
||||||
|
else:
|
||||||
|
self.logger.bind(tag=TAG).warning(f"API {url} 请求失败: {response.status_code}")
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.bind(tag=TAG).error(f"获取上下文数据 {url} 失败: {e}")
|
||||||
|
|
||||||
|
# 将所有格式化后的行拼接成一个字符串
|
||||||
|
self.context_data = "\n".join(formatted_lines)
|
||||||
|
if self.context_data:
|
||||||
|
self.logger.bind(tag=TAG).debug(f"已注入动态上下文数据:\n{self.context_data}")
|
||||||
|
return self.context_data
|
||||||
@@ -4,7 +4,6 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import cnlunar
|
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
from config.logger import setup_logging
|
from config.logger import setup_logging
|
||||||
from jinja2 import Template
|
from jinja2 import Template
|
||||||
@@ -61,6 +60,11 @@ class PromptManager:
|
|||||||
self.cache_manager = cache_manager
|
self.cache_manager = cache_manager
|
||||||
self.CacheType = CacheType
|
self.CacheType = CacheType
|
||||||
|
|
||||||
|
# 初始化数据上下文填充
|
||||||
|
from core.utils.context_provider import ContextDataProvider
|
||||||
|
self.context_provider = ContextDataProvider(config, self.logger)
|
||||||
|
self.context_data = {}
|
||||||
|
|
||||||
self._load_base_template()
|
self._load_base_template()
|
||||||
|
|
||||||
def _load_base_template(self):
|
def _load_base_template(self):
|
||||||
@@ -184,6 +188,14 @@ class PromptManager:
|
|||||||
local_address = self._get_location_info(client_ip)
|
local_address = self._get_location_info(client_ip)
|
||||||
# 获取天气信息(使用全局缓存)
|
# 获取天气信息(使用全局缓存)
|
||||||
self._get_weather_info(conn, local_address)
|
self._get_weather_info(conn, local_address)
|
||||||
|
|
||||||
|
# 获取配置的上下文数据
|
||||||
|
if hasattr(conn, "device_id") and conn.device_id:
|
||||||
|
if self.base_prompt_template and "dynamic_context" in self.base_prompt_template:
|
||||||
|
self.context_data = self.context_provider.fetch_all(conn.device_id)
|
||||||
|
else:
|
||||||
|
self.context_data = ""
|
||||||
|
|
||||||
self.logger.bind(tag=TAG).debug(f"上下文信息更新完成")
|
self.logger.bind(tag=TAG).debug(f"上下文信息更新完成")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -230,6 +242,7 @@ class PromptManager:
|
|||||||
emojiList=EMOJI_List,
|
emojiList=EMOJI_List,
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
client_ip=client_ip,
|
client_ip=client_ip,
|
||||||
|
dynamic_context=self.context_data,
|
||||||
*args,
|
*args,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user