diff --git a/docs/FAQ.md b/docs/FAQ.md
index 83df8c2c..c40e7784 100644
--- a/docs/FAQ.md
+++ b/docs/FAQ.md
@@ -80,6 +80,7 @@ VAD:
7、[如何开启声纹识别](./voiceprint-integration.md)
8、[新闻插件源配置指南](./newsnow_plugin_config.md)
9、[知识库ragflow集成指南](./ragflow-integration.md)
+10、[如何部署上下文源](./context-provider-integration.md)
### 11、语音克隆、本地语音部署相关教程
1、[如何在智控台克隆音色](./huoshan-streamTTS-voice-cloning.md)
diff --git a/docs/context-provider-integration.md b/docs/context-provider-integration.md
new file mode 100644
index 00000000..e49ab0dc
--- /dev/null
+++ b/docs/context-provider-integration.md
@@ -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
+
+- **客厅温度:** 26℃
+- **客厅湿度:** 45%
+- **大门状态:** 已关闭
+
+```
+
+**情况 2:返回列表**
+```json
+{
+ "code": 0,
+ "data": [
+ "您有10个待办事项",
+ "当前汽车的行驶速度是100km每小时"
+ ]
+}
+```
+*注入效果:*
+```markdown
+
+- 您有10个待办事项
+- 当前汽车的行驶速度是100km每小时
+
+```
+
+## 配置指南
+
+### 方式 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
+
+【重要!以下信息已实时提供,无需调用工具查询,请直接使用:】
+- **设备ID:** {{device_id}}
+- **当前时间:** {{current_time}}
+...
+{{ dynamic_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服务已停止")
+```
diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java
index a4ad0863..c09aa141 100644
--- a/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java
+++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/controller/AgentController.java
@@ -44,6 +44,7 @@ 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.AgentContextProviderService;
import xiaozhi.modules.agent.service.AgentPluginMappingService;
import xiaozhi.modules.agent.service.AgentService;
import xiaozhi.modules.agent.service.AgentTemplateService;
@@ -64,6 +65,7 @@ public class AgentController {
private final AgentChatHistoryService agentChatHistoryService;
private final AgentChatAudioService agentChatAudioService;
private final AgentPluginMappingService agentPluginMappingService;
+ private final AgentContextProviderService agentContextProviderService;
private final RedisUtils redisUtils;
@GetMapping("/list")
@@ -135,6 +137,8 @@ public class AgentController {
agentChatHistoryService.deleteByAgentId(id, true, true);
// 删除关联的插件
agentPluginMappingService.deleteByAgentId(id);
+ // 删除关联的上下文源配置
+ agentContextProviderService.deleteByAgentId(id);
// 再删除智能体
agentService.deleteById(id);
return new Result<>();
diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AgentContextProviderDao.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AgentContextProviderDao.java
new file mode 100644
index 00000000..d46ad3ab
--- /dev/null
+++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/dao/AgentContextProviderDao.java
@@ -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 {
+}
diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentUpdateDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentUpdateDTO.java
index 0e3d9bc3..ebb29fbd 100644
--- a/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentUpdateDTO.java
+++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/AgentUpdateDTO.java
@@ -69,6 +69,9 @@ public class AgentUpdateDTO implements Serializable {
@Schema(description = "排序", example = "1", nullable = true)
private Integer sort;
+ @Schema(description = "上下文源配置", nullable = true)
+ private List contextProviders;
+
@Data
@Schema(description = "插件函数信息")
public static class FunctionInfo implements Serializable {
diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/ContextProviderDTO.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/ContextProviderDTO.java
new file mode 100644
index 00000000..0b8edfd8
--- /dev/null
+++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/dto/ContextProviderDTO.java
@@ -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 headers;
+}
diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentContextProviderEntity.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentContextProviderEntity.java
new file mode 100644
index 00000000..937556ef
--- /dev/null
+++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/entity/AgentContextProviderEntity.java
@@ -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 contextProviders;
+
+ @Schema(description = "创建者")
+ private Long creator;
+
+ @Schema(description = "创建时间")
+ private Date createdAt;
+
+ @Schema(description = "更新者")
+ private Long updater;
+
+ @Schema(description = "更新时间")
+ private Date updatedAt;
+}
diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentContextProviderService.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentContextProviderService.java
new file mode 100644
index 00000000..da759971
--- /dev/null
+++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/AgentContextProviderService.java
@@ -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 {
+ /**
+ * 根据智能体ID获取上下文源配置
+ * @param agentId 智能体ID
+ * @return 上下文源配置实体
+ */
+ AgentContextProviderEntity getByAgentId(String agentId);
+
+ /**
+ * 保存或更新上下文源配置
+ * @param entity 实体
+ */
+ void saveOrUpdateByAgentId(AgentContextProviderEntity entity);
+
+ /**
+ * 根据智能体ID删除上下文源配置
+ * @param agentId 智能体ID
+ */
+ void deleteByAgentId(String agentId);
+}
diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentContextProviderServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentContextProviderServiceImpl.java
new file mode 100644
index 00000000..b68ab8c9
--- /dev/null
+++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentContextProviderServiceImpl.java
@@ -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 implements AgentContextProviderService {
+
+ @Override
+ public AgentContextProviderEntity getByAgentId(String agentId) {
+ return baseDao.selectOne(new QueryWrapper().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().eq("agent_id", agentId));
+ }
+}
diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java
index cb550b3e..0adf0873 100644
--- a/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java
+++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/service/impl/AgentServiceImpl.java
@@ -32,10 +32,12 @@ 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;
@@ -62,6 +64,7 @@ public class AgentServiceImpl extends BaseServiceImpl imp
private final AgentChatHistoryService agentChatHistoryService;
private final AgentTemplateService agentTemplateService;
private final ModelProviderService modelProviderService;
+ private final AgentContextProviderService agentContextProviderService;
@Override
public PageData adminAgentList(Map params) {
@@ -85,6 +88,13 @@ public class AgentServiceImpl extends BaseServiceImpl imp
agent.setChatHistoryConf(Constant.ChatHistoryConfEnum.RECORD_TEXT_AUDIO.getCode());
}
}
+
+ // 查询上下文源配置
+ AgentContextProviderEntity contextProviderEntity = agentContextProviderService.getByAgentId(id);
+ if (contextProviderEntity != null) {
+ agent.setContextProviders(contextProviderEntity.getContextProviders());
+ }
+
// 无需额外查询插件列表,已通过SQL查询出来
return agent;
}
@@ -331,6 +341,14 @@ public class AgentServiceImpl extends BaseServiceImpl 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);
diff --git a/main/manager-api/src/main/java/xiaozhi/modules/agent/vo/AgentInfoVO.java b/main/manager-api/src/main/java/xiaozhi/modules/agent/vo/AgentInfoVO.java
index d56c8bb4..3da8b71c 100644
--- a/main/manager-api/src/main/java/xiaozhi/modules/agent/vo/AgentInfoVO.java
+++ b/main/manager-api/src/main/java/xiaozhi/modules/agent/vo/AgentInfoVO.java
@@ -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 functions;
+
+ @Schema(description = "上下文源配置")
+ private List contextProviders;
}
diff --git a/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java b/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java
index 3ab3d977..7ddb4b82 100644
--- a/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java
+++ b/main/manager-api/src/main/java/xiaozhi/modules/config/service/impl/ConfigServiceImpl.java
@@ -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;
@@ -178,6 +181,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);
diff --git a/main/manager-api/src/main/resources/db/changelog/202512041515.sql b/main/manager-api/src/main/resources/db/changelog/202512041515.sql
new file mode 100644
index 00000000..5e26b99f
--- /dev/null
+++ b/main/manager-api/src/main/resources/db/changelog/202512041515.sql
@@ -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='智能体上下文源配置表';
diff --git a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml
index c929f760..700b16f8 100755
--- a/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml
+++ b/main/manager-api/src/main/resources/db/changelog/db.changelog-master.yaml
@@ -423,11 +423,17 @@ databaseChangeLog:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202511131023.sql
+ - changeSet:
+ id: 202512041515
+ author: cgd
+ changes:
+ - sqlFile:
+ encoding: utf8
+ path: classpath:db/changelog/202512041515.sql
- changeSet:
id: 202512031514
author: hrz
changes:
- sqlFile:
encoding: utf8
- path: classpath:db/changelog/202512031514.sql
-
+ path: classpath:db/changelog/202512031514.sql
\ No newline at end of file
diff --git a/main/manager-web/src/components/ContextProviderDialog.vue b/main/manager-web/src/components/ContextProviderDialog.vue
new file mode 100644
index 00000000..21d31278
--- /dev/null
+++ b/main/manager-web/src/components/ContextProviderDialog.vue
@@ -0,0 +1,329 @@
+
+
+
+
+ 添加
+
+
+
+
+
+
+ 接口地址
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/main/manager-web/src/views/roleConfig.vue b/main/manager-web/src/views/roleConfig.vue
index c9ea518c..16e0f19e 100644
--- a/main/manager-web/src/views/roleConfig.vue
+++ b/main/manager-web/src/views/roleConfig.vue
@@ -55,10 +55,24 @@
+
+
+
+ 已成功添加 {{ currentContextProviders.length }} 个源。如何部署上下文源
+
+
+ 编辑源
+
+
+
+
@@ -283,15 +302,17 @@ import Api from "@/apis/api";
import { getServiceUrl } from "@/apis/api";
import RequestService from "@/apis/httpRequest";
import FunctionDialog from "@/components/FunctionDialog.vue";
+import ContextProviderDialog from "@/components/ContextProviderDialog.vue";
import HeaderBar from "@/components/HeaderBar.vue";
import i18n from "@/i18n";
import featureManager from "@/utils/featureManager";
export default {
name: "RoleConfigPage",
- components: { HeaderBar, FunctionDialog },
+ components: { HeaderBar, FunctionDialog, ContextProviderDialog },
data() {
return {
+ showContextProviderDialog: false,
form: {
agentCode: "",
agentName: "",
@@ -329,6 +350,7 @@ export default {
voiceDetails: {}, // 保存完整的音色信息
showFunctionDialog: false,
currentFunctions: [],
+ currentContextProviders: [],
allFunctions: [],
originalFunctions: [],
playingVoice: false,
@@ -370,6 +392,7 @@ export default {
paramInfo: item.params,
};
}),
+ contextProviders: this.currentContextProviders,
};
Api.agent.updateAgentConfig(this.$route.query.agentId, configData, ({ data }) => {
if (data.code === 0) {
@@ -486,6 +509,9 @@ export default {
};
// 后端只给了最小映射:[{ id, agentId, pluginId }, ...]
const savedMappings = data.data.functions || [];
+
+ // 加载上下文配置
+ this.currentContextProviders = data.data.contextProviders || [];
// 先保证 allFunctions 已经加载(如果没有,则先 fetchAllFunctions)
const ensureFuncs = this.allFunctions.length
@@ -660,6 +686,12 @@ export default {
this.showFunctionDialog = true;
}
},
+ openContextProviderDialog() {
+ this.showContextProviderDialog = true;
+ },
+ handleUpdateContext(providers) {
+ this.currentContextProviders = providers;
+ },
handleUpdateFunctions(selected) {
this.currentFunctions = selected;
},
@@ -1374,4 +1406,18 @@ export default {
height: 32px;
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;
+ }
+}
diff --git a/main/xiaozhi-server/agent-base-prompt.txt b/main/xiaozhi-server/agent-base-prompt.txt
index 490b23d3..7fd90d21 100644
--- a/main/xiaozhi-server/agent-base-prompt.txt
+++ b/main/xiaozhi-server/agent-base-prompt.txt
@@ -74,6 +74,7 @@
- **今天农历:** {{lunar_date}}
- **用户所在城市:** {{local_address}}
- **当地未来7天天气:** {{weather_info}}
+{{ dynamic_context }}
diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml
index a4087395..13d9fcb3 100644
--- a/main/xiaozhi-server/config.yaml
+++ b/main/xiaozhi-server/config.yaml
@@ -113,6 +113,14 @@ wakeup_words:
# MCP接入点地址,地址格式为:ws://你的mcp接入点ip或者域名:端口号/mcp/?token=你的token
# 详细教程 https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/mcp-endpoint-integration.md
mcp_endpoint: 你的接入点 websocket地址
+
+# 数据上下文填充配置
+# 用于在系统提示词中注入动态数据,如健康数据、股票信息等
+context_providers:
+ - url: ""
+ headers:
+ Authorization: ""
+
# 插件的基础配置
plugins:
# 获取天气插件的配置,这里填写你的api_key
diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py
index 31bfa0c6..6b34e32f 100644
--- a/main/xiaozhi-server/core/connection.py
+++ b/main/xiaozhi-server/core/connection.py
@@ -162,7 +162,7 @@ class ConnectionHandler:
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):
try:
@@ -630,6 +630,8 @@ class ConnectionHandler:
self.chat_history_conf = int(private_config["chat_history_conf"])
if private_config.get("mcp_endpoint", None) is not None:
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,避免阻塞主循环
try:
diff --git a/main/xiaozhi-server/core/utils/context_provider.py b/main/xiaozhi-server/core/utils/context_provider.py
new file mode 100644
index 00000000..5f317540
--- /dev/null
+++ b/main/xiaozhi-server/core/utils/context_provider.py
@@ -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
diff --git a/main/xiaozhi-server/core/utils/prompt_manager.py b/main/xiaozhi-server/core/utils/prompt_manager.py
index 444b16ee..4d8d56af 100644
--- a/main/xiaozhi-server/core/utils/prompt_manager.py
+++ b/main/xiaozhi-server/core/utils/prompt_manager.py
@@ -4,7 +4,6 @@
"""
import os
-import cnlunar
from typing import Dict, Any
from config.logger import setup_logging
from jinja2 import Template
@@ -60,6 +59,11 @@ class PromptManager:
self.cache_manager = cache_manager
self.CacheType = CacheType
+
+ # 初始化数据上下文填充
+ from core.utils.context_provider import ContextDataProvider
+ self.context_provider = ContextDataProvider(config, self.logger)
+ self.context_data = {}
self._load_base_template()
@@ -184,6 +188,14 @@ class PromptManager:
local_address = self._get_location_info(client_ip)
# 获取天气信息(使用全局缓存)
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"上下文信息更新完成")
except Exception as e:
@@ -230,6 +242,7 @@ class PromptManager:
emojiList=EMOJI_List,
device_id=device_id,
client_ip=client_ip,
+ dynamic_context=self.context_data,
*args,
**kwargs,
)