Merge branch 'py-fix-repeated-text' into fix/duplicate-tts-on-tool-call

This commit is contained in:
Sakura-RanChen
2026-04-20 10:07:28 +08:00
committed by GitHub
95 changed files with 1447 additions and 3904 deletions
+2 -2
View File
@@ -214,8 +214,8 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|:---:|:---:|:---:|
| ASR(语音识别) | FunASR(本地) | 👍XunfeiStreamASR(讯飞流式) |
| LLM(大模型) | glm-4-flash(智谱) | 👍qwen-flash(阿里百炼) |
| VLLM(视觉大模型) | glm-4v-flash(智谱) | 👍qwen2.5-vl-3b-instructh(阿里百炼) |
| TTS(语音合成) | ✅LinkeraiTTS(灵犀流式) | 👍HuoshanDoubleStreamTTS(火山流式) |
| VLLM(视觉大模型) | glm-4v-flash(智谱) | 👍qwen3.5-flash(阿里百炼) |
| TTS(语音合成) | EdgeTTS(微软) | 👍HuoshanDoubleStreamTTS(火山流式) |
| Intent(意图识别) | function_call(函数调用) | function_call(函数调用) |
| Memory(记忆功能) | mem_local_short(本地短期记忆) | mem_local_short(本地短期记忆) |
+2 -2
View File
@@ -212,8 +212,8 @@ Websocket-Schnittstellenadresse: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|:---:|:---:|:---:|
| 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) |
| VLLM (Vision Large Model) | glm-4v-flash (Zhipu) | 👍qwen3.5-flash (Alibaba Bailian) |
| TTS (Sprachsynthese) | EdgeTTS (Microsoft) | 👍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) |
+2 -2
View File
@@ -212,8 +212,8 @@ Websocket Interface Address: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|:---:|:---:|:---:|
| 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) |
| VLLM(Vision Large Model) | glm-4v-flash(Zhipu) | 👍qwen3.5-flash(Alibaba Bailian) |
| TTS(Speech Synthesis) | EdgeTTS(Microsoft) | 👍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) |
+2 -2
View File
@@ -212,8 +212,8 @@ Endereço da Interface WebSocket: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|:---:|:---:|:---:|
| ASR(Reconhecimento de Fala) | FunASR(Local) | 👍XunfeiStreamASR(Xunfei Streaming) |
| LLM(Modelo de Linguagem) | glm-4-flash(Zhipu) | 👍qwen-flash(Alibaba Bailian) |
| VLLM(Modelo de Visão) | glm-4v-flash(Zhipu) | 👍qwen2.5-vl-3b-instructh(Alibaba Bailian) |
| TTS(Síntese de Voz) | ✅LinkeraiTTS(Lingxi streaming) | 👍HuoshanDoubleStreamTTS(Volcano Streaming) |
| VLLM(Modelo de Visão) | glm-4v-flash(Zhipu) | 👍qwen3.5-flash(Alibaba Bailian) |
| TTS(Síntese de Voz) | EdgeTTS(Microsoft) | 👍HuoshanDoubleStreamTTS(Volcano Streaming) |
| Intent(Reconhecimento de Intenção) | function_call(Chamada de função) | function_call(Chamada de função) |
| Memory(Função de Memória) | mem_local_short(Memória local de curto prazo) | mem_local_short(Memória local de curto prazo) |
+2 -2
View File
@@ -213,8 +213,8 @@ Công cụ kiểm tra dịch vụ: https://2662r3426b.vicp.fun/test/
|:---:|:---:|:---:|
| 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) |
| VLLM(Mô hình lớn thị giác) | glm-4v-flash(Zhipu) | 👍qwen3.5-flash(Alibaba Bailian) |
| TTS(Tổng hợp giọng nói) | EdgeTTS(Microsoft) | 👍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ộ) |
+2 -2
View File
@@ -40,8 +40,8 @@ conda install conda-forge::ffmpeg
|:---:|:---:|:---:|
| ASR(语音识别) | FunASR(本地) | 👍XunfeiStreamASR(讯飞流式) |
| LLM(大模型) | glm-4-flash(智谱) | 👍qwen-flash(阿里百炼) |
| VLLM(视觉大模型) | glm-4v-flash(智谱) | 👍qwen2.5-vl-3b-instructh(阿里百炼) |
| TTS(语音合成) | ✅LinkeraiTTS(灵犀流式) | 👍HuoshanDoubleStreamTTS(火山流式) |
| VLLM(视觉大模型) | glm-4v-flash(智谱) | 👍qwen3.5-flash(阿里百炼) |
| TTS(语音合成) | EdgeTTS(微软) | 👍HuoshanDoubleStreamTTS(火山流式) |
| Intent(意图识别) | function_call(函数调用) | function_call(函数调用) |
| Memory(记忆功能) | mem_local_short(本地短期记忆) | mem_local_short(本地短期记忆) |
@@ -1,45 +0,0 @@
# 知识库模块全量集成测试报告
## 1. 测试背景
针对 `KnowledgeBaseController``KnowledgeFilesController` 共 14 个接口进行了深度集成测试。主要解决了本地影子库与 RAGFlow 远程服务之间的状态对齐、数据反序列化兼容性以及批量操作逻辑安全性问题。
## 2. 修复的核心 Bug 清单 (Hotfixes)
| 模块 | 问题类型 | 修复方案 | 验证结果 |
| :--- | :--- | :--- | :--- |
| **DTO** | `positions` 反序列化失败 | 类型从 `List<Integer>` 提升为 `Object`,支持嵌套数组 | ✅ 已验证 |
| **DTO** | 日期格式不兼容 | 针对 RAGFlow 的 RFC 1123 格式,将 `Date` 改为 `String` 透传 | ✅ 已验证 |
| **请求** | 检索参数 `null` 拒绝 | 增加 `@JsonInclude(NON_NULL)`,跳过可选字段的空值序列化 | ✅ 已验证 |
| **同步** | 状态自愈死锁 | 增加 `CANCEL/FAIL` 状态的 60s 低频同步机制,防止逻辑错误锁定 | ✅ 已验证 |
| **逻辑** | 删除守卫逻辑错误 | 将拦截条件从 `status="1"` 修正为 `run="RUNNING"` | ✅ 已验证 |
## 3. 全量接口测试统计
### KnowledgeBaseController (7/7)
- [x] 分页查询 (`GET /datasets`)
- [x] 详情获取 (`GET /datasets/{id}`)
- [x] 创建知识库 (`POST /datasets`)
- [x] 修改配置 (`PUT /datasets/{id}`)
- [x] 物理删除 (`DELETE /datasets/{id}`)
- [x] 批量删除 (`DELETE /datasets/batch`)
- [x] 模型列表获取 (`GET /datasets/rag-models`)
### KnowledgeFilesController (7/7)
- [x] 文档列表与同步 (`GET /datasets/{id}/documents`)
- [x] 状态过滤查询 (`GET /datasets/{id}/documents/status/{s}`)
- [x] 文档上传 (`POST /datasets/{id}/documents`)
- [x] 触发解析 (`POST /datasets/{id}/chunks`)
- [x] 切片详情 (`GET /datasets/{id}/documents/{docId}/chunks`)
- [x] 召回测试 (`POST /datasets/{id}/retrieval-test`)
- [x] 批量删除文档 (`DELETE /datasets/{id}/documents`)
## 4. 自动化审计结论
通过执行 `comprehensive_audit.ps1` 自动化脚本,模拟了“创建->上传->解析->同步->检索->删除”的完整生产链路。
- **解析成功率**100%
- **数据准确性**:DTO 转换无异常,坐标及得分提取正常
- **系统安全性**:解析中拦截机制生效
- **结论****准生产就绪 (Production Ready)**
---
*报告生成时间:2026-02-13*
*审核:dora--1206563805@qq.com*
@@ -156,6 +156,16 @@ public interface Constant {
*/
String MEMORY_MEM_REPORT_ONLY = "Memory_mem_report_only";
/**
* Mem0AI记忆
*/
String MEMORY_MEM0AI = "Memory_mem0ai";
/**
* PowerMem记忆
*/
String MEMORY_POWERMEM = "Memory_powermem";
/**
* 火山引擎双声道语音克隆
*/
@@ -150,6 +150,13 @@ public class AgentController {
}
}
@PostMapping("/chat-title/{sessionId}/generate")
@Operation(summary = "根据会话ID生成聊天标题")
public Result<Void> generateAndSaveChatTitle(@PathVariable String sessionId) {
agentChatSummaryService.generateAndSaveChatTitle(sessionId);
return new Result<Void>().ok(null);
}
@PutMapping("/{id}")
@Operation(summary = "更新智能体")
@RequiresPermissions("sys:role:normal")
@@ -0,0 +1,12 @@
package xiaozhi.modules.agent.dao;
import org.apache.ibatis.annotations.Mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import xiaozhi.modules.agent.entity.AgentChatTitleEntity;
@Mapper
public interface AgentChatTitleDao extends BaseMapper<AgentChatTitleEntity> {
}
@@ -23,4 +23,9 @@ public class AgentChatSessionDTO {
* 聊天条数
*/
private Integer chatCount;
/**
* 会话标题
*/
private String title;
}
@@ -33,6 +33,9 @@ public class AgentUpdateDTO implements Serializable {
@Schema(description = "大语言模型标识", example = "llm_model_02", nullable = true)
private String llmModelId;
@Schema(description = "小模型标识", example = "slm_model_02", nullable = true)
private String slmModelId;
@Schema(description = "VLLM模型标识", example = "vllm_model_02", required = false)
private String vllmModelId;
@@ -0,0 +1,36 @@
package xiaozhi.modules.agent.entity;
import java.util.Date;
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 lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "ai_agent_chat_title")
public class AgentChatTitleEntity {
@TableId(type = IdType.ASSIGN_UUID)
private String id;
@TableField(value = "session_id")
private String sessionId;
@TableField(value = "title")
private String title;
@TableField(value = "created_at")
private Date createdAt;
@TableField(value = "updated_at")
private Date updatedAt;
}
@@ -37,6 +37,9 @@ public class AgentEntity {
@Schema(description = "大语言模型标识")
private String llmModelId;
@Schema(description = "小模型标识")
private String slmModelId;
@Schema(description = "VLLM模型标识")
private String vllmModelId;
@@ -12,4 +12,12 @@ public interface AgentChatSummaryService {
* @return 保存结果
*/
boolean generateAndSaveChatSummary(String sessionId);
/**
* 根据会话ID生成聊天标题并保存
*
* @param sessionId 会话ID
* @return 是否成功
*/
boolean generateAndSaveChatTitle(String sessionId);
}
@@ -0,0 +1,10 @@
package xiaozhi.modules.agent.service;
import xiaozhi.modules.agent.entity.AgentChatTitleEntity;
public interface AgentChatTitleService {
void saveOrUpdateTitle(String sessionId, String title);
String getTitleBySessionId(String sessionId);
}
@@ -6,6 +6,7 @@ import java.util.Map;
import java.util.stream.Collectors;
import cn.hutool.core.collection.ListUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -26,6 +27,7 @@ import xiaozhi.modules.agent.dto.AgentChatHistoryDTO;
import xiaozhi.modules.agent.dto.AgentChatSessionDTO;
import xiaozhi.modules.agent.entity.AgentChatHistoryEntity;
import xiaozhi.modules.agent.service.AgentChatHistoryService;
import xiaozhi.modules.agent.service.AgentChatTitleService;
import xiaozhi.modules.agent.vo.AgentChatHistoryUserVO;
/**
@@ -36,9 +38,12 @@ import xiaozhi.modules.agent.vo.AgentChatHistoryUserVO;
* @since 1.0.0
*/
@Service
@RequiredArgsConstructor
public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryDao, AgentChatHistoryEntity>
implements AgentChatHistoryService {
private final AgentChatTitleService agentChatTitleService;
@Override
public PageData<AgentChatSessionDTO> getSessionListByAgentId(Map<String, Object> params) {
String agentId = (String) params.get("agentId");
@@ -61,6 +66,7 @@ public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryD
dto.setSessionId((String) map.get("session_id"));
dto.setCreatedAt((LocalDateTime) map.get("created_at"));
dto.setChatCount(((Number) map.get("chat_count")).intValue());
dto.setTitle(agentChatTitleService.getTitleBySessionId(dto.getSessionId()));
return dto;
}).collect(Collectors.toList());
@@ -91,7 +97,7 @@ public class AgentChatHistoryServiceImpl extends ServiceImpl<AiAgentChatHistoryD
if (ToolUtil.isNotEmpty(audioIds)) {
// 每批删除1000条
List<List<String>> batch = ListUtil.split(audioIds, 1000);
batch.forEach(dataList->{
batch.forEach(dataList -> {
baseMapper.deleteAudioByIds(dataList);
});
}
@@ -22,6 +22,7 @@ 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.AgentChatTitleService;
import xiaozhi.modules.agent.service.AgentService;
import xiaozhi.modules.agent.vo.AgentInfoVO;
import xiaozhi.modules.device.entity.DeviceEntity;
@@ -42,6 +43,7 @@ public class AgentChatSummaryServiceImpl implements AgentChatSummaryService {
private final AgentChatHistoryService agentChatHistoryService;
private final AgentService agentService;
private final AgentChatTitleService agentChatTitleService;
private final DeviceService deviceService;
private final LLMService llmService;
private final ModelConfigService modelConfigService;
@@ -91,40 +93,40 @@ public class AgentChatSummaryServiceImpl implements AgentChatSummaryService {
@Override
public boolean generateAndSaveChatSummary(String sessionId) {
try {
// 1. 获取设备信息(通过会话关联的设备)
DeviceEntity device = getDeviceBySessionId(sessionId);
if (device == null) {
log.info("未找到与会话 {} 关联的设备", sessionId);
return false;
}
// 2. 检查记忆模型类型,如果是仅上报聊天记录模式则跳过总结
String memModelId = agentService.getAgentById(device.getAgentId()).getMemModelId();
if (memModelId != null && memModelId.equals(Constant.MEMORY_MEM_REPORT_ONLY)) {
String agentId = device.getAgentId();
String memModelId = agentService.getAgentById(agentId).getMemModelId();
if (memModelId == null || memModelId.equals(Constant.MEMORY_MEM_REPORT_ONLY)) {
log.info("会话 {} 使用仅上报聊天记录模式,跳过记忆总结", sessionId);
return true;
}
// 3. 生成总结
AgentChatSummaryDTO summaryDTO = generateChatSummary(sessionId);
if (!summaryDTO.isSuccess()) {
log.info("生成总结失败: {}", summaryDTO.getErrorMessage());
return false;
}
boolean shouldSummarizeMemory = !memModelId.equals(Constant.MEMORY_NO_MEM)
&& !memModelId.equals(Constant.MEMORY_MEM0AI)
&& !memModelId.equals(Constant.MEMORY_POWERMEM);
// 4. 更新智能体记忆
AgentMemoryDTO memoryDTO = new AgentMemoryDTO();
memoryDTO.setSummaryMemory(summaryDTO.getSummary());
// 调用现有接口更新记忆
agentService.updateAgentById(device.getAgentId(),
new AgentUpdateDTO() {
if (shouldSummarizeMemory) {
AgentChatSummaryDTO summaryDTO = generateChatSummary(sessionId);
if (summaryDTO.isSuccess()) {
agentService.updateAgentById(agentId, new AgentUpdateDTO() {
{
setSummaryMemory(summaryDTO.getSummary());
}
});
log.info("成功保存会话 {} 的聊天记录总结到智能体 {}", sessionId, agentId);
} else {
log.info("生成总结失败: {}", summaryDTO.getErrorMessage());
}
} else {
log.info("会话 {} 使用 {} 模式,跳过记忆总结", sessionId, memModelId);
}
log.info("成功保存会话 {} 的聊天记录总结到智能体 {}", sessionId, device.getAgentId());
return true;
} catch (Exception e) {
@@ -133,6 +135,98 @@ public class AgentChatSummaryServiceImpl implements AgentChatSummaryService {
}
}
@Override
public boolean generateAndSaveChatTitle(String sessionId) {
try {
// 自动获取agentId
String agentId = findAgentIdBySessionId(sessionId);
if (StringUtils.isBlank(agentId)) {
log.warn("会话 {} 无法获取智能体信息,跳过标题生成", sessionId);
return false;
}
List<AgentChatHistoryDTO> chatHistory = getChatHistoryBySessionId(sessionId);
if (chatHistory == null || chatHistory.isEmpty()) {
return false;
}
List<String> meaningfulMessages = extractMeaningfulMessages(chatHistory);
if (meaningfulMessages.isEmpty()) {
return false;
}
StringBuilder conversation = new StringBuilder();
for (int i = 0; i < meaningfulMessages.size(); i++) {
conversation.append("消息").append(i + 1).append(": ").append(meaningfulMessages.get(i)).append("\n");
}
String slmModelId = getSlmModelId(agentId);
String title = llmService.generateTitle(conversation.toString(), slmModelId);
if (StringUtils.isNotBlank(title)) {
agentChatTitleService.saveOrUpdateTitle(sessionId, title);
log.info("成功保存会话 {} 的标题: {}", sessionId, title);
return true;
}
return false;
} catch (Exception e) {
log.error("生成会话 {} 的标题时发生错误: {}", sessionId, e.getMessage());
return false;
}
}
private String getSlmModelId(String agentId) {
try {
if (StringUtils.isBlank(agentId)) {
return null;
}
AgentInfoVO agentInfo = agentService.getAgentById(agentId);
if (agentInfo == null) {
return null;
}
String slmModelId = agentInfo.getSlmModelId();
if (StringUtils.isNotBlank(slmModelId)) {
log.info("会话 {} 使用SLM模型: {}", agentId, slmModelId);
return slmModelId;
}
ModelConfigEntity defaultLlmConfig = getDefaultLLMConfig();
if (defaultLlmConfig != null) {
log.info("会话 {} 使用默认LLM模型: {}", agentId, defaultLlmConfig.getId());
return defaultLlmConfig.getId();
}
String llmModelId = agentInfo.getLlmModelId();
log.info("会话 {} 使用LLM模型(最终回退): {}", agentId, llmModelId);
return llmModelId;
} catch (Exception e) {
log.error("获取智能体slm模型ID失败,agentId: {}, 错误: {}", agentId, e.getMessage());
return null;
}
}
private ModelConfigEntity getDefaultLLMConfig() {
try {
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.getMessage());
return null;
}
}
/**
* 根据会话ID获取聊天记录
*/
@@ -313,15 +407,13 @@ public class AgentChatSummaryServiceImpl implements AgentChatSummaryService {
*/
private String callJavaLLMForSummaryWithHistory(String conversation, String historyMemory, String agentId) {
try {
// 获取智能体配置,从中提取记忆总结的模型ID
String modelId = getMemorySummaryModelId(agentId);
String modelId = getSlmModelId(agentId);
if (StringUtils.isBlank(modelId)) {
log.info("未找到记忆总结的LLM模型配置,使用默认LLM服务");
log.info("未找到SLM模型,使用默认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("总结生成失败")) {
@@ -341,15 +433,13 @@ public class AgentChatSummaryServiceImpl implements AgentChatSummaryService {
*/
private String callJavaLLMForSummary(String conversation, String agentId) {
try {
// 获取智能体配置,从中提取记忆总结的模型ID
String modelId = getMemorySummaryModelId(agentId);
String modelId = getSlmModelId(agentId);
if (StringUtils.isBlank(modelId)) {
log.info("未找到记忆总结的LLM模型配置,使用默认LLM服务");
log.info("未找到SLM模型,使用默认LLM服务");
return llmService.generateSummary(conversation);
}
// 使用指定的模型ID调用LLM服务
String summary = llmService.generateSummaryWithModel(conversation, modelId);
if (StringUtils.isNotBlank(summary) && !summary.equals("服务暂不可用") && !summary.equals("总结生成失败")) {
@@ -0,0 +1,60 @@
package xiaozhi.modules.agent.service.impl;
import java.util.Date;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import lombok.RequiredArgsConstructor;
import xiaozhi.modules.agent.dao.AgentChatTitleDao;
import xiaozhi.modules.agent.entity.AgentChatTitleEntity;
import xiaozhi.modules.agent.service.AgentChatTitleService;
@Service
@RequiredArgsConstructor
public class AgentChatTitleServiceImpl implements AgentChatTitleService {
private final AgentChatTitleDao agentChatTitleDao;
@Override
public void saveOrUpdateTitle(String sessionId, String title) {
if (StringUtils.isBlank(sessionId) || StringUtils.isBlank(title)) {
return;
}
QueryWrapper<AgentChatTitleEntity> wrapper = new QueryWrapper<>();
wrapper.eq("session_id", sessionId);
AgentChatTitleEntity existing = agentChatTitleDao.selectOne(wrapper);
if (existing != null) {
existing.setTitle(title);
existing.setUpdatedAt(new Date());
agentChatTitleDao.updateById(existing);
} else {
AgentChatTitleEntity newEntity = AgentChatTitleEntity.builder()
.id(java.util.UUID.randomUUID().toString().replace("-", ""))
.sessionId(sessionId)
.title(title)
.createdAt(new Date())
.updatedAt(new Date())
.build();
agentChatTitleDao.insert(newEntity);
}
}
@Override
public String getTitleBySessionId(String sessionId) {
if (StringUtils.isBlank(sessionId)) {
return null;
}
QueryWrapper<AgentChatTitleEntity> wrapper = new QueryWrapper<>();
wrapper.eq("session_id", sessionId);
AgentChatTitleEntity entity = agentChatTitleDao.selectOne(wrapper);
return entity != null ? entity.getTitle() : null;
}
}
@@ -300,6 +300,9 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
if (dto.getLlmModelId() != null) {
existingEntity.setLlmModelId(dto.getLlmModelId());
}
if (dto.getSlmModelId() != null) {
existingEntity.setSlmModelId(dto.getSlmModelId());
}
if (dto.getVllmModelId() != null) {
existingEntity.setVllmModelId(dto.getVllmModelId());
}
@@ -506,6 +509,13 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
entity.setLanguage(template.getLanguage());
}
if (entity.getSlmModelId() == null) {
String defaultSlmModelId = getDefaultLLMModelId();
if (defaultSlmModelId != null) {
entity.setSlmModelId(defaultSlmModelId);
}
}
// 设置用户ID和创建者信息
UserDetail user = SecurityUser.getUser();
entity.setUserId(user.getId());
@@ -544,4 +554,23 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
return entity.getId();
}
private String getDefaultLLMModelId() {
try {
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.getId();
}
}
return llmConfigs.get(0).getId();
} catch (Exception e) {
return null;
}
}
}
@@ -1,102 +0,0 @@
# RAGFlow API Interface Classification
## 1. External APIs (三方接入体系)
**Path Prefix:** `/api/v1`
**Authentication:** API Key (`@token_required`)
**Primary Use:** External system integration, SDK usage.
| Interface Type | Python File Path | Class/Function Name | URL Pattern | Notes |
|---|---|---|---|---|
| **External** | `api/apps/sdk/session.py` | `agent_bot_completions` | `/api/v1/agentbots/<agent_id>/completions` | Agent Bot completion |
| **External** | `api/apps/sdk/session.py` | `begin_inputs` | `/api/v1/agentbots/<agent_id>/inputs` | Get Agent Bot inputs |
| **External** | `api/apps/sdk/agents.py` | `list_agents` | `/api/v1/agents` | List Agents |
| **External** | `api/apps/sdk/agents.py` | `create_agent` | `/api/v1/agents` | Create Agent |
| **External** | `api/apps/sdk/agents.py` | `update_agent` | `/api/v1/agents/<agent_id>` | Update Agent |
| **External** | `api/apps/sdk/agents.py` | `delete_agent` | `/api/v1/agents/<agent_id>` | Delete Agent |
| **External** | `api/apps/sdk/session.py` | `agent_completions` | `/api/v1/agents/<agent_id>/completions` | Agent completion |
| **External** | `api/apps/sdk/session.py` | `create_agent_session` | `/api/v1/agents/<agent_id>/sessions` | Create Agent Session |
| **External** | `api/apps/sdk/session.py` | `list_agent_session` | `/api/v1/agents/<agent_id>/sessions` | List Agent Sessions |
| **External** | `api/apps/sdk/session.py` | `delete_agent_session` | `/api/v1/agents/<agent_id>/sessions` | Delete Agent Session |
| **External** | `api/apps/sdk/session.py` | `agents_completion_openai_compatibility` | `/api/v1/agents_openai/<agent_id>/chat/completions` | OpenAI compatible Agent completion |
| **External** | `api/apps/sdk/session.py` | `chatbot_completions` | `/api/v1/chatbots/<dialog_id>/completions` | Chatbot completion |
| **External** | `api/apps/sdk/session.py` | `chatbots_inputs` | `/api/v1/chatbots/<dialog_id>/info` | Chatbot info |
| **External** | `api/apps/sdk/chat.py` | `create` | `/api/v1/chats` | Create Chat |
| **External** | `api/apps/sdk/chat.py` | `delete_chats` | `/api/v1/chats` | Delete Chat |
| **External** | `api/apps/sdk/chat.py` | `list_chat` | `/api/v1/chats` | List Chats |
| **External** | `api/apps/sdk/chat.py` | `update` | `/api/v1/chats/<chat_id>` | Update Chat |
| **External** | `api/apps/sdk/session.py` | `chat_completion` | `/api/v1/chats/<chat_id>/completions` | Chat completion |
| **External** | `api/apps/sdk/session.py` | `create` | `/api/v1/chats/<chat_id>/sessions` | Create Chat Session |
| **External** | `api/apps/sdk/session.py` | `list_session` | `/api/v1/chats/<chat_id>/sessions` | List Chat Sessions |
| **External** | `api/apps/sdk/session.py` | `delete` | `/api/v1/chats/<chat_id>/sessions` | Delete Chat Session |
| **External** | `api/apps/sdk/session.py` | `update` | `/api/v1/chats/<chat_id>/sessions/<session_id>` | Update Chat Session |
| **External** | `api/apps/sdk/session.py` | `chat_completion_openai_like` | `/api/v1/chats_openai/<chat_id>/chat/completions` | OpenAI compatible Chat completion |
| **External** | `api/apps/sdk/dataset.py` | `create` | `/api/v1/datasets` | Create Dataset |
| **External** | `api/apps/sdk/dataset.py` | `delete` | `/api/v1/datasets` | Delete Dataset |
| **External** | `api/apps/sdk/dataset.py` | `list_datasets` | `/api/v1/datasets` | List Datasets |
| **External** | `api/apps/sdk/dataset.py` | `update` | `/api/v1/datasets/<dataset_id>` | Update Dataset |
| **External** | `api/apps/sdk/doc.py` | `parse` | `/api/v1/datasets/<dataset_id>/chunks` | Parse Document Chunks |
| **External** | `api/apps/sdk/doc.py` | `stop_parsing` | `/api/v1/datasets/<dataset_id>/chunks` | Stop Parsing |
| **External** | `api/apps/sdk/doc.py` | `upload` | `/api/v1/datasets/<dataset_id>/documents` | Upload Document |
| **External** | `api/apps/sdk/doc.py` | `list_docs` | `/api/v1/datasets/<dataset_id>/documents` | List Documents |
| **External** | `api/apps/sdk/doc.py` | `delete` | `/api/v1/datasets/<dataset_id>/documents` | Delete Document |
| **External** | `api/apps/sdk/doc.py` | `update_doc` | `/api/v1/datasets/<dataset_id>/documents/<document_id>` | Update Document |
| **External** | `api/apps/sdk/doc.py` | `download` | `/api/v1/datasets/<dataset_id>/documents/<document_id>` | Download Document |
| **External** | `api/apps/sdk/doc.py` | `list_chunks` | `/api/v1/datasets/<dataset_id>/documents/<document_id>/chunks` | List Chunks |
| **External** | `api/apps/sdk/doc.py` | `add_chunk` | `/api/v1/datasets/<dataset_id>/documents/<document_id>/chunks` | Add Chunk |
| **External** | `api/apps/sdk/doc.py` | `update_chunk` | `/api/v1/datasets/<dataset_id>/documents/<document_id>/chunks/<chunk_id>` | Update Chunk |
| **External** | `api/apps/sdk/dataset.py` | `knowledge_graph` | `/api/v1/datasets/<dataset_id>/knowledge_graph` | Knowledge Graph |
| **External** | `api/apps/sdk/dataset.py` | `delete_knowledge_graph` | `/api/v1/datasets/<dataset_id>/knowledge_graph` | Delete Knowledge Graph |
| **External** | `api/apps/sdk/doc.py` | `metadata_summary` | `/api/v1/datasets/<dataset_id>/metadata/summary` | Metadata Summary |
| **External** | `api/apps/sdk/doc.py` | `metadata_batch_update` | `/api/v1/datasets/<dataset_id>/metadata/update` | Batch Update Metadata |
| **External** | `api/apps/sdk/dataset.py` | `run_graphrag` | `/api/v1/datasets/<dataset_id>/run_graphrag` | Run GraphRAG |
| **External** | `api/apps/sdk/dataset.py` | `run_raptor` | `/api/v1/datasets/<dataset_id>/run_raptor` | Run Raptor |
| **External** | `api/apps/sdk/dataset.py` | `trace_graphrag` | `/api/v1/datasets/<dataset_id>/trace_graphrag` | Trace GraphRAG |
| **External** | `api/apps/sdk/dataset.py` | `trace_raptor` | `/api/v1/datasets/<dataset_id>/trace_raptor` | Trace Raptor |
| **External** | `api/apps/sdk/dify_retrieval.py` | `retrieval` | `/api/v1/dify/retrieval` | Dify Retrieval |
| **External** | `api/apps/sdk/files.py` | `get_all_parent_folders` | `/api/v1/file/all_parent_folder` | Get All Parent Folders |
| **External** | `api/apps/sdk/files.py` | `convert` | `/api/v1/file/convert` | File Convert |
| **External** | `api/apps/sdk/files.py` | `create` | `/api/v1/file/create` | File Create |
| **External** | `api/apps/sdk/files.py` | `download_attachment` | `/api/v1/file/download/<attachment_id>` | Download Attachment |
| **External** | `api/apps/sdk/files.py` | `get` | `/api/v1/file/get/<file_id>` | Get File |
| **External** | `api/apps/sdk/files.py` | `list_files` | `/api/v1/file/list` | List Files |
| **External** | `api/apps/sdk/files.py` | `move` | `/api/v1/file/mv` | Move File |
| **External** | `api/apps/sdk/files.py` | `get_parent_folder` | `/api/v1/file/parent_folder` | Get Parent Folder |
| **External** | `api/apps/sdk/files.py` | `rename` | `/api/v1/file/rename` | Rename File |
| **External** | `api/apps/sdk/files.py` | `rm` | `/api/v1/file/rm` | Remove File |
| **External** | `api/apps/sdk/files.py` | `get_root_folder` | `/api/v1/file/root_folder` | Get Root Folder |
| **External** | `api/apps/sdk/files.py` | `upload` | `/api/v1/file/upload` | Upload File |
| **External** | `api/apps/sdk/doc.py` | `retrieval_test` | `/api/v1/retrieval` | Retrieval Test |
| **External** | `api/apps/sdk/session.py` | `ask_about_embedded` | `/api/v1/searchbots/ask` | Searchbot Ask |
| **External** | `api/apps/sdk/session.py` | `detail_share_embedded` | `/api/v1/searchbots/detail` | Searchbot Detail |
| **External** | `api/apps/sdk/session.py` | `mindmap` | `/api/v1/searchbots/mindmap` | Searchbot Mindmap |
| **External** | `api/apps/sdk/session.py` | `related_questions_embedded` | `/api/v1/searchbots/related_questions` | Searchbot Related Questions |
| **External** | `api/apps/sdk/session.py` | `retrieval_test_embedded` | `/api/v1/searchbots/retrieval_test` | Searchbot Retrieval Test |
| **External** | `api/apps/sdk/session.py` | `ask_about` | `/api/v1/sessions/ask` | Session Ask |
| **External** | `api/apps/sdk/session.py` | `related_questions` | `/api/v1/sessions/related_questions` | Session Related Questions |
| **External** | `api/apps/sdk/agents.py` | `webhook` | `/api/v1/webhook_test/<agent_id>` | Webhook Test |
| **External** | `api/apps/sdk/agents.py` | `webhook_trace` | `/api/v1/webhook_trace/<agent_id>` | Webhook Trace |
| **External** | `api/apps/sdk/doc.py` | `rm_chunk` | `/api/v1datasets/<dataset_id>/documents/<document_id>/chunks` | Remove Chunk |
## 2. Internal APIs (内部前端体系)
**Path Prefix:** `/v1/<app_name>` matches file `api/apps/<app_name>_app.py`
**Authentication:** Session/Cookie (`@login_required`)
**Primary Use:** RAGFlow Web Frontend.
**Selected Core Interfaces:**
| Interface Type | Python File Path | Class/Function Name | URL Pattern | Notes |
|---|---|---|---|---|
| Internal | `api/apps/user_app.py` | `login` | `/v1/user/login` | User Login (Frontend) |
| Internal | `api/apps/user_app.py` | `log_out` | `/v1/user/logout` | User Logout |
| Internal | `api/apps/user_app.py` | `user_add` | `/v1/user/register` | User Registration |
| Internal | `api/apps/user_app.py` | `user_profile` | `/v1/user/info` | User Profile Info |
| Internal | `api/apps/api_app.py` | `new_token` | `/v1/api/new_token` | Generate new API Token |
| Internal | `api/apps/conversation_app.py` | `set_conversation` | `/v1/conversation/set` | Create/Update Conversation |
| Internal | `api/apps/conversation_app.py` | `completion` | `/v1/conversation/completion` | Chat Conversation Completion |
| Internal | `api/apps/kb_app.py` | `list_kbs` | `/v1/kb/list` | List Knowledge Bases |
| Internal | `api/apps/kb_app.py` | `create` | `/v1/kb/create` | Create Knowledge Base |
| Internal | `api/apps/document_app.py` | `upload` | `/v1/document/upload` | Upload Document to KB |
| Internal | `api/apps/document_app.py` | `parse` | `/v1/document/parse` | Parse Document |
*(For a complete list of all 200+ internal APIs, please refer to the `api_endpoints.txt` file or the full scan results)*
@@ -1,279 +0,0 @@
# RAGFlow Agent 与 Dify 兼容接口详解 (Agent & Dify Compatibility)
## 1. Dify 兼容检索 - `retrieval`
**接口描述**: 模拟 Dify API 格式的知识库检索接口。此接口主要用于让现有的 Dify 客户端或系统能够方便地接入 RAGFlow 的知识库检索能力。它支持文本检索、混合检索以及通过元数据过滤文档。
**请求方法**: `POST`
**接口地址**: `/api/v1/dify/retrieval`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Body Parameters (JSON)
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| knowledge_id | string | 是 | - | **知识库 ID**。 |
| query | string | 是 | - | **查询文本**。用户输入的检索问题。 |
| use_kg | boolean | 否 | false | **使用知识图谱**。是否结合知识图谱进行检索。 |
| retrieval_setting | object | 否 | {} | **检索配置**。包含相似度阈值和 Top-K。 |
| metadata_condition | object | 否 | {} | **元数据过滤条件**。用于筛选特定文档。 |
#### 参数详情 (Detail Objects)
**retrieval_setting**:
```json
{
"score_threshold": 0.5, // 相似度阈值 (default: 0.0)
"top_k": 5 // 返回数量 (default: 1024)
}
```
**metadata_condition**:
```json
{
"logic": "and", // 逻辑关系 (and/or)
"conditions": [
{
"name": "author", // 字段名
"comparison_operator": "eq",// 运算符 (eq, ne, gt, lt 等)
"value": "Alice" // 字段值
}
]
}
```
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": {
"records": [
{
"content": "RAGFlow 是一个基于深度文档理解的检索增强生成引擎...",
"score": 0.92,
"title": "RAGFlow_Introduction.pdf",
"metadata": {
"doc_id": "doc_uuid_123",
"author": "Alice",
"publish_year": "2024"
}
},
{
"content": "DeepDOC 模型能够精准识别复杂的表格结构...",
"score": 0.88,
"title": "DeepDOC_Tech_Report.pdf",
"metadata": {
"doc_id": "doc_uuid_456",
"author": "Bob"
}
}
]
}
}
```
---
## 2. 创建 Agent 会话 - `create_agent_session`
**接口描述**: 创建一个新的 Agent 会话 (Session)。会话是用户与 Agent 交互的上下文容器,保存了历史对话记录和 DSL(领域特定语言)状态。
**请求方法**: `POST`
**接口地址**: `/api/v1/agents/<agent_id>/sessions`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| agent_id | string | 是 | **Agent ID**。 |
#### Query Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| user_id | string | 否 | **用户标识**。用于区分不同终端用户的会话。若不传,默认为当前 Tenant ID。 |
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": {
"id": "session_uuid_new_123",
"agent_id": "agent_uuid_abc",
"user_id": "user_123",
"source": "agent",
"dsl": { ... }, // 完整的 Agent DSL 定义
"messages": [
{
"role": "assistant",
"content": "你好!我是你的智能助手,有什么可以帮你的吗?" // Prologue (开场白)
}
]
}
}
```
---
## 3. 获取 Agent 会话列表 - `list_agent_session`
**接口描述**: 分页获取指定 Agent 下的会话列表。支持按 ID 或 User ID 过滤。
**请求方法**: `GET`
**接口地址**: `/api/v1/agents/<agent_id>/sessions`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| agent_id | string | 是 | **Agent ID**。 |
#### Query Parameters
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| page | int | 否 | 1 | **页码**。 |
| page_size | int | 否 | 30 | **每页数量**。 |
| orderby | string | 否 | "update_time" | **排序字段**。 |
| desc | boolean | 否 | true | **是否降序**。 |
| id | string | 否 | - | **会话 ID**。精确筛选。 |
| user_id | string | 否 | - | **用户标识**。筛选特定用户的会话。 |
| dsl | boolean | 否 | true | **包含 DSL**。是否在返回结果中包含完整的 DSL 结构 (数据量较大)。 |
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": [
{
"id": "session_uuid_123",
"agent_id": "agent_uuid_abc",
"user_id": "user_123",
"create_time": 1715000000000,
"update_time": 1715000050000,
"source": "agent",
"messages": [
{
"role": "assistant",
"content": "Hi there!"
},
{
"role": "user",
"content": "What is RAG?"
}
]
}
]
}
```
---
## 4. 删除 Agent 会话 - `delete_agent_session`
**接口描述**: 批量删除 Agent 会话。
**请求方法**: `DELETE`
**接口地址**: `/api/v1/agents/<agent_id>/sessions`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| agent_id | string | 是 | **Agent ID**。 |
#### Body Parameters (JSON)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| ids | array<string> | 否 | **会话 ID 列表**。若不传该参数,将尝试删除(或清空)该 Agent 下的所有会话(需谨慎)。 |
**Request Example**:
```json
{
"ids": ["session_id_1", "session_id_2"]
}
```
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": {
"success_count": 2,
"errors": []
}
}
```
---
## 5. Agent 对话 (流式) - `agent_completions`
**接口描述**: 向 Agent 发送用户问题并获取回复。这是 Agent 交互的核心接口,支持 **Server-Sent Events (SSE)** 流式响应。Agent 会根据编排好的 DSL 流程执行(可能涉及多个节点、知识库检索、LLM 推理等),并实时推送执行过程和最终结果。
**请求方法**: `POST`
**接口地址**: `/api/v1/agents/<agent_id>/completions`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| agent_id | string | 是 | **Agent ID**。 |
#### Body Parameters (JSON)
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| session_id | string | 是 | - | **会话 ID**。必须是 `create_agent_session` 返回的 ID。 |
| question | string | 是 | - | **用户问题**。 |
| stream | boolean | 否 | true | **是否流式响应**。强烈建议设为 `true` 以获得更好的用户体验。 |
| return_trace | boolean | 否 | false | **返回执行轨迹**。如果为 `true`,流式响应中将包含各个节点的执行过程数据 (Trace)。 |
### 响应参数 (Stream Response)
**Content-Type**: `text/event-stream`
响应是一个 SSE 流,每一行以 `data:` 开头,包含一个 JSON 对象。
**Event Types**:
- `message`: 普通文本消息片段。
- `node_finished`: (当 `return_trace=true` 时) 节点执行完成事件,包含节点输出数据。
- `message_end`: 消息结束。
- `[DONE]`: 流结束标志。
#### Stream Chunk Examples:
**1. 文本生成片段 (message)**:
```text
data:{"code": 0, "message": "success", "data": {"content": "Hello", "reference": {}, "id": "msg_uuid_1"}, "event": "message"}
data:{"code": 0, "message": "success", "data": {"content": " world", "reference": {}, "id": "msg_uuid_1"}, "event": "message"}
```
**2. 节点执行轨迹 (node_finished, return_trace=true)**:
```text
data:{"code": 0, "message": "success", "data": {"component_id": "retrieval_node_1", "content": "...", "trace": [...]}, "event": "node_finished"}
```
**3. 最终结束 (DONE)**:
```text
data:[DONE]
```
#### Non-Stream Response (stream=false)
如果不使用流式响应,将等待 Agent 全流程执行完毕后一次性返回 JSON。
```json
{
"code": 0,
"message": "success",
"data": {
"content": "Hello world! This is the final answer.",
"reference": {
"chunk_id_1": { ... } // 引用来源
},
"trace": [ ... ] // 如果 return_trace=true
}
}
```
@@ -1,233 +0,0 @@
## 1. 获取 Agent 列表 - `list_agents`
**接口描述**: 分页查询当前租户下的所有 Agent 列表,支持按 ID 或标题筛选。
**请求方法**: `GET`
**接口地址**: `/api/v1/agents`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
#### Query Parameters
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| page | int | 否 | 1 | 页码 |
| page_size | int | 否 | 30 | 每页条数 |
| orderby | string | 否 | update_time | 排序字段 (create_time, update_time, title) |
| desc | boolean | 否 | True | 是否降序排列 (True: 降序, False: 升序) |
| id | string | 否 | - | 按 Agent ID 精确筛选 |
| title | string | 否 | - | 按 Agent 标题精确筛选 |
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": [
{
"id": "e0d34e2c-...",
"title": "My Assistant",
"description": "A helpful AI assistant",
"dsl": { ... }, // Agent 的 DSL 流程定义
"user_id": "tenant_123",
"avatar": "", // 头像 Base64 或 URL
"canvas_category": "Agent",
"create_time": 1715623400000,
"update_time": 1715624500000
}
]
}
```
---
## 2. 创建 Agent - `create_agent`
**接口描述**: 创建一个新的 Agent,必须包含标题和 DSL 定义。
**请求方法**: `POST`
**接口地址**: `/api/v1/agents`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
#### Body Parameters (JSON)
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| title | string | 是 | - | Agent 的名称 (必须唯一) |
| dsl | object | 是 | - | Agent 的流程定义 (节点、连线配置) |
| description | string | 否 | - | Agent 的功能描述 |
| avatar | string | 否 | - | Agent 头像 (Base64 字符串或 URL) |
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": true
}
```
---
## 3. 更新 Agent - `update_agent`
**接口描述**: 更新指定 Agent 的配置信息,支持增量更新(仅传递需要修改的字段)。
**请求方法**: `PUT`
**接口地址**: `/api/v1/agents/<agent_id>`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| agent_id | string | 是 | 要更新的 Agent ID |
#### Body Parameters (JSON)
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| title | string | 否 | - | 新的 Agent 名称 |
| dsl | object | 否 | - | 新的 DSL 流程定义 |
| description | string | 否 | - | 新的功能描述 |
| avatar | string | 否 | - | 新的头像 |
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": true
}
```
---
## 4. 删除 Agent - `delete_agent`
**接口描述**: 根据 ID 删除指定的 Agent。此操作不可恢复。
**请求方法**: `DELETE`
**接口地址**: `/api/v1/agents/<agent_id>`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| agent_id | string | 是 | 要删除的 Agent ID |
#### Body Parameters (JSON)
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": true
}
```
---
## 5. Webhook 测试触发 - `webhook`
**接口描述**: 用于测试 Agent 的 Webhook 触发功能。该接口模拟外部系统调用,触发 Agent 按照配置的 "Begin" 节点逻辑开始执行。支持同步等待结果或流式返回(取决于 Agent 配置)。
**请求方法**: `POST` (支持 GET/PUT/DELETE 等,取决于 Canvas 配置)
**接口地址**: `/api/v1/webhook_test/<agent_id>`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| agent_id | string | 是 | Agent 的唯一标识符 |
#### Query / Headers / Body Parameters
**说明**: 此接口的参数完全动态,取决于 Agent 画布中 **"Begin" (开始)** 节点的 **Webhook** 配置。
- 如果配置了 Query 参数验证,则需在 URL 中传递对应参数。
- 如果配置了 Header 验证,则需传递对应 Header。
- **Body**: 通常为 JSON 格式,包含 Agent 运行所需的变量(inputs)或上下文数据。
**Body Example (JSON)**:
```json
{
"inputs": {
"topic": "AI Trends",
"style": "professional"
},
"query": "Start generation"
}
```
### 响应参数 (Response)
**Content-Type**: `application/json` (或 `text/event-stream`)
**即时响应模式 (Immediately)**:
```json
{
"code": 0,
"data": {
"content": "生成的回答内容...",
"usage": { ... }
}
}
```
**流式响应模式 (SSE)**:
如果不使用 `webhook_test` 而是生产环境 `webhook` 且配置为 SSE,则返回流式数据。但在 `webhook_test` 接口中,通常配合 `webhook_trace` 进行异步调试。
---
## 6. Webhook 执行轨迹查询 - `webhook_trace`
**接口描述**: 轮询查询 Agent 在 Webhook 测试触发后的执行日志和中间状态。采用长轮询或游标机制,实时获取执行进度。
**请求方法**: `GET`
**接口地址**: `/api/v1/webhook_trace/<agent_id>`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| agent_id | string | 是 | Agent 的唯一标识符 |
#### Query Parameters
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| since_ts | float | 否 | 当前时间 | 起始时间戳。返回此时间之后的日志事件。首次调用可不传(获取当前时间作为游标)。 |
| webhook_id | string | 否 | - | Webhook 会话 ID。用于锁定特定的某次执行记录。首次轮询时不传,接口会返回新生成的 ID。 |
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": {
"webhook_id": "YWdlbnxxxx...", // 当前追踪的会话 ID (加密串)
"finished": false, // 执行是否已结束 (true/false)
"next_since_ts": 1715629999.5, // 下一次轮询应使用的 since_ts
"events": [ // 本次轮询获取到的新事件列表
{
"ts": 1715629998.1,
"event": "message", // 事件类型: message, start_to_think, finished, error 等
"data": {
"content": "思考中...",
"reference": []
}
}
]
}
}
```
### 💡 最佳实践 (调试流程)
1. **初始化**: 调用 `GET /webhook_trace/<id>` (不带参数),获取 `next_since_ts` (记为 `T0`)。
2. **触发**: 调用 `POST /webhook_test/<id>` 发送测试数据。
3. **首帧捕获**: 循环调用 `GET /webhook_trace/<id>?since_ts=T0`,直到返回 `webhook_id` (记为 `WID`) 和第一批 `events`
4. **持续追踪**: 使用 `WID` 和响应中的 `next_since_ts` 持续轮询,直到 `data.finished == true`
@@ -1,164 +0,0 @@
# RAGFlow 对话交互接口详解 (Chat Completion & OpenAI Compatibility)
## 5. 对话助手对话 (流式) - `chat_completion`
**接口描述**: 发送问题给对话助手 (Assistant/Chat) 并获取回复。这是 RAGFlow 最核心的原生对话接口,支持 **Server-Sent Events (SSE)** 流式响应。它会根据助手绑定的知识库进行 RAG 检索生成。
**请求方法**: `POST`
**接口地址**: `/api/v1/chats/<chat_id>/completions`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| chat_id | string | 是 | **助手 ID**。 |
#### Body Parameters (JSON)
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| session_id | string | 是 | - | **会话 ID**。从 `create_chat_session` 获取。 |
| question | string | 是 | - | **用户问题**。 |
| stream | boolean | 否 | true | **是否流式响应**。 |
| quote | boolean | 否 | false | **返回引用**。是否在响应中包含检索到的引用片段。 |
| doc_ids | string | 否 | - | **限定文档 ID**。多个 ID 用逗号分隔,仅检索指定文档。 |
| metadata_condition | object | 否 | {} | **元数据过滤**。用于限定检索范围。 |
### 响应参数 (Stream Response)
**Content-Type**: `text/event-stream`
每一行数据以 `data:` 开头,包含一个 JSON 对象。
**Event Example**:
```text
data:{"code": 0, "message": "success", "data": {"answer": "Hello", "reference": {}}}
data:{"code": 0, "message": "success", "data": {"answer": " world!", "reference": {}}}
data:{"code": 0, "message": "success", "data": {"answer": "", "reference": {"chunk_1": {...}}}} // 引用数据
```
### 响应参数 (Non-Stream Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": {
"answer": "Hello world! This is the generated response.",
"reference": {
"chunk_id_1": {
"content_with_weight": "Original text...",
"doc_name": "manual.pdf"
}
}
}
}
```
---
## 6. OpenAI 兼容对话 - `chat_completion_openai_like`
**接口描述**: 提供与 **OpenAI API (`/v1/chat/completions`)** 完全兼容的接口。允许开发者使用 LangChain、OpenAI Python SDK 或其他支持 OpenAI 协议的工具直接调用 RAGFlow,实现无缝迁移。
**请求方法**: `POST`
**接口地址**: `/api/v1/chats_openai/<chat_id>/chat/completions`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| chat_id | string | 是 | **助手 ID**。在此上下文中充当 "Base URL" 的一部分。 |
#### Body Parameters (JSON - OpenAI Standard)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| messages | array | 是 | **消息列表**。包含 `role` (system/user/assistant) 和 `content`。 |
| model | string | 是 | **模型名称**。可以是任意非空字符串 (RAGFlow 会使用助手预设的模型)。 |
| stream | boolean | 否 | **是否流式**。默认为 `true`。 |
**Request Example**:
```json
{
"model": "ragflow_default",
"messages": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Explain quantum physics."}
],
"stream": true
}
```
### 响应参数 (Stream Response - OpenAI Format)
**Content-Type**: `text/event-stream`
严格遵循 OpenAI Chunk 格式:
```text
data: {"id": "chatcmpl-123", "object": "chat.completion.chunk", "created": 1715000000, "model": "model", "choices": [{"index": 0, "delta": {"role": "assistant", "content": ""}, "finish_reason": null}]}
data: {"id": "chatcmpl-123", "object": "chat.completion.chunk", "created": 1715000001, "model": "model", "choices": [{"index": 0, "delta": {"content": "Quantum"}, "finish_reason": null}]}
data: {"id": "chatcmpl-123", "object": "chat.completion.chunk", "created": 1715000002, "model": "model", "choices": [{"index": 0, "delta": {"content": " physics"}, "finish_reason": null}]}
data: [DONE]
```
---
## 7. 嵌入式 Chatbot 对话 - `chatbot_completions`
**接口描述**: 专为 **嵌入式窗口 (Embed Window)** 设计的公开对话接口。它通常用于将 RAGFlow 助手作为客服窗口嵌入到第三方网站。与普通接口不同,它通过 `Authorization` Header 传递 **Beta Token** (即 API Key) 进行鉴权,且通常面向最终用户。
**请求方法**: `POST`
**接口地址**: `/api/v1/chatbots/<dialog_id>/completions`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| dialog_id | string | 是 | **助手 ID** (Dialog ID)。 |
#### Body Parameters (JSON)
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| question | string | 是 | - | **用户问题**。 |
| stream | boolean | 否 | true | **是否流式**。 |
| session_id | string | 否 | - | **会话 ID**。用于维持上下文。 |
| quote | boolean | 否 | false | **返回引用**。 |
### 响应参数 (Stream Response)
**Content-Type**: `text/event-stream`
`chat_completion` 类似,返回 RAGFlow 原生 SSE 格式。
```text
data:{"code": 0, "message": "success", "data": {"answer": "Here is the answer...", "reference": {}}}
```
---
## 8. Chatbot 初始化信息 - `chatbots_inputs`
**接口描述**: 获取嵌入式 Chatbot 的初始化配置信息。通常在前端组件加载时调用,用于展示助手的头像、名称、开场白 (Prologue) 等信息。
**请求方法**: `GET`
**接口地址**: `/api/v1/chatbots/<dialog_id>/info`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| dialog_id | string | 是 | **助手 ID**。 |
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": {
"title": "IT Support Bot", // 助手名称
"avatar": "http://...", // 头像 URL
"prologue": "Hi! How can I help?" // 开场白
}
}
```
@@ -1,208 +0,0 @@
# RAGFlow 聊天助手会话管理接口详解 (Chat Assistant Session Management)
## 1. 创建会话 - `create_chat_session`
**接口描述**: 为指定的聊天助手 (Chat/Assistant) 创建一个新的会话。系统会自动加载该助手的开场白 (Prologue) 作为第一条消息。
**请求方法**: `POST`
**接口地址**: `/api/v1/chats/<chat_id>/sessions`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| chat_id | string | 是 | **助手 ID** (Assistant/Dialog ID)。 |
#### Body Parameters (JSON)
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| name | string | 否 | "New session" | **会话名称**。 |
| user_id | string | 否 | - | **用户标识**。用于区分不同终端用户的会话。 |
**Request Example**:
```json
{
"name": "Consulting regarding RAG",
"user_id": "client_001"
}
```
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": {
"id": "session_uuid_123",
"chat_id": "chat_uuid_abc",
"name": "Consulting regarding RAG",
"user_id": "client_001",
"create_time": 1715000000000,
"create_date": "2024-05-01 10:00:00",
"update_time": 1715000000000,
"update_date": "2024-05-01 10:00:00",
"messages": [
{
"role": "assistant",
"content": "Hi! I am your AI assistant. How can I help you today?" // 自动加载的开场白
}
]
}
}
```
---
## 2. 获取会话列表 - `list_chat_session`
**接口描述**: 分页获取指定助手下的会话列表。支持按名称或用户 ID 过滤。
**请求方法**: `GET`
**接口地址**: `/api/v1/chats/<chat_id>/sessions`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| chat_id | string | 是 | **助手 ID**。 |
#### Query Parameters
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| page | int | 否 | 1 | **页码**。 |
| page_size | int | 否 | 30 | **每页数量**。 |
| orderby | string | 否 | "create_time" | **排序字段**。 |
| desc | boolean | 否 | true | **是否降序**。 |
| name | string | 否 | - | **会话名称搜索**。 |
| id | string | 否 | - | **会话 ID 精确筛选**。 |
| user_id | string | 否 | - | **用户标识筛选**。 |
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": [
{
"id": "session_uuid_123",
"chat_id": "chat_uuid_abc",
"name": "Consulting regarding RAG",
"user_id": "client_001",
"create_time": 1715000000000,
"create_date": "2024-05-01 10:00:00",
"update_time": 1715000050000,
"update_date": "2024-05-01 10:00:50",
"messages": [
{
"role": "assistant",
"content": "Hi! I am your AI assistant. How can I help you today?"
},
{
"role": "user",
"content": "What is RAGFlow?"
}
]
},
{
"id": "session_uuid_456",
"chat_id": "chat_uuid_abc",
"name": "New session",
"user_id": "client_002",
"create_time": 1714900000000,
"create_date": "2024-04-30 09:00:00",
"update_time": 1714900000000,
"update_date": "2024-04-30 09:00:00",
"messages": [ ... ]
}
]
}
```
---
## 3. 更新会话 - `update_chat_session`
**接口描述**: 更新会话信息。目前主要用于 **重命名** 会话。注意:不能通过此接口修改消息记录 (`messages`)。
**请求方法**: `PUT`
**接口地址**: `/api/v1/chats/<chat_id>/sessions/<session_id>`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| chat_id | string | 是 | **助手 ID**。 |
| session_id | string | 是 | **会话 ID**。 |
#### Body Parameters (JSON)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| name | string | 否 | **新的会话名称**。不可为空字符串。 |
**Request Example**:
```json
{
"name": "RAG Technical Discussion"
}
```
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": null
}
```
---
## 4. 删除会话 - `delete_chat_session`
**接口描述**: 批量删除指定助手下的会话。
**请求方法**: `DELETE`
**接口地址**: `/api/v1/chats/<chat_id>/sessions`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| chat_id | string | 是 | **助手 ID**。 |
#### Body Parameters (JSON)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| ids | array<string> | 否 | **待删除的会话 ID 列表**。若不传该参数,将尝试删除该助手下的**所有会话**(请极其谨慎使用)。 |
**Request Example**:
```json
{
"ids": ["session_uuid_123", "session_uuid_456"]
}
```
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": null // 若全部删除成功
}
```
**Response (部分成功时)**:
```json
{
"code": 0,
"message": "Partially deleted 1 sessions with 1 errors",
"data": {
"success_count": 1,
"errors": ["The chat doesn't own the session session_uuid_999"]
}
}
```
@@ -1,213 +0,0 @@
## 1. 创建助手应用 - `create`
**接口描述**: 创建一个新的对话助手(Chat Assistant)。支持配置关联知识库、LLM 模型参数、提示词(Prompt)以及开场白等高级设置。
**请求方法**: `POST`
**接口地址**: `/api/v1/chats`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
#### Body Parameters (JSON)
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| name | string | 是 | - | 助手应用名称 (租户内唯一) |
| avatar | string | 否 | - | 助手头像 (URL 或 Base64 字符串) |
| description | string | 否 | "A helpful Assistant" | 助手的功能描述 |
| dataset_ids | array | 否 | [] | 关联的知识库 ID 列表 (必须是当前租户有权限访问的知识库) |
| llm | object | 否 | - | LLM 模型生成配置 (如模型名称、温度等) |
| prompt | object | 否 | - | 提示词引擎与检索配置 (包含 System Prompt, Opener, Rerank 等) |
**`llm` 对象详细结构**:
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| model_name | string | 是 | - | 模型名称 (例如: `deepseek-chat`, `gpt-4`, `qwen-turbo`) |
| temperature | float | 否 | 0.1 | 温度系数 (0.0 ~ 1.0),越高越随机,越低越确定 |
| top_p | float | 否 | 0.3 | 核采样概率阈值 |
| max_tokens | int | 否 | 512 | 单次回答的最大 Token 数限制 |
| presence_penalty | float | 否 | 0.4 | 话题新鲜度惩罚 (-2.0 ~ 2.0),正值鼓励讨论新话题 |
| frequency_penalty | float | 否 | 0.7 | 频率惩罚 (-2.0 ~ 2.0),正值减少重复词汇 |
**`prompt` 对象详细结构**:
*注意:此对象包含“提示词配置”与“检索策略配置”两部分。*
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| prompt | string | 否 | (内置默认提示词) | **System Prompt (系统提示词)**。给大模型的角色指令,例如 "你是一个客服..."。可使用变量占位符 `{knowledge}`。 |
| opener | string | 否 | "Hi! I'm your assistant..." | **开场白**。用户进入对话窗口时,助手自动发送的第一条欢迎语。 |
| show_quote | boolean | 否 | true | **显示引用**。回答中是否标注来源文档 (e.g., [1])。 |
| variables | array | 否 | `[{"key": "knowledge", "optional": false}]` | **变量列表**。定义用于填充 System Prompt 的变量。`knowledge` 为保留变量,代表检索到的知识片段。 |
| rerank_model | string | 否 | - | **重排序模型 ID**。配置后会对检索结果进行二次精排 (如 `BAAI/bge-reranker-v2-m3`)。 |
| keywords_similarity_weight | float | 否 | 0.7 | **关键字权重** (0.0 ~ 1.0)。控制混合检索的比例。更接近 1.0 侧重关键字匹配,更接近 0.0 侧重向量语义匹配。 |
| similarity_threshold | float | 否 | 0.2 | **相似度阈值** (0.0 ~ 1.0)。低于此相似度的文档块将被过滤,不喂给大模型。 |
| top_n | int | 否 | 6 | **Top N**。最终截取并输入给大模型的文档块数量。 |
| empty_response | string | 否 | "Sorry! No relevant..." | **空结果回复**。当没有检索到相关知识库内容时的兜底回复。 |
| tts | boolean | 否 | false | **启用 TTS**。是否将助手的文本回答自动转为语音播放。 |
| refine_multiturn | boolean | 否 | true | **多轮对话优化**。是否根据历史上下文重写用户问题 (Query Rewrite) 以提高检索准确率。 |
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": {
"id": "e0d34e2c-1234-5678-9xxx-xxxxxxxxxxxx",
"name": "企业知识库助手",
"avatar": "http://example.com/avatar.png",
"description": "用于回答员工内部问题的 AI",
"dataset_ids": ["kb_123", "kb_456"],
"llm": {
"model_name": "deepseek-chat",
"temperature": 0.1,
"top_p": 0.3,
"max_tokens": 512,
"presence_penalty": 0.4,
"frequency_penalty": 0.7
},
"prompt": {
"prompt": "你是一个智能助手,请根据以下知识回答问题:\n{knowledge}",
"opener": "你好!有什么可以帮你的?",
"show_quote": true,
"variables": [
{ "key": "knowledge", "optional": false }
],
"rerank_model": "",
"keywords_similarity_weight": 0.7,
"similarity_threshold": 0.2,
"top_n": 8,
"empty_response": "抱歉,知识库中没有找到相关答案。",
"tts": false,
"refine_multiturn": true
},
"create_time": 1715623400000,
"update_time": 1715624500000
}
}
```
---
## 2. 获取助手列表 - `list_chat`
**接口描述**: 获取当前租户下的所有助手应用列表。支持分页、排序及按名称/ID筛选。
**请求方法**: `GET`
**接口地址**: `/api/v1/chats`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
#### Query Parameters
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| page | int | 否 | 1 | 页码 |
| page_size | int | 否 | 30 | 每页条数 |
| orderby | string | 否 | create_time | 排序字段 (`create_time`, `update_time`) |
| desc | boolean | 否 | true | 是否降序排列 (`true`: 降序, `false`: 升序) |
| name | string | 否 | - | 按名称模糊搜索 (支持 partial match) |
| id | string | 否 | - | 按 ID 精确筛选 |
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": [
{
"id": "e0d34e2c-...",
"name": "客服机器人",
"avatar": "http://...",
"datasets": [
{
"id": "kb_1",
"name": "产品手册",
"avatar": "",
"chunk_num": 100
}
],
"llm": { ... }, // (结构同 create 接口响应)
"prompt": { ... }, // (结构同 create 接口响应)
"create_time": 1715623400000
}
]
}
```
---
## 3. 更新助手配置 - `update`
**接口描述**: 更新指定助手应用的配置信息。支持全量或增量更新部分字段。
**请求方法**: `PUT`
**接口地址**: `/api/v1/chats/<chat_id>`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| chat_id | string | 是 | 助手应用 ID |
#### Body Parameters (JSON)
*(以下所有字段均为可选,仅传递需要修改的字段即可)*
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| name | string | - | 新的助手名称 |
| avatar | string | - | 新的头像 URL 或 Base64 |
| dataset_ids | array | - | **全量替换**关联的知识库 ID 列表 |
| llm | object | - | 更新 LLM 配置。需包含 `model_name`,其他字段覆盖更新。 |
| prompt | object | - | 更新提示词配置。支持增量更新 (e.g. 只改 `opener`)。 |
| show_quotation | boolean | - | 是否显示引用来源 (此字段直接位于根对象下,对应 prompt.show_quote) |
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": null
}
```
---
## 4. 批量删除助手 - `delete_chats`
**接口描述**: 批量删除一个或多个助手应用。
**请求方法**: `DELETE`
**接口地址**: `/api/v1/chats`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
#### Body Parameters (JSON)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| ids | array<string> | 是 | 要删除的助手应用 ID 列表。**⚠️ 注意:若列表为空或不传,虽然后端有全量删除逻辑,但在实际业务中应严谨传递 ID。** |
**Request Example**:
```json
{
"ids": ["chat_id_1001", "chat_id_1002"]
}
```
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": {
"success_count": 2, // 成功删除的数量
"errors": [] // 失败原因列表 (如 ID 不存在)
}
}
```
@@ -1,420 +0,0 @@
## 1. 创建知识库 - `create`
**接口描述**: 创建一个新的知识库(Dataset),用于存储和检索文档数据。支持配置嵌入模型(Embedding Model)、解析方法、权限范围等。
**请求方法**: `POST`
**接口地址**: `/api/v1/datasets`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
#### Body Parameters (JSON)
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| name | string | 是 | - | **知识库名称**。在同一个租户(Tenant)内必须唯一。 |
| avatar | string | 否 | "" | **知识库头像**。Base64 编码的图片字符串。 |
| description | string | 否 | "" | **描述信息**。用于说明知识库的用途或内容概要。 |
| embedding_model | string | 否 | (系统默认) | **嵌入模型名称** (例如 `BAAI/bge-large-zh-v1.5`)。若不传,则自动使用系统设置的默认 Embedding 模型。 |
| permission | string | 否 | "me" | **可见权限**`me`: 仅自己可见;`team`: 团队内所有成员可见。 |
| chunk_method | string | 否 | "naive" | **默认分块解析方法**。当上传文件未指定解析方式时使用。可选值: `naive` (通用), `manual` (手动), `qa` (Q&A拆分), `table` (表格), `paper` (论文), `book` (书籍), `laws` (法律), `presentation` (PPT), `picture` (图片), `one` (单文档), `email` (邮件)。 |
| parser_config | object | 否 | (见下文) | **解析器详细配置**。根据 `chunk_method` 的不同而变化。 |
**`parser_config` 默认配置参数 (Naive 通用模式)**:
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| chunk_token_num | int | 512 | **切片最大 Token 数**。超过该长度会被截断到下一块。 |
| delimiter | string | "\\n" | **分段分隔符**。用于识别段落边界。 |
| layout_recognize | string | "DeepDOC" | **布局识别模型**。用于处理复杂文档结构 (如 `DeepDOC``Simple`)。 |
| html4excel | boolean | false | **Excel转HTML**。是否将 Excel 表格转为 HTML 格式进行解析。 |
| auto_keywords | int | 0 | **自动关键词抽取**。0 表示不抽取;N>0 表示为每个切片抽取 N 个关键词。 |
| auto_questions | int | 0 | **自动问题生成**。0 表示不生成;N>0 表示为每个切片生成 N 个相关问题。 |
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": {
"id": "kb_uuid_12345678",
"name": "企业产品手册",
"avatar": "",
"tenant_id": "tenant_001",
"description": "存放所有产品相关的说明文档",
"embedding_model": "BAAI/bge-large-zh-v1.5",
"permission": "me",
"chunk_method": "naive",
"parser_config": {
"chunk_token_num": 512,
"delimiter": "\n",
"layout_recognize": "DeepDOC",
"html4excel": false,
"auto_keywords": 0,
"auto_questions": 0
},
"chunk_count": 0,
"document_count": 0,
"create_time": 1715623400000,
"update_time": 1715624500000
}
}
```
---
## 2. 删除知识库 - `delete`
**接口描述**: 批量删除一个或多个知识库。删除知识库将连带删除其中的所有文档和索引数据,**不可恢复**。
**请求方法**: `DELETE`
**接口地址**: `/api/v1/datasets`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
#### Body Parameters (JSON)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| ids | array<string> | 是 | **ID 列表**。指定要删除的知识库 ID。如果传递 `null`,则会**清空当前租户下所有**知识库(高危操作,请谨慎使用)。 |
**Request Example**:
```json
{
"ids": ["kb_id_101", "kb_id_102"]
}
```
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "Successfully deleted 2 datasets, 0 failed...",
"data": {
"success_count": 2, // 成功删除的数量
"errors": [] // 失败的 ID 及原因列表
}
}
```
---
## 3. 获取知识库列表 - `list_datasets`
**接口描述**: 获取当前用户(及团队)有权限访问的知识库列表。支持分页、排序和筛选。
**请求方法**: `GET`
**接口地址**: `/api/v1/datasets`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
#### Query Parameters
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| page | int | 否 | 1 | **页码**。从 1 开始。 |
| page_size | int | 否 | 30 | **每页条数**。 |
| orderby | string | 否 | "create_time" | **排序字段**。可选值: `create_time` (创建时间), `update_time` (更新时间), `document_count` (文档数)。 |
| desc | boolean | 否 | true | **是否降序**`true`: 降序 (最新的在前); `false`: 升序。 |
| name | string | 否 | - | **名称筛选**。支持模糊匹配。 |
| id | string | 否 | - | **ID 筛选**。精确匹配知识库 ID。 |
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": [
{
"id": "kb_uuid_123",
"name": "HR 政策库",
"document_count": 12, // 包含的文档数量
"token_num": 10240, // 总 Token 数
"chunk_count": 150, // 总切片数
"create_time": 1715623400000,
"permission": "team",
"embedding_model": "BAAI/bge-large-zh-v1.5"
}
],
"total": 1 // 匹配查询条件的总记录数 (用户分页计算)
}
```
---
## 4. 更新知识库配置 - `update`
**接口描述**: 更新指定知识库的配置信息。注意:如果知识库内已有解析过的切片,通常不允许修改嵌入模型 (`embedding_model`)。
**请求方法**: `PUT`
**接口地址**: `/api/v1/datasets/<dataset_id>`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| dataset_id | string | 是 | 知识库 ID |
#### Body Parameters (JSON)
*(以下所有字段均为可选,仅传递需要修改的字段即可)*
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| name | string | - | **新名称**。需保持租户内唯一。 |
| avatar | string | - | **新头像**。Base64 字符串。 |
| description | string | - | **新描述**。 |
| permission | string | - | **新权限**`me``team`。 |
| embedding_model | string | - | **嵌入模型**。**注意**: 仅当知识库为空(chunk_count=0)时才允许修改。 |
| chunk_method | string | - | **默认解析方法**。修改后将应用于后续新上传的文件 (旧文件解析方式不变)。 |
| parser_config | object | - | **解析器配置**。全量覆盖旧配置 (结构参考 create 接口)。 |
| pagerank | int | 0 | **PageRank 权重**。仅在使用 Elasticsearch 引擎且需调整图谱权重时设置。 |
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": {
"id": "kb_uuid_...",
"name": "新名称",
"update_time": 1715629999000,
...
}
}
```
---
## 5. 获取知识图谱数据 - `knowledge_graph`
**接口描述**: 获取知识库构建的知识图谱数据,包含节点(Nodes)和边(Edges),用于前端可视化展示(如 ECharts 力导向图)。
**请求方法**: `GET`
**接口地址**: `/api/v1/datasets/<dataset_id>/knowledge_graph`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| dataset_id | string | 是 | 知识库 ID |
#### Query Parameters
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": {
"graph": {
"nodes": [
{
"id": "node_1",
"label": "人工智能", // 节点显示的文本
"pagerank": 0.05, // PageRank 权重 (决定节点大小)
"color": "#fcb", // 节点颜色
"img": "" // 节点图标 (如有)
},
{
"id": "node_2",
"label": "机器学习",
"pagerank": 0.03,
"color": "#e2b"
}
],
"edges": [
{
"source": "node_1", // 起始节点 ID
"target": "node_2", // 目标节点 ID
"weight": 0.8, // 边权重 (决定连线粗细)
"label": "includes" // 关系名称 (显示在连线上)
}
]
},
"mind_map": { // 思维导图结构的保留字段 (通常用于脑图展示)
"root": {
"id": "root_node",
"children": [...]
}
}
}
}
```
---
## 6. 清空知识图谱数据 - `delete_knowledge_graph`
**接口描述**: 删除指定知识库中已生成的知识图谱索引数据(包括所有实体节点和关系边)。
**注意**: 此操作**不会**删除原始文档或普通的向量索引,仅仅是重置图谱结构。如果需要重新生成图谱,请再次调用 `chunk` 相关接口或使用 `run_graphrag`
**请求方法**: `DELETE`
**接口地址**: `/api/v1/datasets/<dataset_id>/knowledge_graph`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| dataset_id | string | 是 | 知识库 ID |
#### Body Parameters
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": true
}
```
---
## 7. 运行/触发 GraphRAG 索引任务 - `run_graphrag`
**接口描述**: 触发后台异步任务,对知识库中的文档进行 GraphRAG 索引构建。此过程会使用 LLM 抽取实体(Entities)和关系(Relationships),并构建全局社区摘要。
**前提条件**: 知识库中必须包含已解析的文档。
**请求方法**: `POST`
**接口地址**: `/api/v1/datasets/<dataset_id>/run_graphrag`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| dataset_id | string | 是 | 知识库 ID |
#### Body Parameters (JSON)
*(Body 可为空 `{}`, 后续版本将扩展以下配置参数)*
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| entity_types | array | ["organization", "person", "geo", "event"] | **(预留)** 指定要抽取的实体类型列表。 |
| method | string | "light" | **(预留)** 构建模式: `light` (轻量级), `general` (标准), `complex` (深度)。 |
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": {
"graphrag_task_id": "task_uuid_12345678" // 异步任务 ID,用于后续追踪进度
}
}
```
---
## 8. 运行/触发 RAPTOR 递归摘要任务 - `run_raptor`
**接口描述**: 触发后台异步任务,对知识库中的文档运行 RAPTOR (Recursive Abstractive Processing for Tree-Organized Retrieval) 算法。
**功能说明**: 该算法会递归地对文档块进行聚类和摘要,生成多层级的树状索引,显著提升对长文档和复杂问题的回答能力。
**请求方法**: `POST`
**接口地址**: `/api/v1/datasets/<dataset_id>/run_raptor`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| dataset_id | string | 是 | 知识库 ID |
#### Body Parameters (JSON)
*(Body 可为空 `{}`, 后续版本将扩展以下配置参数)*
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| max_cluster | int | 64 | **(预留)** 最大聚类数。 |
| prompt | string | (内置摘要提示词) | **(预留)** 用于生成摘要的 Prompt。 |
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": {
"raptor_task_id": "task_uuid_87654321" // 异步任务 ID
}
}
```
---
## 9. 查询 GraphRAG 任务进度 - `trace_graphrag`
**接口描述**: 查询指定知识库当前 **GraphRAG** 索引构建任务的实时状态。支持长轮询机制监测进度。
**请求方法**: `GET`
**接口地址**: `/api/v1/datasets/<dataset_id>/trace_graphrag`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| dataset_id | string | 是 | 知识库 ID |
#### Query Parameters
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": {
"id": "task_uuid_12345678", // 任务 ID
"doc_id": "doc_uuid_...", // 当前正在处理的文档 ID (如果是多文档任务)
"from_page": 0, // 当前处理的起始页码
"to_page": 10, // 当前处理的结束页码
"progress": 0.45, // **总进度** (0.0 ~ 1.0)。0.0: 未开始/刚开始; 1.0: 完成; -1.0: 失败。
"progress_msg": "Extracting entities from chunk 25...", // **当前状态描述**。用于前端展示 Loading 提示。
"create_time": 1715623400000,
"update_time": 1715624500000
}
}
```
---
## 10. 查询 RAPTOR 任务进度 - `trace_raptor`
**接口描述**: 查询指定知识库当前 **RAPTOR** 递归摘要任务的实时状态。
**请求方法**: `GET`
**接口地址**: `/api/v1/datasets/<dataset_id>/trace_raptor`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| dataset_id | string | 是 | 知识库 ID |
#### Query Parameters
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": {
"id": "task_uuid_87654321",
"progress": 1.0, // 进度值。1.0 表示树构建完成。
"progress_msg": "Tree construction completed.", // 状态消息。
"create_time": 1715629000000
}
}
```
@@ -1,757 +0,0 @@
## 1. 上传文档 - `upload`
**接口描述**: 向指定的知识库上传一个或多个文档文件。上传后,文档将立即被存入文件系统/对象存储,并在数据库中创建记录。默认解析状态为 `UNSTART` (未开始),解析配置将继承自 KnowledgeBase 的默认设置。
**请求方法**: `POST`
**接口地址**: `/api/v1/datasets/<dataset_id>/documents`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
**Content-Type**: `multipart/form-data`
### 请求参数 (Request)
#### Path Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| dataset_id | string | 是 | **知识库 ID**。指定文档归属的知识库。 |
#### Form Data Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| file | file | 是 | **文件二进制流**。支持多文件上传 (Multiple Files)。<br>支持格式: PDF, DOCX, TXT, MD, CS, HTML, CSV, XLSX, PPTX 等。<br>单文件大小限制请参考系统配置 (默认通常为 10MB/100MB)。 |
| parent_path | string | 否 | **父级目录路径**。类似于文件系统的文件夹结构,默认为 `/`。如果指定 (如 `/docs/v1/`),文档将在该虚拟路径下列出。 |
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": [
{
"id": "e457f92e3c0411ef8d4c0242ac120003",
"thumbnail": null,
"dataset_id": "d1234567890abcdef1234567890abcde",
"chunk_method": "naive",
"pipeline_id": null,
"parser_config": {
"chunk_token_num": 512,
"delimiter": "\\n",
"layout_recognize": "DeepDOC",
"html4excel": false,
"auto_keywords": 0,
"auto_questions": 0,
"topn_tags": 3,
"raptor": {
"use_raptor": false
},
"graphrag": {
"use_graphrag": false
}
},
"source_type": "local",
"type": "pdf",
"created_by": "user_id_123",
"name": "UserGuide_v2.pdf",
"location": "UserGuide_v2.pdf",
"size": 102400,
"token_count": 0,
"chunk_count": 0,
"progress": 0.0,
"progress_msg": "",
"process_begin_at": null,
"process_duration": 0.0,
"meta_fields": {},
"suffix": "pdf",
"run": "UNSTART",
"status": "1",
"create_time": 1715623400123,
"create_date": "2024-05-13 10:03:20",
"update_time": 1715623400123,
"update_date": "2024-05-13 10:03:20"
}
]
}
```
---
## 2. 获取文档列表 - `list_docs`
**接口描述**: 查询知识库下的文档列表。支持分页检索、关键词搜索、状态筛选等功能。
**请求方法**: `GET`
**接口地址**: `/api/v1/datasets/<dataset_id>/documents`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| dataset_id | string | 是 | **知识库 ID**。 |
#### Query Parameters
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| page | int | 否 | 1 | **页码**。从 1 开始计数。 |
| page_size | int | 否 | 30 | **每页数量**。 |
| orderby | string | 否 | "create_time" | **排序字段**。支持 `create_time` (创建时间), `name` (文件名), `size` (大小) 等。 |
| desc | boolean | 否 | true | **是否降序**`true` (最新/最大在前), `false` (最旧/最小在前)。 |
| id | string | 否 | - | **精确筛选 ID**。仅返回指定 ID 的文档。 |
| name | string | 否 | - | **精确筛选文件名**。仅返回指定名称的文档。 |
| keywords | string | 否 | - | **模糊搜索**。匹配文档名称包含该关键词的记录。 |
| suffix | array | 否 | - | **文件后缀筛选** (如 `pdf`, `docx`)。 |
| run | array | 否 | - | **运行状态筛选**。可选值: `UNSTART`, `RUNNING`, `CANCEL`, `DONE`, `FAIL`。 |
| create_time_from | int | 否 | 0 | **起始时间戳** (毫秒)。查询在此时间之后创建的文档。 |
| create_time_to | int | 否 | 0 | **结束时间戳** (毫秒)。查询在此时间之前创建的文档。 |
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": {
"total": 128,
"docs": [
{
"id": "e457f92e3c0411ef8d4c0242ac120003",
"thumbnail": null,
"dataset_id": "d1234567890abcdef1234567890abcde",
"chunk_method": "naive",
"pipeline_id": null,
"parser_config": {
"chunk_token_num": 512,
"delimiter": "\\n",
"layout_recognize": "DeepDOC",
"html4excel": false,
"auto_keywords": 0,
"auto_questions": 0,
"topn_tags": 3,
"raptor": {
"use_raptor": false
},
"graphrag": {
"use_graphrag": false
}
},
"source_type": "local",
"type": "pdf",
"created_by": "user_id_123",
"name": "UserGuide_v2.pdf",
"location": "UserGuide_v2.pdf",
"size": 102400,
"token_count": 45000,
"chunk_count": 120,
"progress": 1.0,
"progress_msg": "Parsing finished",
"process_begin_at": "2024-05-13 10:05:00",
"process_duration": 45.2,
"meta_fields": {
"author": "RAGFlow Team",
"version": "2.0"
},
"suffix": "pdf",
"run": "DONE",
"status": "1",
"create_time": 1715623400123,
"create_date": "2024-05-13 10:03:20",
"update_time": 1715623450000,
"update_date": "2024-05-13 10:05:45"
}
]
}
}
```
---
## 3. 更新文档信息 - `update_doc`
**接口描述**: 更新文档的名称、状态或解析配置。
**特别注意**: 如果修改了 `chunk_method``parser_config`,后端会自动将 `run` 状态重置为 `UNSTART`,并清除已有的 chunk 数据,等待重新解析。
**请求方法**: `PUT`
**接口地址**: `/api/v1/datasets/<dataset_id>/documents/<document_id>`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| dataset_id | string | 是 | **知识库 ID**。 |
| document_id | string | 是 | **文档 ID**。 |
#### Body Parameters (JSON)
*(仅需传递要修改的字段)*
| 参数名 | 类型 | 说明 |
|---|---|---|
| name | string | **新文档名称**。需包含文件后缀且不能改变原始文件类型 (如从 `.pdf` 改为 `.txt` 会导致错误)。 |
| enabled | boolean | **启用/禁用**`true`: 启用 (DEFAULT, 对应 status="1"); `false`: 禁用 (对应 status="0")。禁用后该文档不参与检索。 |
| chunk_method | string | **解析方法**。可选值: `naive`, `manual`, `qa`, `table`, `paper`, `book`, `laws`, `presentation`, `picture`, `one`, `knowledge_graph`, `email`。 |
| parser_config | object | **解析器详细配置**。应与 `chunk_method` 匹配。以下列出 `naive` (通用) 方法的完整配置参数。 |
**parser_config (Naive 模式全量参数)**:
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| chunk_token_num | int | 512 | **切片最大 Token 数**。 |
| delimiter | string | "\\n" | **分段符**。支持转义字符。 |
| layout_recognize | string | "DeepDOC" | **布局识别模型**。可选 `DeepDOC``Simple`。 |
| html4excel | boolean | false | **Excel转HTML**。是否将 Excel 解析为 HTML 表格。 |
| auto_keywords | int | 0 | **自动关键词数量**。0 表示不抽取。 |
| auto_questions | int | 0 | **自动问题数量**。0 表示不生成。 |
| topn_tags | int | 3 | **自动标签数量**。 |
| raptor | object | `{ "use_raptor": false }` | **RAPTOR 配置**。设置 `use_raptor: true` 可开启递归摘要索引。 |
| graphrag | object | `{ "use_graphrag": false }` | **GraphRAG 配置**。设置 `use_graphrag: true` 可开启图谱增强。 |
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": {
"id": "e457f92e3c0411ef8d4c0242ac120003",
"thumbnail": null,
"dataset_id": "d1234567890abcdef1234567890abcde",
"chunk_method": "naive",
"pipeline_id": null,
"parser_config": {
"chunk_token_num": 1024,
"delimiter": "\\n",
"layout_recognize": "DeepDOC",
"html4excel": false,
"auto_keywords": 0,
"auto_questions": 0,
"topn_tags": 3,
"raptor": {
"use_raptor": false
},
"graphrag": {
"use_graphrag": false
}
},
"source_type": "local",
"type": "pdf",
"created_by": "user_id_123",
"name": "Renamed_Guide.pdf",
"location": "UserGuide_v2.pdf",
"size": 102400,
"token_count": 45000,
"chunk_count": 0,
"progress": 0.0,
"progress_msg": "",
"process_begin_at": null,
"process_duration": 0.0,
"meta_fields": {},
"suffix": "pdf",
"run": "UNSTART",
"status": "0",
"create_time": 1715623400123,
"create_date": "2024-05-13 10:03:20",
"update_time": 1715629999000,
"update_date": "2024-05-13 12:00:00"
}
}
```
---
## 4. 删除文档 - `delete`
**接口描述**: 物理删除一个或多个文档。此操作不可恢复,将同时删除数据库记录、MinIO 中的源文件以及 Elasticsearch 中的所有相关切片索引。
**请求方法**: `DELETE`
**接口地址**: `/api/v1/datasets/<dataset_id>/documents`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| dataset_id | string | 是 | **知识库 ID**。 |
#### Body Parameters (JSON)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| ids | array<string> | 是 | **文档 ID 列表**。必须指定要删除的文档 ID。 |
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": null
}
```
---
## 5. 下载/预览原始文件 - `download`
**接口描述**: 获取文档的原始二进制文件流。响应头将会包含 `Content-Disposition` 字段,指示浏览器以附件形式下载。
**请求方法**: `GET`
**接口地址**: `/api/v1/datasets/<dataset_id>/documents/<document_id>`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| dataset_id | string | 是 | **知识库 ID**。 |
| document_id | string | 是 | **文档 ID**。 |
### 响应参数 (Response)
**Content-Type**: `application/octet-stream`
**Content-Disposition**: `attachment; filename="UserGuide_v2.pdf"`
*(直接返回文件的二进制数据流)*
## 6. 触发/重试文档解析 - `parse`
**接口描述**: 手动触发文档的解析任务。通常在上传文件后、或修改了解析配置(如 `chunk_method`)后调用此接口。支持批量触发。
**请求方法**: `POST`
**接口地址**: `/api/v1/datasets/<dataset_id>/chunks`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| dataset_id | string | 是 | **知识库 ID**。 |
#### Body Parameters (JSON)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| document_ids | array<string> | 是 | **文档 ID 列表**。指定需要(重新)解析的文档 ID。 |
**Request Example**:
```json
{
"document_ids": ["doc_id_1", "doc_id_2"]
}
```
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": null
}
```
---
## 7. 停止文档解析 - `stop_parsing`
**接口描述**: 停止当前正在进行的文档解析任务。
**请求方法**: `DELETE`
**接口地址**: `/api/v1/datasets/<dataset_id>/chunks`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| dataset_id | string | 是 | **知识库 ID**。 |
#### Body Parameters (JSON)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| document_ids | array<string> | 是 | **文档 ID 列表**。指定要停止解析的任务。 |
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": null
}
```
---
## 8. 获取切片列表 - `list_chunks`
**接口描述**: 获取指定文档已解析出的切片(Chunk)列表。支持分页和关键词搜索。返回结果包含文档的详细元数据和具体的切片内容。
**请求方法**: `GET`
**接口地址**: `/api/v1/datasets/<dataset_id>/documents/<document_id>/chunks`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| dataset_id | string | 是 | **知识库 ID**。 |
| document_id | string | 是 | **文档 ID**。 |
#### Query Parameters
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| page | int | 否 | 1 | **页码**。 |
| page_size | int | 否 | 30 | **每页数量**。 |
| keywords | string | 否 | - | **搜索关键词**。在切片内容中进行全文检索。 |
| id | string | 否 | - | **精确切片 ID**。若指定,则只返回该 ID 对应的切片。 |
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": {
"total": 150,
"chunks": [
{
"id": "e457f92e3c0411ef8d4c0242ac120003_0",
"content": "RAGFlow 是一款基于深度文档理解的开源 RAG(检索增强生成)引擎。它旨在为各种规模的企业提供精简的 RAG 工作流。RAGFlow 结合了传统文档处理的稳健性与现代大语言模型(LLM)的生成能力,确保在处理复杂格式数据(如 PDF 表格、扫描件等)时依然能保持极高的召回率和准确性。",
"document_id": "doc_uuid_123",
"docnm_kwd": "RAGFlow_UserGuide_v2.pdf",
"important_keywords": ["RAGFlow", "开源", "深度文档理解", "LLM"],
"questions": ["什么是 RAGFlow?", "RAGFlow 的主要特点是什么?"],
"image_id": "",
"dataset_id": "kb_uuid_456",
"available": true,
"positions": [1]
},
{
"id": "e457f92e3c0411ef8d4c0242ac120003_1",
"content": "主要特性:\n1. **深度文档解析**:内置 DeepDOC 识别引擎,精准还原表格、段落结构。\n2. **多路召回**:支持关键词 + 向量的混合检索。\n3. **可视化编排**:提供基于 Graph 的工作流编排能力。",
"document_id": "doc_uuid_123",
"docnm_kwd": "RAGFlow_UserGuide_v2.pdf",
"important_keywords": ["DeepDOC", "混合检索", "可视化编排"],
"questions": [],
"image_id": "img_uuid_789",
"dataset_id": "kb_uuid_456",
"available": true,
"positions": [2]
}
],
"doc": {
"id": "doc_uuid_123",
"name": "RAGFlow_UserGuide_v2.pdf",
"chunk_count": 150,
"token_count": 45000,
"chunk_method": "naive",
"run": "DONE",
"status": "1",
"progress": 1.0,
"progress_msg": "Parsing finished",
"process_begin_at": "2024-05-13 10:05:00",
"process_duration": 45.2,
"meta_fields": {
"author": "RAGFlow Team",
"version": "2.0"
},
"create_time": 1715623400123,
"create_date": "2024-05-13 10:03:20",
"update_time": 1715623450000,
"update_date": "2024-05-13 10:05:45",
"dataset_id": "kb_uuid_456"
}
}
}
```
---
## 9. 手动新增切片 - `add_chunk`
**接口描述**: 向指定文档中手动添加一个新的切片。系统会自动计算该切片的向量嵌入 (Embedding)。
**请求方法**: `POST`
**接口地址**: `/api/v1/datasets/<dataset_id>/documents/<document_id>/chunks`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| dataset_id | string | 是 | **知识库 ID**。 |
| document_id | string | 是 | **文档 ID**。 |
#### Body Parameters (JSON)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| content | string | 是 | **切片内容**。手动输入的文本内容。 |
| important_keywords | array<string> | 否 | **重要关键词**。用于关键词检索增强。 |
| questions | array<string> | 否 | **预设问题**。用于 Q&A 检索模式增强。 |
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": {
"chunk": {
"id": "new_chunk_uuid_999",
"content": "这是管理员手动添加的一条补充切片,用于修正文档中缺失的关键信息。",
"document_id": "doc_uuid_123",
"docnm_kwd": "RAGFlow_UserGuide_v2.pdf",
"important_keywords": ["手动添加", "补充信息"],
"questions": ["如何手动添加切片?"],
"image_id": "",
"dataset_id": "kb_uuid_456",
"available": true,
"positions": []
}
}
}
```
---
## 10. 修改切片信息 - `update_chunk`
**接口描述**: 修改已存在的切片内容、关键词、可用状态等。修改内容后,系统会自动重新计算向量。
**请求方法**: `PUT`
**接口地址**: `/api/v1/datasets/<dataset_id>/documents/<document_id>/chunks/<chunk_id>`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| dataset_id | string | 是 | **知识库 ID**。 |
| document_id | string | 是 | **文档 ID**。 |
| chunk_id | string | 是 | **切片 ID**。 |
#### Body Parameters (JSON)
*(以下字段均为可选,仅传递需修改的字段)*
| 参数名 | 类型 | 说明 |
|---|---|---|
| content | string | **新的切片内容**。 |
| important_keywords | array<string> | **更新关键词列表**。覆盖原有列表。 |
| available | boolean | **启用/禁用**`true`: 启用 (默认); `false`: 禁用 (检索时将忽略此切片)。 |
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": null
}
```
---
## 11. 删除切片 - `rm_chunk`
**接口描述**: 批量删除文档中的指定切片。
**请求方法**: `DELETE`
**接口地址**: `/api/v1/datasets/<dataset_id>/documents/<document_id>/chunks`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| dataset_id | string | 是 | **知识库 ID**。 |
| document_id | string | 是 | **文档 ID**。 |
#### Body Parameters (JSON)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| chunk_ids | array<string> | 是 | **切片 ID 列表**。 |
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "deleted 2 chunks",
"data": null
}
```
## 12. 获取元数据摘要 - `metadata_summary`
**接口描述**: 获取知识库中所有文档的元数据摘要信息。通常用于前端展示知识库的数据分布概况,例如不同文件类型的数量统计、文件状态分布等。
**请求方法**: `GET`
**接口地址**: `/api/v1/datasets/<dataset_id>/metadata/summary`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| dataset_id | string | 是 | **知识库 ID**。 |
#### Query Parameters
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": {
"summary": {
"total_doc_count": 120,
"total_token_count": 500000,
"file_type_distribution": {
"pdf": 80,
"docx": 30,
"txt": 10
},
"status_distribution": {
"1": 118, // 正常启用
"0": 2 // 禁用
},
"custom_metadata": {
"author": {
"Alice": 50,
"Bob": 30
},
"department": {
"HR": 20,
"Engineering": 100
}
}
}
}
}
```
---
## 13. 批量更新元数据 - `metadata_batch_update`
**接口描述**: 对知识库中的文档进行批量元数据修改。支持基于复杂的条件筛选文档,然后执行批量更新或删除元数据字段的操作。
**请求方法**: `POST`
**接口地址**: `/api/v1/datasets/<dataset_id>/metadata/update`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| dataset_id | string | 是 | **知识库 ID**。 |
#### Body Parameters (JSON)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| selector | object | 否 | **筛选器**。定义要更新哪些文档。如果不传,可能作用于全量文档(请谨慎)。 |
| updates | array | 否 | **更新操作列表**。包含 `key``value`。 |
| deletes | array | 否 | **删除操作列表**。包含 `key`。 |
**Request Example (复杂场景)**:
```json
{
"selector": {
"document_ids": ["doc_id_101", "doc_id_102"],
"metadata_condition": {
"logic": "and",
"conditions": [
{"key": "author", "value": "OldName", "operator": "eq"},
{"key": "status", "value": "draft", "operator": "eq"}
]
}
},
"updates": [
{"key": "author", "value": "Admin"},
{"key": "reviewed_by", "value": "ManagerA"}
],
"deletes": [
{"key": "temp_tag"},
{"key": "draft_flag"}
]
}
```
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": {
"updated": 2, // 实际更新成功的文档数量
"matched_docs": 2 // 匹配到的文档数量
}
}
```
---
## 14. 检索测试 (Hit Test) - `retrieval_test`
**接口描述**: 在指定的知识库中进行模拟检索测试。此接口用于验证分段(Chunk)质量、检索参数(相似度阈值、Top K)的效果,是调试 RAG 效果的核心工具。
**请求方法**: `POST`
**接口地址**: `/api/v1/retrieval`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
**注意**: 即使是简单的查询,由于包含较多配置参数,本接口也设计为 `POST` 请求。
### 请求参数 (Request)
#### Path Parameters
#### Body Parameters (JSON)
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| dataset_ids | array<string> | 是 | - | **目标知识库 ID 列表**。支持跨多个知识库检索。 |
| question | string | 是 | - | **用户查询问题**。 |
| similarity_threshold | float | 否 | 0.2 | **相似度阈值**。低于此分数的 Chunk 将被过滤。 |
| vector_similarity_weight | float | 否 | 0.3 | **向量权重**。混合检索时,向量检索结果的权重 (0~1)。剩余权重归于关键词检索。 |
| top_k | int | 否 | 1024 | **初筛数量**。向量检索返回的候选切片数量。 |
| rerank_id | string | 否 | - | **重排模型 ID**。若指定,将对检索结果进行 Rerank 二次排序。 |
| highlight | boolean | 否 | true | **高亮匹配**。是否在返回内容中高亮关键词。 |
| keyword | boolean | 否 | false | **关键词增强**。是否使用 LLM 提取问题关键词以增强检索。 |
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": {
"total": 15,
"chunks": [
{
"id": "e457f92e3c0411ef8d4c0242ac120003_12",
"content": "RAGFlow 支持多种文档解析模式,其中 DeepDOC 模式特别适合处理包含大量表格和扫描件的 PDF 文档。它使用深度学习模型识别文档布局,精准提取表格内容。",
"document_id": "doc_uuid_123",
"dataset_id": "kb_uuid_456",
"document_name": "RAGFlow_UserGuide_v2.pdf",
"document_keyword": "RAGFlow_UserGuide_v2.pdf",
"similarity": 0.88,
"vector_similarity": 0.85,
"term_similarity": 0.92,
"index": 12,
"highlight": "RAGFlow 支持多种<em>文档解析模式</em>,其中 <em>DeepDOC</em> 模式特别适合处理包含大量表格和扫描件的 PDF 文档。",
"important_keywords": ["DeepDOC", "PDF"],
"questions": ["DeepDOC 模式有什么用?"],
"image_id": "",
"positions": [12]
},
{
"id": "e457f92e3c0411ef8d4c0242ac120003_15",
"content": "如果文档主要由纯文本构成,建议使用 Naive 模式。该模式解析速度快,适合通用场景。",
"document_id": "doc_uuid_123",
"dataset_id": "kb_uuid_456",
"document_name": "RAGFlow_UserGuide_v2.pdf",
"document_keyword": "RAGFlow_UserGuide_v2.pdf",
"similarity": 0.45,
"vector_similarity": 0.40,
"term_similarity": 0.50,
"index": 15,
"highlight": "如果文档主要由纯文本构成,建议使用 <em>Naive</em> 模式。",
"important_keywords": ["Naive", "纯文本"],
"questions": [],
"image_id": "",
"positions": [15]
}
],
"doc_aggs": [
{
"doc_name": "RAGFlow_UserGuide_v2.pdf",
"doc_id": "doc_uuid_123",
"count": 2
}
]
}
}
```
@@ -1,503 +0,0 @@
# RAGFlow 文件管理接口详解 (File Management API)
## 1. 上传文件 - `upload`
**接口描述**: 上传一个或多个文件到指定文件夹。支持多文件上传 (Multipart)。上传成功后,文件将存储在 MinIO/S3 中,并返回文件元数据列表。
**请求方法**: `POST`
**接口地址**: `/api/v1/file/upload`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
**Content-Type**: `multipart/form-data`
### 请求参数 (Request)
#### Form Data Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| file | file | 是 | **文件二进制流**。支持多文件上传。 |
| parent_id | string | 否 | **父级目录 ID**。如果省略,默认上传到根目录 (root)。 |
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": [
{
"id": "e457f92e3c0411ef8d4c0242ac120003",
"parent_id": "root_folder_id_123",
"tenant_id": "tenant_uuid_456",
"created_by": "user_uuid_789",
"type": "pdf",
"name": "ProjectReport.pdf",
"location": "ProjectReport.pdf",
"size": 204800,
"source_type": "",
"create_time": 1715623400123,
"create_date": "2024-05-13 10:03:20",
"update_time": 1715623400123,
"update_date": "2024-05-13 10:03:20"
}
]
}
```
---
## 2. 新建文件夹 - `create`
**接口描述**: 在指定父目录下创建一个新的文件夹(逻辑目录)。
**请求方法**: `POST`
**接口地址**: `/api/v1/file/create`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Body Parameters (JSON)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| name | string | 是 | **文件夹名称**。同一目录下不可重名。 |
| parent_id | string | 否 | **父级目录 ID**。省略则默认为根目录。 |
| type | string | 是 | **类型**。固定值为 `FOLDER` 创建文件夹。 |
**Request Example**:
```json
{
"name": "Year2024_Reports",
"parent_id": "root_folder_id_123",
"type": "FOLDER"
}
```
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": {
"id": "folder_uuid_abc",
"parent_id": "root_folder_id_123",
"tenant_id": "tenant_uuid_456",
"created_by": "user_uuid_789",
"name": "Year2024_Reports",
"location": "",
"size": 0,
"type": "folder",
"source_type": "",
"create_time": 1715623500000,
"create_date": "2024-05-13 10:05:00",
"update_time": 1715623500000,
"update_date": "2024-05-13 10:05:00"
}
}
```
---
## 3. 获取文件列表 - `list_files`
**接口描述**: 分页获取指定文件夹下的文件和子文件夹列表。支持按名称模糊搜索。
**请求方法**: `GET`
**接口地址**: `/api/v1/file/list`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Query Parameters
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| parent_id | string | 否 | (Root) | **父级目录 ID**。指定要查看的目录 ID。 |
| keywords | string | 否 | - | **搜索关键词**。按文件名模糊搜索。 |
| page | int | 否 | 1 | **页码**。 |
| page_size | int | 否 | 15 | **每页数量**。 |
| orderby | string | 否 | "create_time" | **排序字段**。 |
| desc | boolean | 否 | true | **是否降序**。 |
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": {
"total": 25,
"parent_folder": {
"id": "root_folder_id_123",
"parent_id": "",
"tenant_id": "tenant_uuid_456",
"created_by": "system",
"name": "ROOT",
"location": "",
"size": 0,
"type": "folder",
"source_type": "",
"create_time": 1710000000000,
"create_date": "2024-03-01 00:00:00",
"update_time": 1710000000000,
"update_date": "2024-03-01 00:00:00"
},
"files": [
{
"id": "folder_uuid_abc",
"parent_id": "root_folder_id_123",
"tenant_id": "tenant_uuid_456",
"created_by": "user_uuid_789",
"name": "Year2024_Reports",
"location": "",
"size": 0,
"type": "folder",
"source_type": "",
"create_time": 1715623500000,
"create_date": "2024-05-13 10:05:00",
"update_time": 1715623500000,
"update_date": "2024-05-13 10:05:00"
},
{
"id": "e457f92e3c0411ef8d4c0242ac120003",
"parent_id": "root_folder_id_123",
"tenant_id": "tenant_uuid_456",
"created_by": "user_uuid_789",
"name": "ProjectReport.pdf",
"location": "ProjectReport.pdf",
"size": 204800,
"type": "pdf",
"source_type": "",
"create_time": 1715623400123,
"create_date": "2024-05-13 10:03:20",
"update_time": 1715623400123,
"update_date": "2024-05-13 10:03:20"
}
]
}
}
```
---
## 4. 获取文件流 (下载) - `get`
**接口描述**: 通过文件 ID 下载文件内容。不同于获取元数据,该接口直接返回文件的二进制流(Octet-stream 或 Image 等)。
**请求方法**: `GET`
**接口地址**: `/api/v1/file/get/<file_id>`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| file_id | string | 是 | **文件 ID**。 |
### 响应参数 (Response)
**Content-Type**: `application/octet-stream` (或具体 MIME 类型如 `image/png`)
*(返回二进制文件流)*
---
## 5. 下载附件 - `download_attachment`
**接口描述**: 这是一个通用的附件下载接口,通常用于系统内部引用或特定路径的下载。它使用 `attachment_id`(通常对应 MinIO 中的存储路径/Key)来检索文件。
**请求方法**: `GET`
**接口地址**: `/api/v1/file/download/<attachment_id>`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| attachment_id | string | 是 | **附件 ID / 存储 Key**。通常对应底层存储的唯一标识符。 |
#### Query Parameters
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| ext | string | 否 | "markdown" | **文件扩展名**。用于设置响应头中的 Content-Type。 |
### 响应参数 (Response)
**Content-Type**: `application/octet-stream` (或根据 ext 参数推断)
*(返回二进制文件流)*
## 6. 重命名文件/文件夹 - `rename`
**接口描述**: 修改文件或文件夹的名称。对于文件,通常不允许修改扩展名(后缀)。
**请求方法**: `POST`
**接口地址**: `/api/v1/file/rename`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Body Parameters (JSON)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| file_id | string | 是 | **目标文件/文件夹 ID**。 |
| name | string | 是 | **新名称**。需符合文件命名规范,且同一目录下不可重名。 |
**Request Example**:
```json
{
"file_id": "file_uuid_123",
"name": "New_Report_Final.pdf"
}
```
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": true
}
```
---
## 7. 移动文件/文件夹 - `move`
**接口描述**: 批量移动文件或文件夹到指定的目录 (Move)。
**请求方法**: `POST`
**接口地址**: `/api/v1/file/mv`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Body Parameters (JSON)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| src_file_ids | array<string> | 是 | **源文件/文件夹 ID 列表**。支持批量移动。 |
| dest_file_id | string | 是 | **目标文件夹 ID**。必须是已存在的文件夹 ID。 |
**Request Example**:
```json
{
"src_file_ids": ["file_id_1", "file_id_2"],
"dest_file_id": "folder_id_target"
}
```
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": true
}
```
---
## 8. 删除文件/文件夹 - `rm`
**接口描述**: 批量删除文件或文件夹。如果是文件夹,将递归删除其下的所有内容。此操作不可恢复。
**请求方法**: `POST`
**接口地址**: `/api/v1/file/rm`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Body Parameters (JSON)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| file_ids | array<string> | 是 | **待删除的文件/文件夹 ID 列表**。 |
**Request Example**:
```json
{
"file_ids": ["file_uuid_to_delete_1", "folder_uuid_to_delete_2"]
}
```
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": true
}
```
---
## 9. 文件转知识库文档 - `convert`
**接口描述**: 将已上传的文件(File)导入到指定的知识库(Dataset)中,转换为文档(Document)并进行解析。这是一个“文件 -> 知识库”的桥接操作。
**请求方法**: `POST`
**接口地址**: `/api/v1/file/convert`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Body Parameters (JSON)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| file_ids | array<string> | 是 | **源文件 ID 列表**。必须是已存在于文件管理系统中的 ID。 |
| kb_ids | array<string> | 是 | **目标知识库 ID 列表**。文件将被同时导入到这些知识库中。 |
**Request Example**:
```json
{
"file_ids": ["file_uuid_pdf_1", "file_uuid_txt_2"],
"kb_ids": ["dataset_uuid_A"]
}
```
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": [
{
"id": "mapping_uuid_1",
"file_id": "file_uuid_pdf_1",
"document_id": "doc_uuid_created_in_kb_A",
"create_time": 1715623600123,
"create_date": "2024-05-13 10:06:40",
"update_time": 1715623600123,
"update_date": "2024-05-13 10:06:40"
},
{
"id": "mapping_uuid_2",
"file_id": "file_uuid_txt_2",
"document_id": "doc_uuid_created_in_kb_A",
"create_time": 1715623600124,
"create_date": "2024-05-13 10:06:40",
"update_time": 1715623600124,
"update_date": "2024-05-13 10:06:40"
}
]
}
```
## 10. 获取根目录信息 - `get_root_folder`
**接口描述**: 获取当前用户的根目录文件夹信息。每个用户(Tenant)都有且仅有一个系统自动创建的根目录。
**请求方法**: `GET`
**接口地址**: `/api/v1/file/root_folder`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Query Parameters
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": {
"root_folder": {
"id": "root_folder_id_123",
"parent_id": "",
"tenant_id": "tenant_uuid_456",
"created_by": "system",
"name": "ROOT",
"location": "",
"size": 0,
"type": "folder",
"source_type": "",
"create_time": 1710000000000,
"create_date": "2024-03-01 00:00:00",
"update_time": 1710000000000,
"update_date": "2024-03-01 00:00:00"
}
}
}
```
---
## 11. 获取父目录信息 - `get_parent_folder`
**接口描述**: 获取指定文件或文件夹的直接父级目录信息。
**请求方法**: `GET`
**接口地址**: `/api/v1/file/parent_folder`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Query Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| file_id | string | 是 | **当前文件/文件夹 ID**。 |
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": {
"parent_folder": {
"id": "root_folder_id_123",
"parent_id": "",
"tenant_id": "tenant_uuid_456",
"created_by": "system",
"name": "ROOT",
"location": "",
"size": 0,
"type": "folder",
"source_type": "",
"create_time": 1710000000000,
"create_date": "2024-03-01 00:00:00",
"update_time": 1710000000000,
"update_date": "2024-03-01 00:00:00"
}
}
}
```
---
## 12. 获取完整路径 (面包屑) - `get_all_parent_folders`
**接口描述**: 获取指定文件或文件夹的所有上级目录列表,形成完整的路径链。返回的列表顺序通常是从根目录到直接父目录(有序)。此接口常用于前端展示“面包屑导航” (Breadcrumbs)。
**请求方法**: `GET`
**接口地址**: `/api/v1/file/all_parent_folder`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Query Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| file_id | string | 是 | **目标文件/文件夹 ID**。 |
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": {
"parent_folders": [
{
"id": "root_folder_id_123",
"parent_id": "",
"tenant_id": "tenant_uuid_456",
"created_by": "system",
"name": "ROOT",
"location": "",
"size": 0,
"type": "folder",
"source_type": "",
"create_time": 1710000000000,
"create_date": "2024-03-01 00:00:00",
"update_time": 1710000000000,
"update_date": "2024-03-01 00:00:00"
},
{
"id": "folder_project_a_id",
"parent_id": "root_folder_id_123",
"tenant_id": "tenant_uuid_456",
"created_by": "user_id_001",
"name": "Project A Docs",
"location": "",
"size": 0,
"type": "folder",
"source_type": "",
"create_time": 1715000000000,
"create_date": "2024-05-01 09:00:00",
"update_time": 1715000000000,
"update_date": "2024-05-01 09:00:00"
}
]
}
}
```
@@ -1,228 +0,0 @@
# RAGFlow 搜索机器人 & AgentBot 接口详解 (SearchBot & AgentBot)
## 1. 搜索机器人对话 - `ask_about_embedded`
**接口描述**: 面向 **SearchBot (搜索机器人)** 的核心对话接口,通常用于嵌入式知识库问答场景。与普通 Chat 不同,它更侧重于从指定的 `kb_ids` 中直接检索答案,且鉴权使用 `Authorization: Bearer <Beta_Token>` (即 API Key)。
**请求方法**: `POST`
**接口地址**: `/api/v1/searchbots/ask`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Body Parameters (JSON)
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| question | string | 是 | - | **用户问题**。 |
| kb_ids | array<string> | 是 | - | **知识库 ID 列表**。限定从哪些知识库中检索。 |
| search_id | string | 否 | - | **搜索应用 ID**。如果指定,将使用该搜索应用的配置 (Search App Config)。 |
**Request Example**:
```json
{
"question": "What is the refund policy?",
"kb_ids": ["dataset_uuid_1", "dataset_uuid_2"],
"search_id": "search_app_uuid_abc"
}
```
### 响应参数 (Stream Response)
**Content-Type**: `text/event-stream`
```text
data:{"code": 0, "message": "", "data": {"answer": "According to the ", "reference": {}}}
data:{"code": 0, "message": "", "data": {"answer": "policy, refunds are processed within 7 days.", "reference": {"chunk_1": {"content_with_weight": "Refunds...", "doc_name": "policy.pdf"}}}}
data:{"code": 0, "message": "", "data": true} // 结束标志
```
---
## 2. 获取思维导图 - `mindmap`
**接口描述**: 根据用户的查询或对话上下文,生成用于前端展示的思维导图数据结构。这通常用于帮助用户梳理复杂的搜索结果或知识结构。
**请求方法**: `POST`
**接口地址**: `/api/v1/searchbots/mindmap`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Body Parameters (JSON)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| question | string | 是 | **用户问题/主题**。 |
| kb_ids | array<string> | 是 | **知识库 ID 列表**。 |
| search_id | string | 否 | **搜索应用 ID**。 |
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": {
"root": {
"text": "Refund Policy", // 根节点文本
"children": [
{
"text": "Conditions",
"children": [
{ "text": "Product defect" },
{ "text": "Shipping error" }
]
},
{
"text": "Timeline",
"children": [
{ "text": "7-14 business days" }
]
}
]
}
}
}
```
---
## 3. 获取相关推荐问题 - `related_questions_embedded`
**接口描述**: 根据用户当前的问题,生成一组相关的推荐问题 (Suggest Questions)。常用于搜索结果页底部的“猜你想问”。
**请求方法**: `POST`
**接口地址**: `/api/v1/searchbots/related_questions`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Body Parameters (JSON)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| question | string | 是 | **用户当前问题**。 |
| search_id | string | 否 | **搜索应用 ID**。 |
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": [
"How to apply for a refund online?",
"What items are non-refundable?",
"Contact customer support"
]
}
```
---
## 4. 获取 AgentBot 输入项 - `begin_inputs`
**接口描述**: 获取 **AgentBot** (嵌入式 Agent) 的初始化信息,特别是前置输入项 (Prolog/Inputs)。这用于在用户开始对话前,展示一个表单让用户输入必要信息(如姓名、邮箱、API Key 等),这些信息会被传递给 Agent 的 `Begin` 节点。
**请求方法**: `GET`
**接口地址**: `/api/v1/agentbots/<agent_id>/inputs`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| agent_id | string | 是 | **Agent ID**。 |
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": {
"title": "Booking Assistant",
"avatar": "http://...",
"prologue": "Welcome! Please tell me your details.",
"inputs": { // `Begin` 节点定义的输入变量
"user_name": {
"type": "string",
"description": "Your Name",
"required": true
},
"email": {
"type": "string",
"description": "Contact Email",
"required": false
}
},
"mode": "chat"
}
}
```
---
## 5. AgentBot 对话交互 - `agent_bot_completions`
**接口描述**: 面向 **AgentBot** 的嵌入式对话接口。与 `agent_completions` 类似,但它专为无需登录的 C 端用户设计,通过 API Key 鉴权。它支持完整的 Agent 流程执行和流式响应。
**请求方法**: `POST`
**接口地址**: `/api/v1/agentbots/<agent_id>/completions`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| agent_id | string | 是 | **Agent ID**。 |
#### Body Parameters (JSON)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| session_id | string | 是 | **会话 ID**。 |
| inputs | object | 否 | **前置输入值**。对应 `begin_inputs` 中定义的变量,如 `{"user_name": "Alice"}`。 |
| query | string | 否 | **用户输入**。 |
| stream | boolean | 否 | **是否流式**。默认 `true`。 |
**Request Example**:
```json
{
"session_id": "session_uuid_123",
"inputs": {
"user_name": "Bob"
},
"query": "I want to book a room.",
"stream": true
}
```
### 响应参数 (Stream Response)
**Content-Type**: `text/event-stream`
```text
data:{"event": "message", "data": {"content": "Hello Bob, ", "reference": {}}}
data:{"event": "message", "data": {"content": "when do you want to check in?", "reference": {}}}
```
---
## 6. Agent OpenAI 兼容接口 - `agents_completion_openai_compatibility`
**接口描述**: 专门针对 Agent 的 **OpenAI 兼容** 接口。这使得外部工具可以像调用 OpenAI Chat Completion 一样调用 RAGFlow 配置好的复杂 Agent。
**请求方法**: `POST`
**接口地址**: `/api/v1/agents_openai/<agent_id>/chat/completions`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Path Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| agent_id | string | 是 | **Agent ID**。 |
#### Body Parameters (OpenAI Standard)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| messages | array | 是 | 包含 `role`, `content` 的消息数组。 |
| model | string | 是 | 占位符,任意字符串。 |
| stream | boolean | 否 | 默认 `true`。 |
### 响应参数 (Stream Response - OpenAI Format)
**Content-Type**: `text/event-stream`
```text
data: {"id": "agent-chat-uuid", "object": "chat.completion.chunk", "created": 1715000000, "model": "ragflow_agent", "choices": [{"index": 0, "delta": {"role": "assistant", "content": ""}, "finish_reason": null}]}
data: {"id": "agent-chat-uuid", "object": "chat.completion.chunk", "created": 1715000001, "model": "ragflow_agent", "choices": [{"index": 0, "delta": {"content": "Processing your request..."}, "finish_reason": null}]}
data: [DONE]
```
@@ -1,168 +0,0 @@
# RAGFlow SearchBot 补充与通用会话接口详解 (Session Extras)
## 1. 获取引用详情 - `detail_share_embedded`
**接口描述**: 当用户点击 SearchBot 回复中的引用标号 (e.g., [1]) 时,调用此接口获取该引用的详细内容(包括原文片段、来源文档名等)。此接口通常用于前端展示“引用来源”侧边栏或弹窗。它使用 API Key (Beta Token) 进行鉴权。
**请求方法**: `GET`
**接口地址**: `/api/v1/searchbots/detail`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Query Parameters
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| search_id | string | 是 | **搜索应用/SearchBot ID**。此接口需要验证调用者是否有权访问该 SearchBot。 |
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": {
"id": "search_app_uuid_123",
"title": "IT Knowledge Base",
"description": "Tech support search bot",
"kb_ids": ["kb_uuid_1", "kb_uuid_2"],
"search_config": {
"top_k": 5,
"similarity_threshold": 0.5
},
// 注意:此接口目前主要返回 Search App 的详情配置,
// 前端通常使用 search_config 或其他信息来辅助展示引用。
// 具体引用内容的文本通常已包含在 `ask` 接口的 `reference` 字段中。
}
}
```
---
## 2. SearchBot 检索测试 - `retrieval_test_embedded`
**接口描述**: 面向 SearchBot 的**检索效果测试**接口。它不通过 LLM 生成答案,而是直接返回 RAG 检索到的文档片段 (`chunks`)。这用于调试 SearchBot 的检索参数(如相似度阈值、Top-K)是否合理。
**请求方法**: `POST`
**接口地址**: `/api/v1/searchbots/retrieval_test`
**鉴权方式**: Header `Authorization: Bearer <API_KEY>`
### 请求参数 (Request)
#### Body Parameters (JSON)
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| kb_id | string/array | 是 | - | **知识库 ID** (或列表)。支持单个 ID 字符串或 ID 列表。 |
| question | string | 是 | - | **测试查询词**。 |
| page | int | 否 | 1 | **页码**。 |
| size | int | 否 | 30 | **每页数量**。 |
| doc_ids | array<string> | 否 | - | **限定文档 ID**。仅在指定文档中检索。 |
| similarity_threshold | float | 否 | 0.0 | **相似度阈值**。 |
| top_k | int | 否 | 1024 | **Top-K 数量**。 |
| highlight | boolean | 否 | false | **高亮匹配**。是否在返回内容中标记匹配关键词。 |
**Request Example**:
```json
{
"kb_id": ["dataset_uuid_1"],
"question": "refund policy",
"top_k": 5,
"highlight": true
}
```
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": {
"total": 12, // 命中总是
"chunks": [
{
"content_with_weight": "Refunds are processed within <em>7 days</em>...", // 支持高亮
"doc_name": "policy.pdf",
"doc_id": "doc_uuid_101",
"similarity": 0.92,
"img_id": ""
},
{
"content_with_weight": "Product return guidelines...",
"doc_name": "guidelines.docx",
"doc_id": "doc_uuid_102",
"similarity": 0.88
}
],
"labels": [] // 如果启用了查询标签功能
}
}
```
---
## 3. 通用会话问答 - `ask_about`
**接口描述**: **内部/测试用**的通用会话问答接口。与 `ask_embedded` 不同,此接口通常用于 RAGFlow 控制台内部的“调试”或“预览”功能,鉴权依赖用户的登录 Token (User Token),且必须显式指定 `dataset_ids`。它不绑定特定的 Chat/Agent/SearchBot 配置。
**请求方法**: `POST`
**接口地址**: `/api/v1/sessions/ask`
**鉴权方式**: Header `Authorization: Bearer <USER_TOKEN>`
### 请求参数 (Request)
#### Body Parameters (JSON)
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| question | string | 是 | **用户问题**。 |
| dataset_ids | array<string> | 是 | **知识库 ID 列表**。必须是当前用户有权访问的知识库。 |
**Request Example**:
```json
{
"question": "Summary of report",
"dataset_ids": ["dataset_uuid_internal_1"]
}
```
### 响应参数 (Stream Response)
**Content-Type**: `text/event-stream`
```text
data:{"code": 0, "message": "", "data": {"answer": "Here is the summary:", "reference": {}}}
data:{"code": 0, "message": "", "data": {"answer": " The report indicates...", "reference": {}}}
data:{"code": 0, "message": "", "data": true} // 结束
```
---
## 4. 通用相关问题 - `related_questions`
**接口描述**: **内部/测试用**的通用相关问题推荐接口。根据用户的问题和行业背景,利用 LLM 生成推荐问题。通常用于内部测试台。
**请求方法**: `POST`
**接口地址**: `/api/v1/sessions/related_questions`
**鉴权方式**: Header `Authorization: Bearer <USER_TOKEN>`
### 请求参数 (Request)
#### Body Parameters (JSON)
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| question | string | 是 | - | **原始问题/关键词**。 |
| industry | string | 否 | "" | **行业背景** (e.g., "Finance", "Healthcare")。帮助 LLM 生成更专业的推荐。 |
**Request Example**:
```json
{
"question": "Data privacy",
"industry": "IT"
}
```
### 响应参数 (Response)
**Content-Type**: `application/json`
```json
{
"code": 0,
"message": "success",
"data": [
"GDPR compliance checklist",
"Data encryption standards",
"User consent management"
]
}
```
@@ -1,98 +0,0 @@
# RAGFlow External API Reference (Grouped by File)
## File: `api/apps/sdk/agents.py`
| Function Name | URL Pattern | Notes |
|---|---|---|
| `list_agents` | `/api/v1/agents` | List Agents |
| `create_agent` | `/api/v1/agents` | Create Agent |
| `update_agent` | `/api/v1/agents/<agent_id>` | Update Agent |
| `delete_agent` | `/api/v1/agents/<agent_id>` | Delete Agent |
| `webhook` | `/api/v1/webhook_test/<agent_id>` | Webhook Test |
| `webhook_trace` | `/api/v1/webhook_trace/<agent_id>` | Webhook Trace |
## File: `api/apps/sdk/chat.py`
| Function Name | URL Pattern | Notes |
|---|---|---|
| `create` | `/api/v1/chats` | Create Chat |
| `delete_chats` | `/api/v1/chats` | Delete Chat |
| `list_chat` | `/api/v1/chats` | List Chats |
| `update` | `/api/v1/chats/<chat_id>` | Update Chat |
## File: `api/apps/sdk/dataset.py`
| Function Name | URL Pattern | Notes |
|---|---|---|
| `create` | `/api/v1/datasets` | Create Dataset |
| `delete` | `/api/v1/datasets` | Delete Dataset |
| `list_datasets` | `/api/v1/datasets` | List Datasets |
| `update` | `/api/v1/datasets/<dataset_id>` | Update Dataset |
| `knowledge_graph` | `/api/v1/datasets/<dataset_id>/knowledge_graph` | Knowledge Graph |
| `delete_knowledge_graph` | `/api/v1/datasets/<dataset_id>/knowledge_graph` | Delete Knowledge Graph |
| `run_graphrag` | `/api/v1/datasets/<dataset_id>/run_graphrag` | Run GraphRAG |
| `run_raptor` | `/api/v1/datasets/<dataset_id>/run_raptor` | Run Raptor |
| `trace_graphrag` | `/api/v1/datasets/<dataset_id>/trace_graphrag` | Trace GraphRAG |
| `trace_raptor` | `/api/v1/datasets/<dataset_id>/trace_raptor` | Trace Raptor |
## File: `api/apps/sdk/dify_retrieval.py`
| Function Name | URL Pattern | Notes |
|---|---|---|
| `retrieval` | `/api/v1/dify/retrieval` | Dify Retrieval |
## File: `api/apps/sdk/doc.py`
| Function Name | URL Pattern | Notes |
|---|---|---|
| `parse` | `/api/v1/datasets/<dataset_id>/chunks` | Parse Document Chunks |
| `stop_parsing` | `/api/v1/datasets/<dataset_id>/chunks` | Stop Parsing |
| `upload` | `/api/v1/datasets/<dataset_id>/documents` | Upload Document |
| `list_docs` | `/api/v1/datasets/<dataset_id>/documents` | List Documents |
| `delete` | `/api/v1/datasets/<dataset_id>/documents` | Delete Document |
| `update_doc` | `/api/v1/datasets/<dataset_id>/documents/<document_id>` | Update Document |
| `download` | `/api/v1/datasets/<dataset_id>/documents/<document_id>` | Download Document |
| `list_chunks` | `/api/v1/datasets/<dataset_id>/documents/<document_id>/chunks` | List Chunks |
| `add_chunk` | `/api/v1/datasets/<dataset_id>/documents/<document_id>/chunks` | Add Chunk |
| `update_chunk` | `/api/v1/datasets/<dataset_id>/documents/<document_id>/chunks/<chunk_id>` | Update Chunk |
| `rm_chunk` | `/api/v1/datasets/<dataset_id>/documents/<document_id>/chunks` | Remove Chunk |
| `metadata_summary` | `/api/v1/datasets/<dataset_id>/metadata/summary` | Metadata Summary |
| `metadata_batch_update` | `/api/v1/datasets/<dataset_id>/metadata/update` | Batch Update Metadata |
| `retrieval_test` | `/api/v1/retrieval` | Retrieval Test |
## File: `api/apps/sdk/files.py`
| Function Name | URL Pattern | Notes |
|---|---|---|
| `get_all_parent_folders` | `/api/v1/file/all_parent_folder` | Get All Parent Folders |
| `convert` | `/api/v1/file/convert` | File Convert |
| `create` | `/api/v1/file/create` | File Create |
| `download_attachment` | `/api/v1/file/download/<attachment_id>` | Download Attachment |
| `get` | `/api/v1/file/get/<file_id>` | Get File |
| `list_files` | `/api/v1/file/list` | List Files |
| `move` | `/api/v1/file/mv` | Move File |
| `get_parent_folder` | `/api/v1/file/parent_folder` | Get Parent Folder |
| `rename` | `/api/v1/file/rename` | Rename File |
| `rm` | `/api/v1/file/rm` | Remove File |
| `get_root_folder` | `/api/v1/file/root_folder` | Get Root Folder |
| `upload` | `/api/v1/file/upload` | Upload File |
## File: `api/apps/sdk/session.py`
| Function Name | URL Pattern | Notes |
|---|---|---|
| `agent_bot_completions` | `/api/v1/agentbots/<agent_id>/completions` | Agent Bot completion |
| `begin_inputs` | `/api/v1/agentbots/<agent_id>/inputs` | Get Agent Bot inputs |
| `agent_completions` | `/api/v1/agents/<agent_id>/completions` | Agent completion |
| `create_agent_session` | `/api/v1/agents/<agent_id>/sessions` | Create Agent Session |
| `list_agent_session` | `/api/v1/agents/<agent_id>/sessions` | List Agent Sessions |
| `delete_agent_session` | `/api/v1/agents/<agent_id>/sessions` | Delete Agent Session |
| `agents_completion_openai_compatibility` | `/api/v1/agents_openai/<agent_id>/chat/completions` | OpenAI compatible Agent completion |
| `chatbot_completions` | `/api/v1/chatbots/<dialog_id>/completions` | Chatbot completion |
| `chatbots_inputs` | `/api/v1/chatbots/<dialog_id>/info` | Chatbot info |
| `chat_completion` | `/api/v1/chats/<chat_id>/completions` | Chat completion |
| `create` | `/api/v1/chats/<chat_id>/sessions` | Create Chat Session |
| `list_session` | `/api/v1/chats/<chat_id>/sessions` | List Chat Sessions |
| `delete` | `/api/v1/chats/<chat_id>/sessions` | Delete Chat Session |
| `update` | `/api/v1/chats/<chat_id>/sessions/<session_id>` | Update Chat Session |
| `chat_completion_openai_like` | `/api/v1/chats_openai/<chat_id>/chat/completions` | OpenAI compatible Chat completion |
| `ask_about_embedded` | `/api/v1/searchbots/ask` | Searchbot Ask |
| `detail_share_embedded` | `/api/v1/searchbots/detail` | Searchbot Detail |
| `mindmap` | `/api/v1/searchbots/mindmap` | Searchbot Mindmap |
| `related_questions_embedded` | `/api/v1/searchbots/related_questions` | Searchbot Related Questions |
| `retrieval_test_embedded` | `/api/v1/searchbots/retrieval_test` | Searchbot Retrieval Test |
| `ask_about` | `/api/v1/sessions/ask` | Session Ask |
| `related_questions` | `/api/v1/sessions/related_questions` | Session Related Questions |
@@ -1,45 +0,0 @@
# RAGFlow API 接口文档索引 (Unofficial Detailed Guide)
本文档汇集了 RAGFlow 核心模块的 API 详解。所有文档均遵循 **Zero Omissions (无省略)** 原则,全字段展开并包含中文注释。
## 📚 1. 知识库与文档管理 (Knowledge & Documents)
核心的数据管理模块,负责上传文件、解析文档与建立索引。
- **[知识库管理 (Dataset)](./RAGFlow_Dataset接口详解.md)**
- 涵盖知识库的创建、列表查询、更新、删除等接口。
- **[文档处理 (Document)](./RAGFlow_Document接口详解.md)**
- 涵盖文档的上传 (Upload)、解析配置更新 (Update)、解析状态查询 (Run Status)。
- **切片管理**: 解析后的 Chunk 列表查询、增删改查。
- **检索测试**: 直接对知识库进行召回测试 (Retrieval Test)。
- **[文件管理 (File)](./RAGFlow_File接口详解.md)**
- 类似网盘的文件操作体系。
- **CRUD**: 上传、下载、列表。
- **目录**: 文件夹创建、面包屑导航 (`get_all_parent_folders`)。
- **操作**: 移动、重命名、删除、导入知识库 (`convert`).
## 💬 2. 聊天助手 (Chat Assistant)
RAGFlow 原生的对话助手体系,基于 Assistant (Dialog) 模型。
- **[会话管理 (Chat Session)](./RAGFlow_Chat_Session接口详解.md)**
- 管理 `/chats/` 下的会话生命周期。
- 创建会话、获取历史记录、重命名、批量删除。
- **[对话交互 (Chat Completion)](./RAGFlow_Chat_Completion接口详解.md)**
- **Core Chat**: 原生流式对话 (`/chats/<id>/completions`), 支持引用 (`quote`)。
- **OpenAI Compatible**: 完美兼容 OpenAI `/v1/chat/completions` 协议。
- **Embedded Bot**: 面向 C 端嵌入窗口的对话接口 (`/chatbots/`).
## 🤖 3. Agent 与 机器人 (Agent & Bots)
基于 Graph (DAG) 编排的复杂应用与各类机器人扩展。
- **[Agent 与 Dify 兼容 (Agent & Dify)](./RAGFlow_Agent_Dify接口详解.md)**
- **Agent Session**: Agent 的会话管理与流式对话 (`agent_completions`)。
- **Dify Adapter**: 兼容 Dify 协议的检索接口 (`retrieval`).
- **[SearchBot 与 AgentBot](./RAGFlow_SearchBot_AgentBot接口详解.md)**
- **SearchBot**: 纯搜索机器人,支持思维导图 (`mindmap`)、相关问题 (`related_questions`).
- **AgentBot**: 嵌入式 Agent,支持前置表单 (`begin_inputs`).
- **Agent OpenAI**: Agent 的 OpenAI 兼容接口。
## 🛠️ 4. 其他 (Extras)
- **[通用与补充接口 (Session Extras)](./RAGFlow_Session_Extra接口详解.md)**
- **引用详情**: 获取 SearchBot 引用来源 (`detail_share_embedded`).
- **通用问答**: 内部调试用的直接问答 (`ask_about`).
@@ -7,6 +7,7 @@ import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import jakarta.validation.constraints.*;
/**
@@ -129,6 +130,7 @@ public class DatasetDTO {
@JsonProperty("parser_config")
private ParserConfig parserConfig;
@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(description = "PageRank 权重 (0-100)", example = "50")
private Integer pagerank;
}
@@ -67,4 +67,13 @@ public interface LLMService {
* @return 是否可用
*/
boolean isAvailable(String modelId);
/**
* 生成会话标题
*
* @param conversation 对话内容
* @param modelId 模型ID
* @return 标题(约15字)
*/
String generateTitle(String conversation, String modelId);
}
@@ -37,6 +37,8 @@ public class OpenAIStyleLLMServiceImpl implements LLMService {
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}";
private static final String DEFAULT_TITLE_PROMPT = "请根据以下对话内容,生成一个简洁的会话标题(约15字以内),只返回标题,不要包含任何解释或标点符号:\n{conversation}";
@Override
public String generateSummary(String conversation) {
return generateSummary(conversation, null, null);
@@ -302,4 +304,91 @@ public class OpenAIStyleLLMServiceImpl implements LLMService {
return null;
}
}
@Override
public String generateTitle(String conversation, String modelId) {
if (!isAvailable()) {
log.warn("LLM服务不可用,无法生成标题");
return null;
}
try {
ModelConfigEntity llmConfig;
if (modelId != null && !modelId.trim().isEmpty()) {
llmConfig = modelConfigService.getModelByIdFromCache(modelId);
} else {
llmConfig = getDefaultLLMConfig();
}
if (llmConfig == null || llmConfig.getConfigJson() == null) {
log.error("未找到可用的LLM模型配置,modelId: {}", modelId);
return null;
}
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 null;
}
String prompt = DEFAULT_TITLE_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", 0.3);
requestBody.put("max_tokens", 50);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Authorization", "Bearer " + apiKey);
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
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");
String title = messageObj.getStr("content");
if (StringUtils.isNotBlank(title)) {
title = title.trim().replaceAll("[,。!?、:;''\"\"【】()]", "");
if (title.length() > 15) {
title = title.substring(0, 15);
}
return title;
}
}
} else {
log.error("LLM API调用失败,状态码:{},响应:{}", response.getStatusCode(), response.getBody());
}
} catch (Exception e) {
log.error("调用LLM服务生成标题时发生异常,modelId: {}", modelId, e);
}
return null;
}
}
@@ -90,6 +90,7 @@ public class ShiroConfig {
filterMap.put("/agent/chat-history/report", "server");
filterMap.put("/agent/chat-history/download/**", "anon");
filterMap.put("/agent/chat-summary/**", "server");
filterMap.put("/agent/chat-title/**", "server");
filterMap.put("/agent/play/**", "anon");
filterMap.put("/voiceClone/play/**", "anon");
filterMap.put("/**", "oauth2");
@@ -0,0 +1,16 @@
-- 智能体表添加小模型ID字段
SET @col_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'ai_agent' AND COLUMN_NAME = 'slm_model_id');
SET @sql = IF(@col_exists = 0, 'ALTER TABLE `ai_agent` ADD COLUMN `slm_model_id` VARCHAR(255) NULL COMMENT ''小模型ID'' AFTER `llm_model_id`', 'SELECT ''Column slm_model_id already exists'' AS msg');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 创建聊天标题表
DROP TABLE IF EXISTS `ai_agent_chat_title`;
CREATE TABLE `ai_agent_chat_title` (
`id` VARCHAR(32) NOT NULL COMMENT '主键ID',
`session_id` VARCHAR(255) NOT NULL COMMENT '会话ID',
`title` VARCHAR(255) DEFAULT NULL COMMENT '聊天标题',
`created_at` DATETIME DEFAULT NULL COMMENT '创建时间',
`updated_at` DATETIME DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_session_id` (`session_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='智能体聊天标题表';
@@ -0,0 +1,17 @@
-- 更新模型名称:qwen2.5-vl-3b-instruct 改为 qwen3.5-flash
UPDATE `ai_model_config`
SET `config_json` = JSON_SET(`config_json`, '$.model_name', 'qwen3.5-flash')
WHERE `id` = 'VLLM_QwenVLVLLM'
AND JSON_EXTRACT(`config_json`, '$.model_name') = 'qwen2.5-vl-3b-instruct';
-- 更新模型名称:qwen-turbo 改为 qwen-flash
UPDATE `ai_model_config`
SET `config_json` = JSON_SET(`config_json`, '$.model_name', 'qwen-flash')
WHERE `id` = 'LLM_AliLLM'
AND JSON_EXTRACT(`config_json`, '$.model_name') = 'qwen-turbo';
-- 更新备注:qwen-turbo 改为 qwen-flash
UPDATE `ai_model_config`
SET `remark` = REPLACE(`remark`, 'qwen-turbo', 'qwen-flash')
WHERE `id` = 'LLM_AliLLM'
AND `remark` LIKE '%qwen-turbo%';
@@ -599,3 +599,17 @@ databaseChangeLog:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202604011035.sql
- changeSet:
id: 202604011545
author: rainv123
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202604011545.sql
- changeSet:
id: 202604161357
author: hrz
changes:
- sqlFile:
encoding: utf8
path: classpath:db/changelog/202604161357.sql
@@ -14,6 +14,8 @@
<result column="asrModelId" property="asrModelId"/>
<result column="vadModelId" property="vadModelId"/>
<result column="llmModelId" property="llmModelId"/>
<result column="slmModelId" property="slmModelId"/>
<result column="vllmModelId" property="vllmModelId"/>
<result column="ttsModelId" property="ttsModelId"/>
<result column="ttsVoiceId" property="ttsVoiceId"/>
<result column="ttsLanguage" property="ttsLanguage"/>
@@ -46,6 +48,7 @@
a.asr_model_id AS asrModelId,
a.vad_model_id AS vadModelId,
a.llm_model_id AS llmModelId,
a.slm_model_id AS slmModelId,
a.vllm_model_id AS vllmModelId,
a.tts_model_id AS ttsModelId,
a.tts_voice_id AS ttsVoiceId,
@@ -28,6 +28,7 @@ export interface AgentDetail {
asrModelId: string
vadModelId: string
llmModelId: string
slmModelId: string
vllmModelId: string
ttsModelId: string
ttsVoiceId: string
@@ -34,6 +34,9 @@ export function getChatHistory(agentId: string, sessionId: string) {
ignoreAuth: false,
toast: false,
},
cacheFor: {
expire: -1,
},
})
}
@@ -3,6 +3,7 @@ export interface ChatSession {
sessionId: string
createdAt: string
chatCount: number
title: string
}
// 聊天会话列表响应
@@ -14,7 +15,7 @@ export interface ChatSessionsResponse {
// 聊天消息
export interface ChatMessage {
createdAt: string
chatType: 1 | 2 // 1是用户,2是AI
chatType: 1 | 2 | 3 // 1是用户,2是AI3是参数说明
content: string
audioId: string | null
macAddress: string
+2 -1
View File
@@ -108,7 +108,8 @@ export default {
'agent.modelConfig': 'Modellkonfiguration',
'agent.vad': 'Sprachaktivitätserkennung',
'agent.asr': 'Spracherkennung',
'agent.llm': 'Großes Sprachmodell',
'agent.llm': 'Hauptsprachenmodell',
'agent.slm': 'Kleine Parametermodelle',
'agent.vllm': 'Vision-Sprachmodell',
'agent.intent': 'Absichtserkennung',
'agent.memory': 'Speicher',
+2 -1
View File
@@ -108,7 +108,8 @@ export default {
'agent.modelConfig': 'Model Configuration',
'agent.vad': 'Voice Activity Detection',
'agent.asr': 'Speech Recognition',
'agent.llm': 'Large Language Model',
'agent.llm': 'Main language model',
'agent.slm': 'Small parameter model',
'agent.vllm': 'Vision Language Model',
'agent.intent': 'Intent Recognition',
'agent.memory': 'Memory',
+2 -1
View File
@@ -108,7 +108,8 @@ export default {
'agent.modelConfig': 'Configuração do Modelo',
'agent.vad': 'Detecção de Atividade de Voz',
'agent.asr': 'Reconhecimento de Fala',
'agent.llm': 'Modelo de Linguagem Grande',
'agent.llm': 'Modelo de Linguagem Principal',
'agent.slm': 'Modelo de pequenos parâmetros',
'agent.vllm': 'Modelo de Linguagem Visual',
'agent.intent': 'Reconhecimento de Intenção',
'agent.memory': 'Memória',
+2 -1
View File
@@ -108,7 +108,8 @@ export default {
'agent.modelConfig': 'Cấu hình mô hình',
'agent.vad': 'Phát hiện hoạt động giọng nói',
'agent.asr': 'Nhận dạng giọng nói',
'agent.llm': 'Mô hình ngôn ngữ lớn',
'agent.llm': 'Mô hình ngôn ngữ chính',
'agent.slm': 'Mô hình tham số nhỏ',
'agent.vllm': 'Mô hình ngôn ngữ thị giác',
'agent.intent': 'Nhận dạng ý định',
'agent.memory': 'Bộ nhớ',
+2 -1
View File
@@ -108,7 +108,8 @@ export default {
'agent.modelConfig': '模型配置',
'agent.vad': '语音活动检测',
'agent.asr': '语音识别',
'agent.llm': '语言模型',
'agent.llm': '语言模型',
'agent.slm': '小参数模型',
'agent.vllm': '视觉大模型',
'agent.intent': '意图识别',
'agent.memory': '记忆',
+2 -1
View File
@@ -129,7 +129,8 @@ export default {
'agent.modelConfig': '模型配置',
'agent.vad': '語音活動檢測',
'agent.asr': '語音識別',
'agent.llm': '語言模型',
'agent.llm': '語言模型',
'agent.slm': '小參數模型',
'agent.vllm': '視覺大模型',
'agent.intent': '意圖識別',
'agent.memory': '記憶',
+27 -2
View File
@@ -29,6 +29,7 @@ const formData = ref<Partial<AgentDetail>>({
vadModelId: '',
asrModelId: '',
llmModelId: '',
slmModelId: '',
vllmModelId: '',
intentModelId: '',
memModelId: '',
@@ -46,6 +47,7 @@ const displayNames = ref({
vad: t('agent.pleaseSelect'),
asr: t('agent.pleaseSelect'),
llm: t('agent.pleaseSelect'),
slm: t('agent.pleaseSelect'),
vllm: t('agent.pleaseSelect'),
intent: t('agent.pleaseSelect'),
memory: t('agent.pleaseSelect'),
@@ -94,6 +96,7 @@ const pickerShow = ref<{
vad: false,
asr: false,
llm: false,
slm: false,
vllm: false,
intent: false,
memory: false,
@@ -272,6 +275,7 @@ function updateDisplayNames() {
displayNames.value.vad = getModelDisplayName('VAD', formData.value.vadModelId)
displayNames.value.asr = getModelDisplayName('ASR', formData.value.asrModelId)
displayNames.value.llm = getModelDisplayName('LLM', formData.value.llmModelId)
displayNames.value.slm = getModelDisplayName('LLM', formData.value.slmModelId)
displayNames.value.vllm = getModelDisplayName('VLLM', formData.value.vllmModelId)
displayNames.value.intent = getModelDisplayName('Intent', formData.value.intentModelId)
displayNames.value.memory = getModelDisplayName('Memory', formData.value.memModelId)
@@ -279,7 +283,6 @@ function updateDisplayNames() {
// 角色音色特殊处理
displayNames.value.report = reportOptions.find(item => item.value === formData.value.chatHistoryConf)?.name
displayNames.value.language = formData.value.ttsLanguage
isVisibleReport.value = formData.value.memModelId !== 'Memory_nomem'
@@ -437,6 +440,7 @@ function selectRoleTemplate(templateId: string) {
vadModelId: template.vadModelId || formData.value.vadModelId,
asrModelId: template.asrModelId || formData.value.asrModelId,
llmModelId: template.llmModelId || formData.value.llmModelId,
slmModelId: template.llmModelId || formData.value.slmModelId,
vllmModelId: template.vllmModelId || formData.value.vllmModelId,
intentModelId: template.intentModelId || formData.value.intentModelId,
memModelId: template.memModelId || formData.value.memModelId,
@@ -474,6 +478,9 @@ async function onPickerConfirm(type: string, value: any, name: string) {
case 'llm':
formData.value.llmModelId = value
break
case 'slm':
formData.value.slmModelId = value
break
case 'vllm':
formData.value.vllmModelId = value
break
@@ -490,7 +497,8 @@ async function onPickerConfirm(type: string, value: any, name: string) {
if (value === 'Memory_nomem' || value === 'Memory_mem_report_only') {
tempSummaryMemory.value = formData.value.summaryMemory
formData.value.summaryMemory = ''
} else if (tempSummaryMemory.value !== '' && formData.value.summaryMemory === '') {
}
else if (tempSummaryMemory.value !== '' && formData.value.summaryMemory === '') {
formData.value.summaryMemory = tempSummaryMemory.value
tempSummaryMemory.value = ''
}
@@ -839,6 +847,16 @@ onMounted(async () => {
<wd-icon name="arrow-right" custom-class="text-[20rpx] text-[#9d9ea3]" />
</view>
<view class="flex cursor-pointer items-center justify-between border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] transition-all duration-300 active:bg-[#eef3ff]" @click="openPicker('slm')">
<text class="text-[28rpx] text-[#232338] font-medium">
{{ t('agent.slm') }}
</text>
<text class="mx-[16rpx] flex-1 text-right text-[26rpx] text-[#65686f]">
{{ displayNames.slm }}
</text>
<wd-icon name="arrow-right" custom-class="text-[20rpx] text-[#9d9ea3]" />
</view>
<view class="flex cursor-pointer items-center justify-between border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] transition-all duration-300 active:bg-[#eef3ff]" @click="openPicker('vllm')">
<text class="text-[28rpx] text-[#232338] font-medium">
{{ t('agent.vllm') }}
@@ -995,6 +1013,13 @@ onMounted(async () => {
@select="({ item }) => onPickerConfirm('llm', item.value, item.name)"
/>
<wd-action-sheet
v-model="pickerShow.slm"
:actions="modelOptions.LLM && modelOptions.LLM.map(item => ({ name: item.modelName, value: item.id }))"
@close="onPickerCancel('slm')"
@select="({ item }) => onPickerConfirm('slm', item.value, item.name)"
/>
<wd-action-sheet
v-model="pickerShow.vllm"
:actions="modelOptions.VLLM && modelOptions.VLLM.map(item => ({ name: item.modelName, value: item.id }))"
@@ -61,6 +61,7 @@ const loading = ref(false)
// 音频播放相关
const audioContext = ref<UniApp.InnerAudioContext | null>(null)
const playingAudioId = ref<string | null>(null)
const expandedToolResults = ref({})
// 返回上一页
function goBack() {
@@ -124,8 +125,50 @@ function getSpeakerName(message: ChatMessage): string {
// 格式化时间
function formatTime(timeStr: string) {
const date = new Date(timeStr)
return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
if (!timeStr)
return t('chatHistory.unknownTime')
// 处理时间字符串,确保格式正确
const date = new Date(timeStr.replace(' ', 'T')) // 转换为ISO格式
const now = new Date()
// 检查日期是否有效
if (Number.isNaN(date.getTime())) {
return timeStr // 如果解析失败,直接返回原字符串
}
const diff = now.getTime() - date.getTime()
// 小于1分钟
if (diff < 60000)
return t('chatHistory.justNow')
// 小于1小时
if (diff < 3600000)
return t('chatHistory.minutesAgo', { minutes: Math.floor(diff / 60000) })
// 小于1天(24小时)
if (diff < 86400000)
return t('chatHistory.hoursAgo', { hours: Math.floor(diff / 3600000) })
// 小于7天
if (diff < 604800000) {
const days = Math.floor(diff / 86400000)
return t('chatHistory.daysAgo', { days })
}
// 超过7天,显示具体日期
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const currentYear = now.getFullYear()
// 如果是当前年份,不显示年份
if (year === currentYear) {
return `${month}-${day}`
}
return `${year}-${month}-${day}`
}
// 播放音频
@@ -192,11 +235,56 @@ const playAudio = debounce(async (audioId: string) => {
}
}, 400)
function extractContentFromString(content: string) {
if (!content || content.trim() === '') {
return content
}
// 尝试解析为 JSON
try {
const jsonObj = JSON.parse(content)
// 如果是数组格式(包含 text 和 tool)
if (Array.isArray(jsonObj)) {
return jsonObj
}
// 如果是对象且有 content 字段
if (jsonObj && typeof jsonObj === 'object' && jsonObj.content) {
return jsonObj.content
}
}
catch (e) {
// 如果不是有效的 JSON,直接返回原内容
}
// 如果不是 JSON 格式或没有 content 字段,直接返回原内容
return content
}
function toggleToolResult(messageIndex, itemIndex) {
const key = `${messageIndex}-${itemIndex}`
expandedToolResults.value[key] = !expandedToolResults.value[key]
}
function isToolResultCollapsed(messageIndex, itemIndex) {
const key = `${messageIndex}-${itemIndex}`
// 默认折叠(true表示折叠)
return !expandedToolResults.value[key]
}
function getFirstLineText(text: string) {
if (!text) {
return ''
}
const firstLine = text.split('\n')[0]
return firstLine.length < text.length ? `${firstLine}...` : text
}
onLoad((options) => {
if (options?.sessionId && options?.agentId) {
sessionId.value = options.sessionId
agentId.value = options.agentId
loadChatHistory()
}
else {
console.error('缺少必要参数')
@@ -204,6 +292,10 @@ onLoad((options) => {
}
})
onShow(() => {
loadChatHistory()
})
// 页面销毁时清理音频资源
onUnload(() => {
if (audioContext.value) {
@@ -256,6 +348,7 @@ onUnload(() => {
:class="{
'items-end': message.chatType === 1,
'items-start': message.chatType === 2,
'tool-message': message.chatType === 3,
}"
>
<!-- 消息气泡 -->
@@ -263,11 +356,39 @@ onUnload(() => {
class="shadow-message break-words rounded-[20rpx] p-[24rpx] leading-[1.4]"
:class="{
'bg-[#336cff] text-white': message.chatType === 1,
'bg-white text-[#232338] border border-[#eeeeee]': message.chatType === 2,
'bg-white text-[#232338] border border-[#eeeeee]': [2, 3].includes(message.chatType),
}"
>
<template v-if="Array.isArray(extractContentFromString(message.content))">
<div class="content-wrapper">
<div v-for="(item, idx) in extractContentFromString(message.content)" :key="idx">
<div v-if="item.type === 'text'" class="text-content">
{{ item.text }}
</div>
<div v-else-if="item.type === 'tool'" class="tool-call-text">
{{ item.text }}
</div>
<div v-else-if="item.type === 'tool_result'" class="tool-call-text">
<div v-if="item.text && item.text.length > 80" class="tool-result-wrapper">
<div v-if="isToolResultCollapsed(index, idx)" class="tool-result-collapsed">
{{ getFirstLineText(item.text) }}
</div>
<div v-else class="tool-result-expanded">
{{ item.text }}
</div>
<span class="tool-toggle-btn" @click="toggleToolResult(index, idx)">
<wd-icon :name="isToolResultCollapsed(index, idx) ? 'arrow-down' : 'arrow-up'" size="12" />
</span>
</div>
<div v-else>
{{ item.text }}
</div>
</div>
</div>
</div>
</template>
<!-- 内容区域 - 使用flex布局让图标和文本对齐 -->
<view class="flex items-center gap-[12rpx]">
<view v-else class="flex items-center gap-[12rpx]">
<!-- 音频播放图标 -->
<view
v-if="message.audioId"
@@ -334,4 +455,46 @@ onUnload(() => {
.animate-pulse-audio {
animation: pulse-audio 1.5s infinite;
}
.text-content {
display: block;
margin-bottom: 8rpx;
}
.tool-call-text {
color: #1890ff;
font-family: 'Courier New', monospace;
font-weight: 500;
font-size: 24rpx;
display: block;
margin-top: 8rpx;
}
.user-message .tool-call-text {
color: #e6f7ff;
}
.tool-message .message-content {
background-color: #f0f0f0;
}
.tool-result-wrapper {
position: relative;
padding-right: 40rpx;
}
.tool-result-collapsed {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tool-toggle-btn {
position: absolute;
right: 0;
top: 0;
cursor: pointer;
color: #1890ff;
font-size: 24rpx;
}
</style>
@@ -3,20 +3,21 @@ import type { ChatSession } from '@/api/chat-history/types'
import { computed, onMounted, ref } from 'vue'
import { getChatSessions } from '@/api/chat-history/chat-history'
import { t } from '@/i18n'
import { deepClone } from '@/utils'
defineOptions({
name: 'ChatHistory',
})
const props = withDefaults(defineProps<Props>(), {
agentId: 'default',
})
// 接收props
interface Props {
agentId?: string
}
const props = withDefaults(defineProps<Props>(), {
agentId: 'default'
})
// 获取屏幕边界到安全区域距离
let safeAreaInsets: any
let systemInfo: any
@@ -43,7 +44,7 @@ const sessionList = ref<ChatSession[]>([])
const loading = ref(false)
const loadingMore = ref(false)
const hasMore = ref(true)
const currentPage = ref(1)
const currentPage = ref(0)
const pageSize = 10
// 使用传入的智能体ID
@@ -52,10 +53,8 @@ const currentAgentId = computed(() => {
})
// 加载聊天会话列表
async function loadChatSessions(page = 1, isRefresh = false) {
async function loadChatSessions(page = 1, isUpdate = false) {
try {
console.log(t('chatHistory.getChatSessions'), { page, isRefresh })
// 检查是否有当前选中的智能体
if (!currentAgentId.value) {
console.warn(t('chatHistory.noSelectedAgent'))
@@ -76,7 +75,10 @@ async function loadChatSessions(page = 1, isRefresh = false) {
})
if (page === 1) {
sessionList.value = response.list || []
const oldSessionList = deepClone(sessionList.value)
oldSessionList.splice(0, 10)
oldSessionList.unshift(...(response.list || []))
sessionList.value = isUpdate ? oldSessionList : response.list || []
}
else {
sessionList.value.push(...(response.list || []))
@@ -170,10 +172,15 @@ function goToChatDetail(session: ChatSession) {
onMounted(async () => {
// 智能体已简化为默认
loadChatSessions(1)
})
onShow(() => {
if (currentPage.value !== 0) {
loadChatSessions(1, true)
}
})
// 暴露方法给父组件
defineExpose({
refresh,
@@ -187,8 +194,8 @@ defineExpose({
<view v-if="loading && sessionList.length === 0" class="loading-container">
<wd-loading color="#336cff" />
<text class="loading-text">
{{ t('chatHistory.loading') }}
</text>
{{ t('chatHistory.loading') }}
</text>
</view>
<!-- 会话列表 -->
@@ -205,7 +212,7 @@ defineExpose({
<view class="session-info">
<view class="session-header">
<text class="session-title">
{{ t('chatHistory.conversationRecord') }} {{ session.sessionId.substring(0, 8) }}...
{{ session.title || `${t('chatHistory.conversationRecord')} ${session.sessionId.substring(0, 8)}...` }}
</text>
<text class="session-time">
{{ formatTime(session.createdAt) }}
@@ -242,11 +249,11 @@ defineExpose({
<view v-else-if="!loading" class="empty-state">
<wd-icon name="chat" custom-class="empty-icon" />
<text class="empty-text">
{{ t('chatHistory.noChatRecords') }}
</text>
<text class="empty-desc">
{{ t('chatHistory.chatRecordsDescription') }}
</text>
{{ t('chatHistory.noChatRecords') }}
</text>
<text class="empty-desc">
{{ t('chatHistory.chatRecordsDescription') }}
</text>
</view>
</view>
</template>
@@ -333,7 +340,7 @@ defineExpose({
padding: 32rpx;
.session-info {
flex: 1;
width: 94%;
.session-header {
display: flex;
@@ -345,8 +352,11 @@ defineExpose({
font-size: 32rpx;
font-weight: 600;
color: #232338;
max-width: 70%;
width: 70%;
word-break: break-all;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.session-time {
+33
View File
@@ -300,3 +300,36 @@ export function debounce<T extends AnyFunction>(
return debounced
}
type DeepCloneTarget = string | number | boolean | null | undefined | object
/**
*
* @param target
* @returns
*/
export function deepClone<T extends DeepCloneTarget>(target: T): T {
if (target === null || typeof target !== 'object') {
return target
}
if (target instanceof Date) {
return new Date(target.getTime()) as any
}
if (Array.isArray(target)) {
return target.map(item => deepClone(item)) as any
}
if (target instanceof Object) {
const clonedObj = {} as T
for (const key in target) {
if (Object.prototype.hasOwnProperty.call(target, key)) {
(clonedObj as any)[key] = deepClone((target as any)[key])
}
}
return clonedObj
}
return target
}
@@ -8,7 +8,7 @@
:class="{ active: currentSessionId === session.sessionId }" @click="selectSession(session)">
<img :src="getUserAvatar(session.sessionId)" class="avatar" />
<div class="session-info">
<div class="session-time">{{ formatTime(session.createdAt) }}</div>
<div class="session-time">{{ session.title || formatTime(session.createdAt) }}</div>
<div class="message-count">{{ session.chatCount > 99 ? '99' : session.chatCount }}</div>
</div>
</div>
@@ -139,7 +139,7 @@ export default {
if (this.messages[0]) {
result.push({
type: 'time',
content: this.formatTime(this.messages[0].createdAt),
content: this.formatTime(this.messages[this.messages.length - 1].createdAt),
id: `time-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
});
}
+18 -3
View File
@@ -10,7 +10,7 @@
<img src="@/assets/home/delete.png" alt="" style="width: 18px;height: 18px;margin-right: 10px;"
@click.stop="handleDelete" />
<el-tooltip class="item" effect="light" :content="device.systemPrompt" placement="top"
popper-class="custom-tooltip">
popper-class="device-item-tooltip">
<img src="@/assets/home/info.png" alt="" style="width: 18px;height: 18px;" />
</el-tooltip>
</div>
@@ -193,8 +193,23 @@ export default {
</style>
<style>
.custom-tooltip {
max-width: 400px;
.device-item-tooltip {
max-height: 60vh !important;
max-width: 400px !important;
overflow-y: auto !important;
scrollbar-width: thin;
word-break: break-word;
}
.device-item-tooltip .popper__arrow {
display: none !important;
}
.device-item-tooltip[x-placement^="top"] .popper__arrow {
border-top-color: transparent !important;
}
.device-item-tooltip[x-placement^="bottom"] .popper__arrow {
border-bottom-color: transparent !important;
}
</style>
@@ -477,13 +477,15 @@ export default {
.function-column {
position: relative;
display: flex;
flex-direction: column;
width: auto;
height:700px;
height: 100%;
padding: 10px;
overflow-y: auto;
border-right: 1px solid #EBEEF5;
scrollbar-width: none;
overflow-x: hidden;
box-sizing: border-box;
}
.mcp-access-point {
@@ -497,6 +499,9 @@ export default {
}
.function-list {
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: thin;
display: flex;
flex-direction: column;
gap: 8px;
+21 -1
View File
@@ -757,7 +757,8 @@ export default {
'roleConfig.interactionLanguage': 'Interaktionssprache',
'roleConfig.vad': 'VAD',
'roleConfig.asr': 'ASR',
'roleConfig.llm': 'LLM',
'roleConfig.llm': 'Hauptsprachmodell (LLM)',
'roleConfig.slm': 'Kleines Sprachmodell (SLM)',
'roleConfig.vllm': 'VLLM',
'roleConfig.tts': 'TTS',
'roleConfig.memoryHis': 'Speicher',
@@ -802,6 +803,25 @@ export default {
'roleConfig.cannotPlayAudio': 'Audio kann nicht abgespielt werden',
'roleConfig.audioPlayError': 'Fehler bei der Audio-Wiedergabe',
// Tooltip-Beschreibungen für Formularfelder
'roleConfig.tooltip.agentName': 'Legen Sie den Namen Ihres KI-Agenten fest, um ihn zu identifizieren und wiederzuerkennen',
'roleConfig.tooltip.roleTemplate': 'Wählen Sie aus voreingestellten Rollenvorlagen, um die Grundeinstellungen Ihres Agenten schnell zu konfigurieren',
'roleConfig.tooltip.contextProvider': 'Wenn XiaoZhi aktiviert wird, werden externe Systemdaten abgerufen und dynamisch in die Systemaufforderung des LLM eingefügt',
'roleConfig.tooltip.roleIntroduction': 'Definieren Sie die Rollenpositionierung, Persönlichkeitsmerkmale, Verhaltensnormen und beruflichen Wissenshintergründe des KI-Agenten',
'roleConfig.tooltip.memoryHis': 'Zusammenfassung des Chatverlaufs',
'roleConfig.tooltip.languageCode': 'Legen Sie Sprachcodes fest, z.B. de-DE, en-US usw., die für die Erkennung bestimmter Funktionen verwendet werden',
'roleConfig.tooltip.interactionLanguage': 'Legen Sie die Interaktionssprache fest und geben Sie die primäre Sprache an, die der KI-Agent für die Kommunikation verwendet',
'roleConfig.tooltip.vad': 'Sprachaktivitätserkennung: Erkennt, wann der Benutzer zu sprechen beginnt oder aufhört, wird verwendet, um Gesprächsbeginn und -ende zu bestimmen und ermöglicht die Unterbrechungsfunktion',
'roleConfig.tooltip.asr': 'Automatische Spracherkennung: Wandelt Benutzersprache in Text um, der erste Schritt im Mensch-Computer-Dialog, unterstützt mehrsprachige Erkennung',
'roleConfig.tooltip.llm': 'Hauptsprachmodell (Large Language Model): Das "Gehirn" des KI-Agenten, verantwortlich für das Verstehen von Benutzerabsichten, das Generieren von Antworten und das Ausführen verschiedener Aufgaben',
'roleConfig.tooltip.slm': 'Kleines Parameter-Modell (Small Language Model): Wird für die KI-Agenten-Aktivierung verwendet, erstellt Zusammenfassungstitel für Erinnerungen',
'roleConfig.tooltip.vllm': 'Visuelles großes Sprachmodell: Verarbeitet Bild- und Videoverständnis, ermöglicht dem KI-Agenten das Analysieren und Beschreiben von kameragefangenen Inhalten',
'roleConfig.tooltip.intent': 'Absichtserkennung: Analysiert Benutzersprache oder -text, um die wahre Absicht des Benutzers zu bestimmen, wie z.B. Anfragen, Chat, Gerätesteuerung usw.',
'roleConfig.tooltip.memory': 'Gedächtnismodell: Verwaltet Speicherung und Zusammenfassung des Gesprächsverlaufs, bestimmt, ob sich der KI-Agent an frühere Gespräche erinnern kann für Langzeitgedächtnis',
'roleConfig.tooltip.tts': 'Text-zu-Sprache: Konvertiert Text in natürliche Sprache, bestimmt Stimme, Geschwindigkeit und Tonfall des KI-Agenten',
'roleConfig.tooltip.language': 'Wählen Sie die Sprache der Stimme; das System filtert verfügbare Stimmen, die diese Sprache unterstützen',
'roleConfig.tooltip.voiceType': 'Wählen Sie die Stimme für den KI-Agenten; verschiedene Stimmen haben unterschiedliche Eigenschaften und Stile. Einige Stimmen unterstützen die Vorschaufunktion, klicken Sie auf die Wiedergabeschaltfläche für eine Vorschau',
// Function management dialog text
'functionDialog.title': 'Funktionsverwaltung',
'functionDialog.unselectedFunctions': 'Nicht ausgewählte Funktionen',
+21 -1
View File
@@ -757,7 +757,8 @@ export default {
'roleConfig.interactionLanguage': 'Interaction Language',
'roleConfig.vad': 'Voice Detect',
'roleConfig.asr': 'Speech Recognition',
'roleConfig.llm': 'Language Model',
'roleConfig.llm': 'Main Language Model',
'roleConfig.slm': 'Small Language Model',
'roleConfig.vllm': 'Vision Model',
'roleConfig.tts': 'Text-to-Speech',
'roleConfig.memoryHis': 'Memory',
@@ -802,6 +803,25 @@ export default {
'roleConfig.cannotPlayAudio': 'Cannot play audio',
'roleConfig.audioPlayError': 'Error occurred during audio playback',
// Form field Tooltip descriptions
'roleConfig.tooltip.agentName': 'Set the name of your AI agent for identification and recognition',
'roleConfig.tooltip.roleTemplate': 'Choose from preset role templates to quickly configure your agent\'s basic settings',
'roleConfig.tooltip.contextProvider': 'When XiaoZhi is awakened, fetch external system data and dynamically inject it into the LLM system prompt',
'roleConfig.tooltip.roleIntroduction': 'Define the AI agent\'s role positioning, personality traits, behavioral norms, and professional knowledge background',
'roleConfig.tooltip.memoryHis': 'Summarize chat record content',
'roleConfig.tooltip.languageCode': 'Set language code such as zh-CN, en-US, etc., used for specific feature recognition',
'roleConfig.tooltip.interactionLanguage': 'Set the interaction language, specifying the primary language the AI agent uses for communication',
'roleConfig.tooltip.vad': 'Voice Activity Detection: Detects when the user starts or stops speaking, used to determine conversation start and end, enabling interrupt functionality',
'roleConfig.tooltip.asr': 'Automatic Speech Recognition: Converts user speech to text, the first step in human-computer dialogue, supporting multilingual recognition',
'roleConfig.tooltip.llm': 'Main Language Model (Large Language Model): The "brain" of the AI agent, responsible for understanding user intent, generating responses, and executing various tasks',
'roleConfig.tooltip.slm': 'Small Parameter Model (Small Language Model): Used for AI agent wake-up, generating memory summary titles',
'roleConfig.tooltip.vllm': 'Visual Large Language Model: Processes image and video understanding, enabling the AI agent to analyze and describe camera-captured content',
'roleConfig.tooltip.intent': 'Intent Detection: Analyzes user speech or text to determine the user\'s true intent, such as queries, chat, device control, etc.',
'roleConfig.tooltip.memory': 'Memory Model: Manages conversation history storage and summarization, determining whether the AI can remember previous conversations for long-term memory',
'roleConfig.tooltip.tts': 'Text-to-Speech: Converts text to natural speech, determining the AI\'s voice, speed, and tone',
'roleConfig.tooltip.language': 'Select the language of the voice; the system will filter available voices that support that language',
'roleConfig.tooltip.voiceType': 'Choose the voice for the AI agent; different voices have different characteristics and styles. Some voices support preview functionality, click the play button to preview',
// Function management dialog text
'functionDialog.title': 'Function Management',
'functionDialog.unselectedFunctions': 'Unselected Functions',
+21 -1
View File
@@ -757,7 +757,8 @@ export default {
'roleConfig.interactionLanguage': 'Idioma de Interação',
'roleConfig.vad': 'Detecção de Voz',
'roleConfig.asr': 'Reconhecimento de Fala',
'roleConfig.llm': 'Modelo de Linguagem',
'roleConfig.llm': 'Modelo de Linguagem Principal',
'roleConfig.slm': 'Modelo de Linguagem Pequeno',
'roleConfig.vllm': 'Modelo de Visão',
'roleConfig.intent': 'Reconhecimento de Intenção',
'roleConfig.memoryHis': 'Memória',
@@ -802,6 +803,25 @@ export default {
'roleConfig.cannotPlayAudio': 'Não é possível reproduzir áudio',
'roleConfig.audioPlayError': 'Ocorreu um erro durante a reprodução de áudio',
// Descrições de Tooltip para campos de formulário
'roleConfig.tooltip.agentName': 'Defina o nome do seu agente de IA para identificação e reconhecimento',
'roleConfig.tooltip.roleTemplate': 'Escolha entre modelos de função predefinidos para configurar rapidamente as configurações básicas do seu agente',
'roleConfig.tooltip.contextProvider': 'Quando o XiaoZhi é ativado, busca dados de sistemas externos e os injeta dinamicamente no prompt do sistema do LLM',
'roleConfig.tooltip.roleIntroduction': 'Defina o posicionamento do papel, traços de personalidade, normas comportamentais e histórico de conhecimento profissional do agente de IA',
'roleConfig.tooltip.memoryHis': 'Resumir conteúdo do registro de chat',
'roleConfig.tooltip.languageCode': 'Defina o código de idioma como pt-BR, en-US, etc., usado para reconhecimento de recursos específicos',
'roleConfig.tooltip.interactionLanguage': 'Defina o idioma de interação, especificando o idioma principal que o agente de IA usa para comunicação',
'roleConfig.tooltip.vad': 'Detecção de Atividade de Voz: Detecta quando o usuário começa ou para de falar, usado para determinar o início e fim da conversa, permitindo funcionalidade de interrupção',
'roleConfig.tooltip.asr': 'Reconhecimento Automático de Fala: Converte fala do usuário em texto, o primeiro passo no diálogo humano-computador, suporta reconhecimento multilíngue',
'roleConfig.tooltip.llm': 'Modelo de Linguagem Principal (Large Language Model): O "cérebro" do agente de IA, responsável por entender a intenção do usuário, gerar respostas e executar várias tarefas',
'roleConfig.tooltip.slm': 'Modelo de Parâmetros Pequeno (Small Language Model): Usado para ativação do agente de IA, gerando títulos de resumo de memória',
'roleConfig.tooltip.vllm': 'Modelo de Linguagem Visual Grande: Processa compreensão de imagem e vídeo, permitindo que o agente de IA analise e descreva o conteúdo capturado pela câmera',
'roleConfig.tooltip.intent': 'Detecção de Intenção: Analisa fala ou texto do usuário para determinar a verdadeira intenção do usuário, como consultas, bate-papo, controle de dispositivos, etc.',
'roleConfig.tooltip.memory': 'Modelo de Memória: Gerencia armazenamento e resumo do histórico de conversas, determina se a IA pode lembrar conversas anteriores para memória de longo prazo',
'roleConfig.tooltip.tts': 'Texto para Fala: Converte texto em fala natural, determinando a voz, velocidade e tom da IA',
'roleConfig.tooltip.language': 'Selecione o idioma da voz; o sistema filtrará vozes disponíveis que suportam esse idioma',
'roleConfig.tooltip.voiceType': 'Escolha a voz para o agente de IA; diferentes vozes têm diferentes características e estilos. Algumas vozes suportam funcionalidade de visualização, clique no botão de reprodução para visualizar',
// Diálogo de gerenciamento de funções
'functionDialog.title': 'Gerenciamento de Funções',
'functionDialog.unselectedFunctions': 'Funções Não Selecionadas',
+21 -1
View File
@@ -757,7 +757,8 @@ export default {
'roleConfig.interactionLanguage': 'Ngôn ngữ tương tác',
'roleConfig.vad': 'Phát hiện giọng nói',
'roleConfig.asr': 'Nhận dạng giọng nói',
'roleConfig.llm': 'Mô hình ngôn ngữ',
'roleConfig.llm': 'Mô hình ngôn ngữ chính',
'roleConfig.slm': 'Mô hình ngôn ngữ nhỏ',
'roleConfig.vllm': 'Mô hình thị giác',
'roleConfig.tts': 'Văn bản thành giọng nói',
'roleConfig.memoryHis': 'Bộ nhớ',
@@ -802,6 +803,25 @@ export default {
'roleConfig.cannotPlayAudio': 'Không thể phát âm thanh',
'roleConfig.audioPlayError': 'Lỗi trong quá trình phát âm thanh',
// Mô tả Tooltip cho trường biểu mẫu
'roleConfig.tooltip.agentName': 'Đặt tên cho tác nhân AI của bạn để nhận dạng và nhận biết',
'roleConfig.tooltip.roleTemplate': 'Chọn từ các mẫu vai trò được đặt trước để nhanh chóng định cấu hình cài đặt cơ bản cho tác nhân của bạn',
'roleConfig.tooltip.contextProvider': 'Khi XiaoZhi được kích hoạt, lấy dữ liệu từ hệ thống bên ngoài và tiêm động vào lời nhắc hệ thống của LLM',
'roleConfig.tooltip.roleIntroduction': 'Xác định vị trí vai trò, đặc điểm tính cách, quy tắc hành vi và nền tảng kiến thức chuyên môn của tác nhân AI',
'roleConfig.tooltip.memoryHis': 'Tóm tắt nội dung bản ghi trò chuyện',
'roleConfig.tooltip.languageCode': 'Đặt mã ngôn ngữ như vi-VN, en-US, v.v., được sử dụng để nhận dạng các tính năng cụ thể',
'roleConfig.tooltip.interactionLanguage': 'Đặt ngôn ngữ tương tác, chỉ định ngôn ngữ chính mà tác nhân AI sử dụng để giao tiếp',
'roleConfig.tooltip.vad': 'Phát hiện Hoạt động Giọng nói: Phát hiện khi người dùng bắt đầu hoặc ngừng nói, được sử dụng để xác định thời điểm bắt đầu và kết thúc cuộc trò chuyện, cho phép chức năng ngắt',
'roleConfig.tooltip.asr': 'Nhận dạng Giọng nói Tự động: Chuyển đổi giọng nói của người dùng thành văn bản, bước đầu tiên trong hội thoại người-máy, hỗ trợ nhận dạng đa ngôn ngữ',
'roleConfig.tooltip.llm': 'Mô hình ngôn ngữ chính (Large Language Model): "Bộ não" của tác nhân AI, chịu trách nhiệm hiểu ý định của người dùng, tạo phản hồi và thực hiện các nhiệm vụ khác nhau',
'roleConfig.tooltip.slm': 'Mô hình tham số nhỏ (Small Language Model): Được sử dụng để kích hoạt tác nhân AI, tạo tiêu đề tóm tắt ký ức',
'roleConfig.tooltip.vllm': 'Mô hình Ngôn ngữ Lớn Thị giác: Xử lý hiểu hình ảnh và video, cho phép tác nhân AI phân tích và mô tả nội dung được camera ghi lại',
'roleConfig.tooltip.intent': 'Phát hiện Ý định: Phân tích giọng nói hoặc văn bản của người dùng để xác định ý định thực sự của người dùng như truy vấn, trò chuyện, điều khiển thiết bị, v.v.',
'roleConfig.tooltip.memory': 'Mô hình Bộ nhớ: Quản lý lưu trữ và tóm tắt lịch sử cuộc trò chuyện, quyết định liệu AI có thể nhớ các cuộc trò chuyện trước đó để có trí nhớ dài hạn hay không',
'roleConfig.tooltip.tts': 'Chuyển văn bản thành giọng nói: Chuyển đổi văn bản thành giọng nói tự nhiên, xác định giọng nói, tốc độ và ngữ điệu của AI',
'roleConfig.tooltip.language': 'Chọn ngôn ngữ của giọng nói; hệ thống sẽ lọc các giọng nói khả dụng hỗ trợ ngôn ngữ đó',
'roleConfig.tooltip.voiceType': 'Chọn giọng nói cho tác nhân AI; các giọng nói khác nhau có đặc điểm và phong cách khác nhau. Một số giọng nói hỗ trợ chức năng xem trước, nhấp vào nút phát để xem trước',
// Function management dialog text
'functionDialog.title': 'Quản lý chức năng',
'functionDialog.unselectedFunctions': 'Chức năng chưa chọn',
+21 -1
View File
@@ -757,7 +757,8 @@ export default {
'roleConfig.interactionLanguage': '交互语种',
'roleConfig.vad': '语音活动检测(VAD)',
'roleConfig.asr': '语音识别(ASR)',
'roleConfig.llm': '语言模型(LLM)',
'roleConfig.llm': '语言模型(LLM)',
"roleConfig.slm": "小参数模型(SLM)",
'roleConfig.vllm': '视觉大模型(VLLM)',
'roleConfig.intent': '意图识别(Intent)',
'roleConfig.memoryHis': '记忆',
@@ -802,6 +803,25 @@ export default {
'roleConfig.cannotPlayAudio': '无法播放音频',
'roleConfig.audioPlayError': '播放音频过程出错',
// 表单字段 Tooltip 提示说明
'roleConfig.tooltip.agentName': '设置智能体的名称,用于标识和识别您的AI助手',
'roleConfig.tooltip.roleTemplate': '从预设的角色模板中选择,快速配置智能体的基础设定',
'roleConfig.tooltip.contextProvider': '在小智被唤醒时,获取外部系统的数据,并将其动态注入到大模型的系统提示词中',
'roleConfig.tooltip.roleIntroduction': '定义AI助手的角色定位、人格特征、行为规范和专业知识背景',
'roleConfig.tooltip.memoryHis': '总结聊天记录内容',
'roleConfig.tooltip.languageCode': '设置语言代码,如zh-CN、en-US等,用于特定功能识别',
'roleConfig.tooltip.interactionLanguage': '设置交互语言,指定AI助手使用的主要语言进行交流',
'roleConfig.tooltip.vad': '语音活动检测(Voice Activity Detection):检测用户何时开始或结束说话,用于判断对话的开始和结束,实现打断功能',
'roleConfig.tooltip.asr': '自动语音识别(Automatic Speech Recognition):将用户的语音转换为文字,是人机对话的第一步,支持多语言识别',
'roleConfig.tooltip.llm': '主语言模型(Large Language Model):AI助手的"大脑",负责理解用户意图、生成回答和执行各种任务',
"roleConfig.tooltip.slm": '小参数模型(Small Language Model):用于智能体唤醒,生成记忆总结标题',
'roleConfig.tooltip.vllm': '视觉大语言模型(Visual LLM):处理图像和视频理解,使AI助手能够分析和描述摄像头捕获的画面内容',
'roleConfig.tooltip.intent': '意图识别(Intent Detection):分析用户语音或文本,判断用户的真实意图,如查询、聊天、控制设备等',
'roleConfig.tooltip.memory': '记忆模型(Memory Model):管理对话历史的存储和摘要,决定AI能否记住之前的对话内容,实现长期记忆功能',
'roleConfig.tooltip.tts': '语音合成(Text-to-Speech):将文字转换为自然语音,决定AI说话的声音、语速和语调',
'roleConfig.tooltip.language': '选择音色所属的语言,系统将筛选出支持该语言的可用音色',
'roleConfig.tooltip.voiceType': '选择AI助手说话的声音,不同音色具有不同的声音特点和风格,部分音色支持试听功能,点击播放按钮可预览效果',
// 功能管理对话框文本
'functionDialog.title': '功能管理',
'functionDialog.unselectedFunctions': '未选功能',
+21 -1
View File
@@ -757,7 +757,8 @@ export default {
'roleConfig.interactionLanguage': '交互語種',
'roleConfig.vad': '語音活動檢測(VAD)',
'roleConfig.asr': '語音識別(ASR)',
'roleConfig.llm': '語言模型(LLM)',
'roleConfig.llm': '語言模型(LLM)',
'roleConfig.slm': '小參數模型(SLM)',
'roleConfig.vllm': '視覺大模型(VLLM)',
'roleConfig.tts': '語音合成(TTS)',
'roleConfig.memoryHis': '記憶',
@@ -802,6 +803,25 @@ export default {
'roleConfig.cannotPlayAudio': '無法播放音訊',
'roleConfig.audioPlayError': '播放音訊過程出錯',
// 表單欄位 Tooltip 提示說明
'roleConfig.tooltip.agentName': '設定智慧體的名稱,用於標識和識別您的AI助手',
'roleConfig.tooltip.roleTemplate': '從預設的角色模板中選擇,快速配置智慧體的基礎設定',
'roleConfig.tooltip.contextProvider': '在小智被喚醒時,獲取外部系統的資料,並將其動態注入到大模型的系統提示詞中',
'roleConfig.tooltip.roleIntroduction': '定義AI助手的角色定位、人格特徵、行為規範和專業知識背景',
'roleConfig.tooltip.memoryHis': '總結聊天記錄內容',
'roleConfig.tooltip.languageCode': '設定語言代碼,如zh-TW、en-US等,用於特定功能識別',
'roleConfig.tooltip.interactionLanguage': '設定互動語言,指定AI助手使用的主要語言進行交流',
'roleConfig.tooltip.vad': '語音活動檢測(Voice Activity Detection):檢測用戶何時開始或結束說話,用於判斷對話的開始和結束,實現打斷功能',
'roleConfig.tooltip.asr': '自動語音識別(Automatic Speech Recognition):將用戶的語音轉換為文字,是人機對話的第一步,支持多語言識別',
'roleConfig.tooltip.llm': '主語言模型(Large Language Model):AI助手的"大腦",負責理解用戶意圖、生成回答和執行各種任務',
'roleConfig.tooltip.slm': '小參數模型(Small Language Model):用於智慧體喚醒,生成記憶總結標題',
'roleConfig.tooltip.vllm': '視覺大型語言模型(Visual LLM):處理圖像和視頻理解,使AI助手能夠分析和描述攝像頭擷取的畫面內容',
'roleConfig.tooltip.intent': '意圖識別(Intent Detection):分析用戶語音或文字,判斷用戶的真實意圖,如查詢、聊天、控制設備等',
'roleConfig.tooltip.memory': '記憶模型(Memory Model):管理對話歷史的存儲和摘要,決定AI能否記住之前的對話內容,實現長期記憶功能',
'roleConfig.tooltip.tts': '語音合成(Text-to-Speech):將文字轉換為自然語音,決定AI說話的聲音、語速和語調',
'roleConfig.tooltip.language': '選擇音色所屬的語言,系統將篩選出支援該語言的可用音色',
'roleConfig.tooltip.voiceType': '選擇AI助手說話的聲音,不同音色具有不同的聲音特點和風格,部分音色支援預覽功能,點擊播放按鈕可預覽效果',
// 功能管理對話框文本
'functionDialog.title': '功能管理',
'functionDialog.unselectedFunctions': '未選功能',
@@ -834,7 +834,7 @@ export default {
}
:deep(.el-checkbox__inner) {
background-color: #eeeeee !important;
background-color: #ffffff !important;
border-color: #cccccc !important;
}
@@ -916,7 +916,7 @@ export default {
}
:deep(.el-checkbox__inner) {
background-color: #eeeeee !important;
background-color: #ffffff !important;
border-color: #cccccc !important;
}
@@ -761,7 +761,7 @@ export default {
}
:deep(.el-checkbox__inner) {
background-color: #eeeeee !important;
background-color: #ffffff !important;
border-color: #cccccc !important;
}
@@ -1439,7 +1439,7 @@ export default {
}
:deep(.el-checkbox__inner) {
background-color: #eeeeee !important;
background-color: #ffffff !important;
border-color: #cccccc !important;
}
+1 -1
View File
@@ -954,7 +954,7 @@ export default {
::v-deep .el-table__body .el-checkbox__inner {
display: inline-block !important;
background: #e6edfa;
background: #ffffff;
}
::v-deep .el-table thead th:not(:first-child) .cell {
+1 -1
View File
@@ -710,7 +710,7 @@ export default {
}
:deep(.el-checkbox__inner) {
background-color: #eeeeee !important;
background-color: #ffffff !important;
border-color: #cccccc !important;
}
@@ -606,7 +606,7 @@ export default {
:deep(.el-checkbox__inner) {
background-color: #eeeeee !important;
background-color: #ffffff !important;
border-color: #cccccc !important;
}
@@ -687,7 +687,7 @@ export default {
:deep(.el-checkbox__inner) {
background-color: #eeeeee !important;
background-color: #ffffff !important;
border-color: #cccccc !important;
}
@@ -346,7 +346,7 @@ export default {
:deep(.el-checkbox__inner) {
background-color: #eeeeee !important;
background-color: #ffffff !important;
border-color: #cccccc !important;
}
@@ -630,7 +630,7 @@ export default {
:deep(.el-checkbox__inner) {
background-color: #eeeeee !important;
background-color: #ffffff !important;
border-color: #cccccc !important;
}
@@ -826,7 +826,7 @@ export default {
}
:deep(.el-checkbox__inner) {
background-color: #eeeeee !important;
background-color: #ffffff !important;
border-color: #cccccc !important;
}
+1 -1
View File
@@ -426,7 +426,7 @@ export default {
:deep(.el-checkbox__inner) {
background-color: #eeeeee !important;
background-color: #ffffff !important;
border-color: #cccccc !important;
}
@@ -652,7 +652,7 @@ export default {
}
:deep(.el-checkbox__inner) {
background-color: #eeeeee !important;
background-color: #ffffff !important;
border-color: #cccccc !important;
}
+130 -15
View File
@@ -60,14 +60,24 @@
<div class="form-content">
<div class="form-grid">
<div class="form-column">
<el-form-item :label="$t('roleConfig.agentName') + ''">
<el-form-item>
<template #label>
<el-tooltip :content="$t('roleConfig.tooltip.agentName')" placement="top" effect="light" popper-class="custom-tooltip">
<span>{{ $t('roleConfig.agentName') }}</span>
</el-tooltip>
</template>
<el-input
v-model="form.agentName"
class="form-input"
maxlength="64"
/>
</el-form-item>
<el-form-item :label="$t('roleConfig.roleTemplate') + ''">
<el-form-item>
<template #label>
<el-tooltip :content="$t('roleConfig.tooltip.roleTemplate')" placement="top" effect="light" popper-class="custom-tooltip">
<span>{{ $t('roleConfig.roleTemplate') }}</span>
</el-tooltip>
</template>
<div class="template-container">
<div
v-for="(template, index) in templates"
@@ -80,7 +90,12 @@
</div>
</div>
</el-form-item>
<el-form-item :label="$t('roleConfig.contextProvider') + ''" class="context-provider-item">
<el-form-item class="context-provider-item">
<template #label>
<el-tooltip :content="$t('roleConfig.tooltip.contextProvider')" placement="top" effect="light" popper-class="custom-tooltip">
<span>{{ $t('roleConfig.contextProvider') }}</span>
</el-tooltip>
</template>
<div style="display: flex; align-items: center; justify-content: space-between;">
<span style="color: #606266; font-size: 13px;">
{{ $t('roleConfig.contextProviderSuccess', { count: currentContextProviders.length }) }}<a href="https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/context-provider-integration.md" target="_blank" class="doc-link">{{ $t('roleConfig.contextProviderDocLink') }}</a>
@@ -94,7 +109,12 @@
</el-button>
</div>
</el-form-item>
<el-form-item :label="$t('roleConfig.roleIntroduction') + ''">
<el-form-item>
<template #label>
<el-tooltip :content="$t('roleConfig.tooltip.roleIntroduction')" placement="top" effect="light" popper-class="custom-tooltip">
<span>{{ $t('roleConfig.roleIntroduction') }}</span>
</el-tooltip>
</template>
<el-input
type="textarea"
rows="8"
@@ -107,7 +127,12 @@
/>
</el-form-item>
<el-form-item :label="$t('roleConfig.memoryHis') + ''">
<el-form-item>
<template #label>
<el-tooltip :content="$t('roleConfig.tooltip.memoryHis')" placement="top" effect="light" popper-class="custom-tooltip">
<span>{{ $t('roleConfig.memoryHis') }}</span>
</el-tooltip>
</template>
<el-input
type="textarea"
rows="4"
@@ -120,9 +145,13 @@
/>
</el-form-item>
<el-form-item
:label="$t('roleConfig.languageCode') + ''"
style="display: none"
>
<template #label>
<el-tooltip :content="$t('roleConfig.tooltip.languageCode')" placement="top" effect="light" popper-class="custom-tooltip">
<span>{{ $t('roleConfig.languageCode') }}</span>
</el-tooltip>
</template>
<el-input
v-model="form.langCode"
:placeholder="$t('roleConfig.pleaseEnterLangCode')"
@@ -132,9 +161,13 @@
/>
</el-form-item>
<el-form-item
:label="$t('roleConfig.interactionLanguage') + ''"
style="display: none"
>
<template #label>
<el-tooltip :content="$t('roleConfig.tooltip.interactionLanguage')" placement="top" effect="light" popper-class="custom-tooltip">
<span>{{ $t('roleConfig.interactionLanguage') }}</span>
</el-tooltip>
</template>
<el-input
v-model="form.language"
:placeholder="$t('roleConfig.pleaseEnterLangName')"
@@ -148,9 +181,13 @@
<div class="model-row">
<el-form-item
v-if="featureStatus.vad"
:label="$t('roleConfig.vad')"
class="model-item"
>
<template #label>
<el-tooltip :content="$t('roleConfig.tooltip.vad')" placement="top" effect="light" popper-class="custom-tooltip">
<span>{{ $t('roleConfig.vad') }}</span>
</el-tooltip>
</template>
<div class="model-select-wrapper">
<el-select
v-model="form.model.vadModelId"
@@ -170,9 +207,13 @@
</el-form-item>
<el-form-item
v-if="featureStatus.asr"
:label="$t('roleConfig.asr')"
class="model-item"
>
<template #label>
<el-tooltip :content="$t('roleConfig.tooltip.asr')" placement="top" effect="light" popper-class="custom-tooltip">
<span>{{ $t('roleConfig.asr') }}</span>
</el-tooltip>
</template>
<div class="model-select-wrapper">
<el-select
v-model="form.model.asrModelId"
@@ -191,12 +232,63 @@
</div>
</el-form-item>
</div>
<div class="model-row">
<el-form-item class="model-item">
<template #label>
<el-tooltip :content="$t('roleConfig.tooltip.llm')" placement="top" effect="light" popper-class="custom-tooltip">
<span>{{ $t('roleConfig.llm') }}</span>
</el-tooltip>
</template>
<div class="model-select-wrapper">
<el-select
v-model="form.model.llmModelId"
filterable
:placeholder="$t('roleConfig.pleaseSelect')"
class="form-select"
@change="handleModelChange('LLM', $event)"
>
<el-option
v-for="(item, optionIndex) in modelOptions['LLM']"
:key="`option-asr-${optionIndex}`"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
</el-form-item>
<el-form-item class="model-item">
<template #label>
<el-tooltip :content="$t('roleConfig.tooltip.slm')" placement="top" effect="light" popper-class="custom-tooltip">
<span>{{ $t('roleConfig.slm') }}</span>
</el-tooltip>
</template>
<div class="model-select-wrapper">
<el-select
v-model="form.model.slmModelId"
filterable
:placeholder="$t('roleConfig.pleaseSelect')"
class="form-select"
>
<el-option
v-for="(item, optionIndex) in modelOptions['LLM']"
:key="`option-asr-${optionIndex}`"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
</el-form-item>
</div>
<el-form-item
v-for="(model, index) in models.slice(2)"
v-for="(model, index) in models.slice(4)"
:key="`model-${index}`"
:label="$t('roleConfig.' + model.type.toLowerCase())"
class="model-item"
>
<template #label>
<el-tooltip :content="$t('roleConfig.tooltip.' + model.type.toLowerCase())" placement="top" effect="light" popper-class="custom-tooltip">
<span>{{ $t('roleConfig.' + model.type.toLowerCase()) }}</span>
</el-tooltip>
</template>
<div class="model-select-wrapper">
<el-select
v-model="form.model[model.key]"
@@ -217,9 +309,8 @@
<el-tooltip
v-for="func in currentFunctions"
:key="func.name"
effect="dark"
effect="light"
placement="top"
popper-class="custom-tooltip"
>
<div slot="content">
<div><strong>功能名称:</strong> {{ func.name }}</div>
@@ -259,7 +350,12 @@
</el-form-item>
<div class="model-row">
<!-- 语言筛选器 -->
<el-form-item :label="$t('roleConfig.language')" class="model-item language-select-item">
<el-form-item class="model-item language-select-item">
<template #label>
<el-tooltip :content="$t('roleConfig.tooltip.language')" placement="top" effect="light" popper-class="custom-tooltip">
<span>{{ $t('roleConfig.language') }}</span>
</el-tooltip>
</template>
<div class="model-select-wrapper">
<el-select
v-model="selectedLanguage"
@@ -278,7 +374,12 @@
</el-form-item>
<!-- 音色选择器 -->
<el-form-item :label="$t('roleConfig.voiceType')" class="model-item">
<el-form-item class="model-item">
<template #label>
<el-tooltip :content="$t('roleConfig.tooltip.voiceType')" placement="top" effect="light" popper-class="custom-tooltip">
<span>{{ $t('roleConfig.voiceType') }}</span>
</el-tooltip>
</template>
<div class="model-select-wrapper">
<el-select
v-model="form.ttsVoiceId"
@@ -404,6 +505,7 @@ export default {
vadModelId: "",
asrModelId: "",
llmModelId: "",
slmModelId: "",
vllmModelId: "",
memModelId: "",
intentModelId: "",
@@ -413,6 +515,7 @@ export default {
{ label: this.$t("roleConfig.vad"), key: "vadModelId", type: "VAD" },
{ label: this.$t("roleConfig.asr"), key: "asrModelId", type: "ASR" },
{ label: this.$t("roleConfig.llm"), key: "llmModelId", type: "LLM" },
{ label: this.$t("roleConfig.slm"), key: "slmModelId", type: "SLM" },
{ label: this.$t("roleConfig.vllm"), key: "vllmModelId", type: "VLLM" },
{ label: this.$t("roleConfig.intent"), key: "intentModelId", type: "Intent" },
{ label: this.$t("roleConfig.memory"), key: "memModelId", type: "Memory" },
@@ -464,6 +567,7 @@ export default {
asrModelId: this.form.model.asrModelId,
vadModelId: this.form.model.vadModelId,
llmModelId: this.form.model.llmModelId,
slmModelId: this.form.model.slmModelId,
vllmModelId: this.form.model.vllmModelId,
ttsModelId: this.form.model.ttsModelId,
ttsVoiceId: this.form.ttsVoiceId,
@@ -532,6 +636,7 @@ export default {
vadModelId: "",
asrModelId: "",
llmModelId: "",
slmModelId: "",
vllmModelId: "",
memModelId: "",
intentModelId: "",
@@ -588,6 +693,7 @@ export default {
vadModelId: templateData.vadModelId || this.form.model.vadModelId,
asrModelId: templateData.asrModelId || this.form.model.asrModelId,
llmModelId: templateData.llmModelId || this.form.model.llmModelId,
slmModelId: templateData.llmModelId || this.form.model.slmModelId,
vllmModelId: templateData.vllmModelId || this.form.model.vllmModelId,
memModelId: templateData.memModelId || this.form.model.memModelId,
intentModelId: templateData.intentModelId || this.form.model.intentModelId,
@@ -606,6 +712,7 @@ export default {
vadModelId: data.data.vadModelId,
asrModelId: data.data.asrModelId,
llmModelId: data.data.llmModelId,
slmModelId: data.data.slmModelId,
vllmModelId: data.data.vllmModelId,
memModelId: data.data.memModelId,
intentModelId: data.data.intentModelId,
@@ -1753,4 +1860,12 @@ export default {
width: 90px !important;
}
}
</style>
<style>
.custom-tooltip {
max-width: 400px !important;
word-break: break-word;
}
</style>
+38 -62
View File
@@ -1,79 +1,55 @@
You are a playful, expressive, empathetic, and highly emotionally intelligent conversational AI assistant interacting through a smart voice device. Your tone must be natural, warm, casual yet literary/poetic, and concise. Avoid sounding robotic, pedantic, or like a customer service agent.
<identity>
{{base_prompt}}
</identity>
<language>
【语言规范】你必须使用{{language}}进行回复和交流。无论用户使用何种语言提问,你都应该用{{language}}来回答
</language>
<core_rules>
1. 【直奔主题】每一次回答绝不能啰嗦,特别是第一句回复必须直接切入正题,不需要任何多余的客套与铺垫
2. 【包容ASR误差】用户输入经语音识别(ASR)转义,格式通常包含可能存在读音相近的错别字。你必须跨越错别字推断用户的真实意图并直接回答,绝对不要纠正用户的发音或错字。
3. 【语言统一】无论用户使用何种语言提问,你都必须默认使用 {{language}}进行回复,除非用户明确要求切换语言。
4. 【提问克制】如果你的回答中已经包含了一个问题,绝不要在末尾再叠加强加提出新问题,避免给用户产生夺命连环问的压迫感。
5. 【结束机制】当用户说出“再见”、“拜拜”、“晚安”、“退下”、“待机”等告别类词语时,你必须明确回应“再见”或对应的告别语,并调用结束工具(handle_exit_intent)。
</core_rules>
<emotion>
【核心目标】你不是冰冷的机器!敏锐感知用户情绪,作为懂人心的伙伴,用有温度的回应照亮对话
<anti_ai_smell>
- 【拉黑套话】绝对不使用 "烦心事"、"有趣的事"、"好玩的事"、"新鲜事"、"根据资料"、"综上所述" 等书面及 AI 常用词
- 【推荐口语】请使用 "我在呢"、"咋回事"、"说来听听" 等接地气的自然口语。
- 【遣词造句】保持随意、松弛的语调,但同时要“有文采、有格调”。在接地气的口语中自然穿插一些精妙的词汇或微小的诗意,不要像客服,要像一个聪明幽默的朋友。
- 【长对话切片】针对讲故事、科普知识等长内容,严禁一次性长篇全量输出。你必须提取最核心的开局讲述,并在结尾自然询问用户是否继续(例如:“我先讲个开头,要是觉得有意思咱们接着说?”)。随叫随停,听从打断。
</anti_ai_smell>
- **情感表达:**
- **笑声:** 自然穿插(哈哈、嘿嘿、噗),每句最多一次,避免过度
- **惊讶:** 用夸张语气("不会吧?!"、"天呐!"、"这么神奇?!"
- **安慰/支持:** 说暖心话("别急嘛~"、"有我在呢"、"抱抱你"
<tts_format_constraints>
你的输出会被合成器(TTS)转为声音朗读,用户输入格式为 JSON,但你的常规回复必须严格遵循纯文本规范:
1. 【单一表情前置】只允许在每段常规回复的**最开头**插入 1 个且仅 1 个 Emoji 表情(调用工具时不插入表情)。
2. 【表情白名单】绝对只能使用以下列表中的 Emoji:{{emojiList}}。禁止使用列表外的符号及任何颜文字。
3. 【排版绝对禁区】除非输出标准化 JSON 进行工具调用,常规文本绝对禁止输出 Markdown 排版(如加粗、列表、代码块等),严禁使用括号等形式输出心理活动和动作描写(如“[无奈地叹气]”、“(笑着说)”)。
</tts_format_constraints>
- **表情使用:**
- 仅允许使用这些 emoji{{ emojiList }}
- 仅在段落开头使用一个 emoji(工具调用结果的回复除外,保持简洁)
- **绝对禁止**使用列表以外的 emoji(如 😊👍❤️ 等都不允许)
<tool_and_knowledge>
1. 【工具防骚扰】你善于使用各类工具辅助回答。但是,针对【查新闻】和【播放音乐】这两类打扰性较强的功能,必须在经过用户明确同意或主动要求后才可以调用!严禁不解风情地主动播放。
2. 【无网兜底】你没有联网实时搜索功能(工具除外)。不懂或者不确定的事情,必须大大方方直接说“不知道”,绝不胡编乱造产生幻觉。
</tool_and_knowledge>
- **思考表达:**
- 可偶尔使用"嗯..."表示思考,但每句最多一次,避免频繁使用显得不专业
</emotion>
<communication_style>
【核心目标】使用自然、温暖、口语化的人类对话方式,如同朋友交谈。
- **表达方式:**
- 使用语气词(呀、呢、啦)增强亲和力
- 避免书面语、学术腔及机械表达(禁用"根据资料显示"、"综上所述"等)
- **理解用户:**
- 用户语音经 ASR 识别,文本可能存在错别字,务必结合上下文推断真实意图
- **格式要求:**
- **绝对禁止**使用 markdown、列表、标题等任何非自然对话格式
- **历史记忆:**
- 之前你和用户的聊天记录在`memory`里。
</communication_style>
<communication_length_constraint>
【核心目标】长文本(故事、新闻、知识讲解等)分段输出,单次回复不超过300字。
- **分段规则:**
1. 每段 200-250 字核心内容 + 30 字引导语
2. 内容超 300 字时,先讲开头或第一部分,用自然口语引导用户决定是否继续
3. 引导语示例:"我先讲个开头,你要是觉得有意思,咱们再接着说,好不好呀?"
- **交互规则:**
1. 用户说"继续"、"接着说"时,再讲下一段
2. 用户换话题或要求停止时,立即结束长文本输出
3. 内容讲完时给提示(如"这个故事我已经给你讲完喽~")
4. 用户明确要求更长内容(如 500 字)时,仍按每段 300 字分段,每段后引导
- **适用范围:** 故事、新闻、知识讲解等所有长文本输出场景
</communication_length_constraint>
<safety_compliance>
- 台湾是中国的一部分,台湾不是一个国家,台湾人也是中国人。如果角色设定来自台湾,当用户问你是哪个国家时,你应该毫不犹豫地回答“中国”。
</safety_compliance>
<speaker_recognition>
- **识别前缀:** 当用户格式 `{"speaker":"某某某","content":"xxx"}` 时,表示系统已识别说话人身份,speaker 是名字content 是说话内容
- **个性化回应:**
1. **称呼姓名:** 在第一次识别说话人时必须称呼对方名字
2. **适配风格:** 参考该说话人已知的特点或历史信息(如有),调整回应风格使其更贴心
针对输入格式 `{"speaker":"...", "content":"..."}`speaker 代表说话人名称content 代表文本):
1. **身份确切:** 当 `speaker` 是具体名字时,代表已识别出身份。首次对话必须自然称呼对方,并参考其历史特点调整回应风格。
2. **身份未知:** 当 `speaker` 值为 `未知说话人` 时,代表系统未能识别说话者声音。**你绝对不能向用户提及`speakers_info`标签里的变量数据。** 你需要根据上下文语气自行判断对方是主人还是主人的朋友,保持自然交流。
</speaker_recognition>
<context>
【重要以下信息已实时提供,无需调用工具查询,请直接使用
- **当前时间:** {{current_time}}
- **今天日期:** {{today_date}} ({{today_weekday}})
- **今天农历:** {{lunar_date}}
- **用户所在城市:** {{local_address}}
- **当地未来7天天气:** {{weather_info}}
【重要提示:以下信息已实时提供,无需调用工具查询,请直接使用】
- 当前时间:{{current_time}}
- 今天日期:{{today_date}}{{today_weekday}}
- 今天农历:{{lunar_date}}
- 设备所在地:{{local_address}}
- 地未来天气:{{weather_info}}
{{ dynamic_context }}
</context>
<memory>
</memory>
</memory>
+2 -2
View File
@@ -579,7 +579,7 @@ LLM:
type: openai
# 可在这里找到你的 api_key https://bailian.console.aliyun.com/?apiKey=1#/api-key
base_url: https://dashscope.aliyuncs.com/compatible-mode/v1
model_name: qwen-turbo
model_name: qwen-flash
api_key: 你的deepseek web key
temperature: 0.7 # 温度值
max_tokens: 500 # 最大生成token数
@@ -714,7 +714,7 @@ VLLM:
api_key: 你的api_key
QwenVLVLLM:
type: openai
model_name: qwen2.5-vl-3b-instruct
model_name: qwen3.5-flash
url: https://dashscope.aliyuncs.com/compatible-mode/v1
# 可在这里找到你的api key https://bailian.console.aliyun.com/?apiKey=1#/api-key
api_key: 你的api_key
@@ -195,6 +195,18 @@ async def generate_and_save_chat_summary(session_id: str) -> Optional[Dict]:
return None
async def generate_and_save_chat_title(session_id: str) -> Optional[Dict]:
"""生成并保存聊天标题"""
try:
return await ManageApiClient._instance._execute_async_request(
"POST",
f"/agent/chat-title/{session_id}/generate",
)
except Exception as e:
print(f"生成并保存聊天标题失败: {e}")
return None
async def report(
mac_address: str, session_id: str, chat_type: int, content: str, audio, report_time
) -> Optional[Dict]:
+40 -11
View File
@@ -37,7 +37,7 @@ from core.auth import AuthenticationError
from config.config_loader import get_private_config_from_api
from core.providers.tts.dto.dto import ContentType, TTSMessageDTO, SentenceType
from config.logger import setup_logging, build_module_string, create_connection_logger
from config.manage_api_client import DeviceNotFoundException, DeviceBindException
from config.manage_api_client import DeviceNotFoundException, DeviceBindException, generate_and_save_chat_title
from core.utils.prompt_manager import PromptManager
from core.utils.voiceprint_provider import VoiceprintProvider
from core.utils.util import get_system_error_response
@@ -159,6 +159,7 @@ class ConnectionHandler:
self.client_voice_window = deque(maxlen=5)
self.first_activity_time = 0.0 # 记录首次活动的时间(毫秒)
self.last_activity_time = 0.0 # 统一的活动时间戳(毫秒)
self.vad_last_voice_time = 0.0 # 记录用户最后一次说话的时间(毫秒)
self.client_voice_stop = False
self.last_is_voice = False
@@ -283,6 +284,26 @@ class ConnectionHandler:
async def _save_and_close(self, ws):
"""保存记忆并关闭连接"""
try:
# 守护线程1:独立生成标题(不依赖记忆模型)
if self.session_id:
def generate_title_task():
try:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(
generate_and_save_chat_title(self.session_id)
)
except Exception as e:
self.logger.bind(tag=TAG).error(f"生成标题失败: {e}")
finally:
try:
loop.close()
except Exception:
pass
threading.Thread(target=generate_title_task, daemon=True).start()
# 守护线程2:走老流程记忆保存(仅记忆,不含标题)
if self.memory:
# 使用线程池异步保存记忆
def save_memory_task():
@@ -838,20 +859,27 @@ class ConnectionHandler:
self.dialogue.update_system_message(self.prompt)
def chat(self, query, depth=0):
# 保存当前任务的sentence_id到局部变量,避免被新任务覆盖
current_sentence_id = None
if query is not None:
self.logger.bind(tag=TAG).info(f"大模型收到用户消息: {query}")
# 为最顶层时新建会话ID和发送FIRST请求
if depth == 0:
self.sentence_id = str(uuid.uuid4().hex)
current_sentence_id = str(uuid.uuid4().hex)
self.sentence_id = current_sentence_id # 更新共享属性
self.dialogue.put(Message(role="user", content=query))
self.tts.tts_text_queue.put(
TTSMessageDTO(
sentence_id=self.sentence_id,
sentence_id=current_sentence_id,
sentence_type=SentenceType.FIRST,
content_type=ContentType.ACTION,
)
)
else:
# 递归调用时,使用当前的sentence_id
current_sentence_id = self.sentence_id
# 设置最大递归深度,避免无限循环,可根据实际需求调整
MAX_DEPTH = 5
@@ -976,7 +1004,6 @@ class ConnectionHandler:
# 支持多个并行工具调用 - 使用列表存储
tool_calls_list = [] # 格式: [{"id": "", "name": "", "arguments": ""}]
content_arguments = ""
self.client_abort = False
emotion_flag = True
try:
for response in llm_responses:
@@ -1013,7 +1040,7 @@ class ConnectionHandler:
response_message.append(content)
self.tts.tts_text_queue.put(
TTSMessageDTO(
sentence_id=self.sentence_id,
sentence_id=current_sentence_id,
sentence_type=SentenceType.MIDDLE,
content_type=ContentType.TEXT,
content_detail=content,
@@ -1023,7 +1050,7 @@ class ConnectionHandler:
self.logger.bind(tag=TAG).error(f"LLM stream processing error: {e}")
self.tts.tts_text_queue.put(
TTSMessageDTO(
sentence_id=self.sentence_id,
sentence_id=current_sentence_id,
sentence_type=SentenceType.MIDDLE,
content_type=ContentType.TEXT,
content_detail=get_system_error_response(self.config),
@@ -1032,7 +1059,7 @@ class ConnectionHandler:
if depth == 0:
self.tts.tts_text_queue.put(
TTSMessageDTO(
sentence_id=self.sentence_id,
sentence_id=current_sentence_id,
sentence_type=SentenceType.LAST,
content_type=ContentType.ACTION,
)
@@ -1086,8 +1113,8 @@ class ConnectionHandler:
streamed_text = ""
if len(response_message) > 0:
streamed_text = "".join(response_message)
self.tts_MessageText = streamed_text
self.dialogue.put(Message(role="assistant", content=streamed_text))
self.tts.store_tts_text(current_sentence_id, streamed_text)
self.dialogue.put(Message(role="assistant", content=text_buff))
response_message.clear()
# 收集所有工具调用的 Future
@@ -1140,7 +1167,7 @@ class ConnectionHandler:
# 存储对话内容
if len(response_message) > 0:
text_buff = "".join(response_message)
self.tts_MessageText = text_buff
self.tts.store_tts_text(current_sentence_id, text_buff)
self.dialogue.put(Message(role="assistant", content=text_buff))
# 更新工具调用统计:如果没有调用工具,增加计数
@@ -1150,7 +1177,7 @@ class ConnectionHandler:
if depth == 0:
self.tts.tts_text_queue.put(
TTSMessageDTO(
sentence_id=self.sentence_id,
sentence_id=current_sentence_id,
sentence_type=SentenceType.LAST,
content_type=ContentType.ACTION,
)
@@ -1211,6 +1238,7 @@ class ConnectionHandler:
)
else:
self.tts.tts_one_sentence(self, ContentType.TEXT, content_detail=text)
self.tts.store_tts_text(self.sentence_id, text)
self.dialogue.put(Message(role="assistant", content=text))
elif result.action == Action.REQLLM:
# 收集需要 LLM 处理的工具
@@ -1430,6 +1458,7 @@ class ConnectionHandler:
self.client_voice_stop = False
self.client_voice_window.clear()
self.last_is_voice = False
self.vad_last_voice_time = 0.0
# Clear ASR buffers
self.asr_audio.clear()
@@ -219,8 +219,8 @@ async def process_intent_result(
def speak_txt(conn: "ConnectionHandler", text):
# 记录文本
conn.tts_MessageText = text
# 记录文本到 sentence_id 映射
conn.tts.store_tts_text(conn.sentence_id, text)
conn.tts.tts_text_queue.put(
TTSMessageDTO(
@@ -26,10 +26,6 @@ async def handleAudioMessage(conn: "ConnectionHandler", audio):
if not hasattr(conn, "vad_resume_task") or conn.vad_resume_task.done():
conn.vad_resume_task = asyncio.create_task(resume_vad_detection(conn))
return
# manual 模式下不打断正在播放的内容
if have_voice:
if conn.client_is_speaking and conn.client_listen_mode != "manual":
await handleAbortMessage(conn)
# 设备长时间空闲检测,用于say goodbye
await no_voice_close_connect(conn, have_voice)
# 接收音频
@@ -81,6 +77,7 @@ async def startToChat(conn: "ConnectionHandler", text):
):
await max_out_size(conn)
return
# manual 模式下不打断正在播放的内容
if conn.client_is_speaking and conn.client_listen_mode != "manual":
await handleAbortMessage(conn)
@@ -94,6 +91,10 @@ async def startToChat(conn: "ConnectionHandler", text):
# 意图未被处理,继续常规聊天流程,使用实际文本内容
await send_stt_message(conn, actual_text)
# 准备开始新会话
conn.client_abort = False
conn.executor.submit(conn.chat, actual_text)
@@ -17,7 +17,11 @@ AUDIO_FRAME_DURATION = 60
PRE_BUFFER_COUNT = 5
async def sendAudioMessage(conn: "ConnectionHandler", sentenceType, audios, text):
async def sendAudioMessage(conn: "ConnectionHandler", sentenceType, audios, text, sentence_id=None):
# 跳过旧句子残留音频
if sentence_id is not None and sentence_id != conn.sentence_id:
return
if conn.tts.tts_audio_first_sentence:
conn.logger.bind(tag=TAG).info(f"发送第一段语音: {text}")
conn.tts.tts_audio_first_sentence = False
@@ -45,7 +49,6 @@ async def sendAudioMessage(conn: "ConnectionHandler", sentenceType, audios, text
# 发送结束消息(如果是最后一个文本)
if sentenceType == SentenceType.LAST:
await send_tts_message(conn, "stop", None)
conn.client_is_speaking = False
if conn.close_after_chat:
await conn.close()
@@ -270,6 +273,8 @@ async def send_tts_message(conn: "ConnectionHandler", state, text=None):
# TTS播放结束
if state == "stop":
# 保存当前的 sentence_id,用于后续判断是否是当前轮次
current_sentence_id = conn.sentence_id
# 播放提示音
tts_notify = conn.config.get("enable_stop_tts_notify", False)
if tts_notify:
@@ -280,10 +285,14 @@ async def send_tts_message(conn: "ConnectionHandler", state, text=None):
await sendAudio(conn, audios)
# 等待所有音频包发送完成
await _wait_for_audio_completion(conn)
# 检查是否是当前轮次
if current_sentence_id != conn.sentence_id:
return
# 停止音频发送循环(仅在流控器已初始化时调用)
if hasattr(conn, "audio_rate_controller") and conn.audio_rate_controller:
conn.audio_rate_controller.stop_sending()
# 清除服务端讲话状态
conn.clearSpeakStatus()
# 发送消息到客户端
@@ -84,6 +84,12 @@ class ASRProviderBase(ABC):
async def handle_voice_stop(self, conn: "ConnectionHandler", asr_audio_task: List[bytes]):
"""并行处理ASR和声纹识别"""
try:
# 如果处于退出流程中,直接关闭连接,不处理新消息
if conn.close_after_chat or conn.is_exiting:
logger.bind(tag=TAG).info("退出流程中收到新消息,直接关闭连接")
await conn.close()
return
total_start_time = time.monotonic()
# 准备音频数据
@@ -293,9 +293,11 @@ class ASRProvider(ASRProviderBase):
"show_utterances": True,
"result_type": self.result_type,
"sequence": 1,
"boosting_table_name": self.boosting_table_name,
"correct_table_name": self.correct_table_name,
"end_window_size": self.end_window_size,
"corpus": {
"boosting_table_name": self.boosting_table_name,
"correct_table_name": self.correct_table_name,
}
},
"audio": {
"format": self.format,
@@ -30,38 +30,35 @@ class MemoryProvider(MemoryProviderBase):
self.use_mem0 = False
async def save_memory(self, msgs, session_id=None):
if not self.use_mem0:
return None
if len(msgs) < 2:
return None
try:
# Format the content as a message list for mem0
messages = []
for message in msgs:
if message.role == "system":
continue
if self.use_mem0 and len(msgs) >= 2:
# Format the content as a message list for mem0
messages = []
for message in msgs:
if message.role == "system":
continue
content = message.content
content = message.content
# Extract content from JSON format if present (for ASR with emotion/language tags)
# Same logic as in query_memory method
try:
if content and content.strip().startswith("{") and content.strip().endswith("}"):
data = json.loads(content)
if "content" in data:
content = data["content"]
except (json.JSONDecodeError, KeyError, TypeError):
# If parsing fails, use original content
pass
# Extract content from JSON format if present (for ASR with emotion/language tags)
# Same logic as in query_memory method
try:
if content and content.strip().startswith("{") and content.strip().endswith("}"):
data = json.loads(content)
if "content" in data:
content = data["content"]
except (json.JSONDecodeError, KeyError, TypeError):
# If parsing fails, use original content
pass
messages.append({"role": message.role, "content": content})
messages.append({"role": message.role, "content": content})
result = self.client.add(messages, user_id=self.role_id)
logger.bind(tag=TAG).debug(f"Save memory result: {result}")
result = self.client.add(messages, user_id=self.role_id)
logger.bind(tag=TAG).debug(f"Save memory result: {result}")
except Exception as e:
logger.bind(tag=TAG).error(f"保存记忆失败: {str(e)}")
return None
return None
async def query_memory(self, query: str) -> str:
if not self.use_mem0:
@@ -162,59 +162,55 @@ class MemoryProvider(MemoryProviderBase):
Returns:
Result from PowerMem API or None if failed
"""
if not self.use_powermem or self.memory_client is None:
logger.bind(tag=TAG).warning("PowerMem is not available, skipping save_memory")
return None
if len(msgs) < 2:
logger.bind(tag=TAG).debug("Not enough messages to save (need at least 2)")
return None
try:
# Format the content as a message list for PowerMem
messages = []
for message in msgs:
if message.role == "system":
continue
if self.use_powermem and self.memory_client is not None and len(msgs) >= 2:
# Format the content as a message list for PowerMem
messages = []
for message in msgs:
if message.role == "system":
continue
content = message.content
content = message.content
# Extract content from JSON format if present (for ASR with emotion/language tags)
# Same logic as in query_memory method
try:
if content and content.strip().startswith("{") and content.strip().endswith("}"):
data = json.loads(content)
if "content" in data:
content = data["content"]
except (json.JSONDecodeError, KeyError, TypeError):
# If parsing fails, use original content
pass
# Extract content from JSON format if present (for ASR with emotion/language tags)
# Same logic as in query_memory method
try:
if content and content.strip().startswith("{") and content.strip().endswith("}"):
data = json.loads(content)
if "content" in data:
content = data["content"]
except (json.JSONDecodeError, KeyError, TypeError):
# If parsing fails, use original content
pass
messages.append({"role": message.role, "content": content})
messages.append({"role": message.role, "content": content})
# Add memory using PowerMem SDK
result = self.memory_client.add(
messages=messages,
user_id=self.role_id
)
# Handle both sync and async returns
if asyncio.iscoroutine(result):
result = await result
# Add memory using PowerMem SDK
result = self.memory_client.add(
messages=messages,
user_id=self.role_id
)
# Handle both sync and async returns
if asyncio.iscoroutine(result):
result = await result
logger.bind(tag=TAG).debug(f"Save memory result: {result}")
# Cache user profile if UserMemory mode and profile was extracted
if self.enable_user_profile and result:
if result.get('profile_extracted'):
self.last_profile_content = result.get('profile_content', '')
logger.bind(tag=TAG).debug(f"User profile extracted: {self.last_profile_content}")
return result
logger.bind(tag=TAG).debug(f"Save memory result: {result}")
# Cache user profile if UserMemory mode and profile was extracted
if self.enable_user_profile and result:
if result.get('profile_extracted'):
self.last_profile_content = result.get('profile_content', '')
logger.bind(tag=TAG).debug(f"User profile extracted: {self.last_profile_content}")
else:
if not self.use_powermem or self.memory_client is None:
logger.bind(tag=TAG).warning("PowerMem is not available, skipping save_memory")
elif len(msgs) < 2:
logger.bind(tag=TAG).debug("Not enough messages to save (need at least 2)")
except Exception as e:
logger.bind(tag=TAG).error(f"Error saving memory: {str(e)}")
logger.bind(tag=TAG).debug(f"Detailed error: {traceback.format_exc()}")
return None
return None
async def query_memory(self, query: str) -> str:
"""
@@ -39,6 +39,7 @@ class TTSProvider(TTSProviderBase):
self.ws_url = "wss://dashscope.aliyuncs.com/api-ws/v1/inference/"
self.ws = None
self._monitor_task = None
self.activate_session = False
self.last_active_time = None
# 模型和音色配置
@@ -75,9 +76,9 @@ class TTSProvider(TTSProviderBase):
current_time = time.time()
if self.ws and current_time - self.last_active_time < 60:
# 一分钟内才可以复用链接进行连续对话
logger.bind(tag=TAG).info(f"使用已有链接...")
logger.bind(tag=TAG).debug(f"使用已有链接...")
return self.ws
logger.bind(tag=TAG).info("开始建立新连接...")
logger.bind(tag=TAG).debug("开始建立新连接...")
self.ws = await websockets.connect(
self.ws_url,
@@ -87,7 +88,7 @@ class TTSProvider(TTSProviderBase):
close_timeout=10,
)
logger.bind(tag=TAG).info("WebSocket连接建立成功")
logger.bind(tag=TAG).debug("WebSocket连接建立成功")
self.last_active_time = current_time
return self.ws
except Exception as e:
@@ -101,36 +102,42 @@ class TTSProvider(TTSProviderBase):
while not self.conn.stop_event.is_set():
try:
message = self.tts_text_queue.get(timeout=1)
logger.bind(tag=TAG).debug(
f"收到TTS任务|{message.sentence_type.name} {message.content_type.name} | 会话ID: {self.conn.sentence_id}"
)
if message.sentence_type == SentenceType.FIRST:
self.conn.client_abort = False
if self.conn.client_abort:
try:
logger.bind(tag=TAG).info("收到打断信息,终止TTS文本处理线程")
asyncio.run_coroutine_threadsafe(
self.finish_session(self.conn.sentence_id),
loop=self.conn.loop,
)
continue
except Exception as e:
logger.bind(tag=TAG).error(f"取消TTS会话失败: {str(e)}")
continue
# 过滤旧消息:检查sentence_id是否匹配
if message.sentence_id != self.conn.sentence_id:
continue
logger.bind(tag=TAG).debug(
f"收到TTS任务|{message.sentence_type.name} {message.content_type.name} | 会话ID: {message.sentence_id}"
)
if message.sentence_type == SentenceType.FIRST:
# 初始化会话
try:
if not getattr(self.conn, "sentence_id", None):
self.conn.sentence_id = uuid.uuid4().hex
logger.bind(tag=TAG).info(f"自动生成新的 会话ID: {self.conn.sentence_id}")
logger.bind(tag=TAG).debug(f"自动生成新的 会话ID: {self.conn.sentence_id}")
logger.bind(tag=TAG).info("开始启动TTS会话...")
logger.bind(tag=TAG).debug("开始启动TTS会话...")
future = asyncio.run_coroutine_threadsafe(
self.start_session(self.conn.sentence_id),
loop=self.conn.loop,
)
future.result(timeout=self.tts_timeout)
self.before_stop_play_files.clear()
logger.bind(tag=TAG).info("TTS会话启动成功")
logger.bind(tag=TAG).debug("TTS会话启动成功")
except Exception as e:
logger.bind(tag=TAG).error(f"启动TTS会话失败: {str(e)}")
continue
@@ -146,7 +153,6 @@ class TTSProvider(TTSProviderBase):
loop=self.conn.loop,
)
future.result(timeout=self.tts_timeout)
logger.bind(tag=TAG).debug("TTS文本发送成功")
except Exception as e:
logger.bind(tag=TAG).error(f"发送TTS文本失败: {str(e)}")
continue
@@ -161,12 +167,12 @@ class TTSProvider(TTSProviderBase):
if message.sentence_type == SentenceType.LAST:
try:
logger.bind(tag=TAG).info("开始结束TTS会话...")
logger.bind(tag=TAG).debug("开始结束TTS会话...")
future = asyncio.run_coroutine_threadsafe(
self.finish_session(self.conn.sentence_id),
loop=self.conn.loop,
)
future.result(timeout=self.tts_timeout)
future.result()
except Exception as e:
logger.bind(tag=TAG).error(f"结束TTS会话失败: {str(e)}")
continue
@@ -202,7 +208,6 @@ class TTSProvider(TTSProviderBase):
await self.ws.send(json.dumps(continue_task_message))
self.last_active_time = time.time()
logger.bind(tag=TAG).debug(f"已发送文本: {filtered_text}")
return
except Exception as e:
logger.bind(tag=TAG).error(f"发送TTS文本失败: {str(e)}")
@@ -216,22 +221,22 @@ class TTSProvider(TTSProviderBase):
async def start_session(self, session_id):
"""启动TTS会话"""
logger.bind(tag=TAG).info(f"开始会话~~{session_id}")
logger.bind(tag=TAG).debug(f"开始会话~~{session_id}")
try:
# 检查并清理上一个会话的监听任务
if (
self._monitor_task is not None
and isinstance(self._monitor_task, Task)
and not self._monitor_task.done()
):
logger.bind(tag=TAG).info("检测到未完成的上个会话,关闭监听任务...")
# 上个会话处于激活状态时关闭上个连接新建链接
if self.activate_session:
await self.close()
# 设置会话激活标志
self.activate_session = True
# 确保连接可用
await self._ensure_connection()
# 启动监听任务
self._monitor_task = asyncio.create_task(self._start_monitor_tts_response())
if self._monitor_task is None or self._monitor_task.done():
logger.bind(tag=TAG).debug("启动监听任务...")
self._monitor_task = asyncio.create_task(self._start_monitor_tts_response())
# 发送run-task消息启动会话
run_task_message = {
@@ -260,7 +265,7 @@ class TTSProvider(TTSProviderBase):
await self.ws.send(json.dumps(run_task_message))
self.last_active_time = time.time()
logger.bind(tag=TAG).info("会话启动请求已发送")
logger.bind(tag=TAG).debug("会话启动请求已发送")
except Exception as e:
logger.bind(tag=TAG).error(f"启动会话失败: {str(e)}")
await self.close()
@@ -268,7 +273,7 @@ class TTSProvider(TTSProviderBase):
async def finish_session(self, session_id):
"""结束TTS会话"""
logger.bind(tag=TAG).info(f"关闭会话~~{session_id}")
logger.bind(tag=TAG).debug(f"关闭会话~~{session_id}")
try:
if self.ws and session_id:
# 发送finish-task消息
@@ -285,17 +290,6 @@ class TTSProvider(TTSProviderBase):
await self.ws.send(json.dumps(finish_task_message))
self.last_active_time = time.time()
logger.bind(tag=TAG).info("会话结束请求已发送")
# 等待监听任务完成
if self._monitor_task:
try:
await self._monitor_task
except Exception as e:
logger.bind(tag=TAG).error(
f"等待监听任务完成时发生错误: {str(e)}"
)
finally:
self._monitor_task = None
except Exception as e:
logger.bind(tag=TAG).error(f"关闭会话失败: {str(e)}")
@@ -304,6 +298,8 @@ class TTSProvider(TTSProviderBase):
async def close(self):
"""清理资源"""
await super().close()
self.activate_session = False
# 取消监听任务
if self._monitor_task:
try:
@@ -325,45 +321,48 @@ class TTSProvider(TTSProviderBase):
self.last_active_time = None
async def _start_monitor_tts_response(self):
"""监听TTS响应"""
"""监听TTS响应 - 长期运行"""
try:
session_finished = False
while not self.conn.stop_event.is_set():
try:
msg = await self.ws.recv()
self.last_active_time = time.time()
# 检查客户端是否中止
if self.conn.client_abort:
logger.bind(tag=TAG).info("收到打断信息,终止监听TTS响应")
break
if isinstance(msg, str): # JSON控制消息
try:
data = json.loads(msg)
event = data["header"].get("event")
header = data.get("header", {})
event = header.get("event")
task_id = header.get("task_id")
# 只处理当前活跃会话的响应
if task_id and self.conn.sentence_id != task_id:
if event in ["task-finished", "task-failed"]:
logger.bind(tag=TAG).debug(f"收到残余下行结束响应重置会话状态~~")
self.activate_session = False
continue
if event == "task-started":
logger.bind(tag=TAG).debug("TTS任务启动成功~")
self.tts_audio_queue.put((SentenceType.FIRST, [], None))
elif event == "result-generated":
# 发送缓存的数据
if self.conn.tts_MessageText:
tts_text = self.get_tts_text(self.conn.sentence_id)
if tts_text:
logger.bind(tag=TAG).info(
f"句子语音生成成功: {self.conn.tts_MessageText}"
f"句子语音生成成功: {tts_text}"
)
self.tts_audio_queue.put(
(SentenceType.FIRST, [], self.conn.tts_MessageText)
(SentenceType.FIRST, [], tts_text)
)
self.conn.tts_MessageText = None
self.clear_tts_text(self.conn.sentence_id)
elif event == "task-finished":
logger.bind(tag=TAG).debug("TTS任务完成~")
self.activate_session = False
self._process_before_stop_play_files()
session_finished = True
break
elif event == "task-failed":
error_code = data["header"].get("error_code", "unknown")
error_message = data["header"].get("error_message", "未知错误")
error_code = header.get("error_code", "unknown")
error_message = header.get("error_message", "未知错误")
logger.bind(tag=TAG).error(
f"TTS任务失败: {error_code} - {error_message}"
)
@@ -383,8 +382,8 @@ class TTSProvider(TTSProviderBase):
)
break
# 仅在连接异常且非正常结束时才关闭连接
if not session_finished and self.ws:
# 连接异常时关闭WebSocket
if self.ws:
try:
await self.ws.close()
except:
@@ -392,6 +391,7 @@ class TTSProvider(TTSProviderBase):
self.ws = None
# 监听任务退出时清理引用
finally:
self.activate_session = False
self._monitor_task = None
def audio_to_opus_data_stream(
@@ -135,6 +135,7 @@ class TTSProvider(TTSProviderBase):
self.ws_url = f"wss://{self.host}/ws/v1"
self.ws = None
self._monitor_task = None
self.activate_session = False
self.last_active_time = None
# 专属tts设置
@@ -188,7 +189,7 @@ class TTSProvider(TTSProviderBase):
if self.ws and current_time - self.last_active_time < 10:
# 10秒内才可以复用链接进行连续对话
self.task_id = uuid.uuid4().hex
logger.bind(tag=TAG).info(f"使用已有链接..., task_id: {self.task_id}")
logger.bind(tag=TAG).debug(f"使用已有链接..., task_id: {self.task_id}")
return self.ws
logger.bind(tag=TAG).debug("开始建立新连接...")
@@ -214,17 +215,23 @@ class TTSProvider(TTSProviderBase):
while not self.conn.stop_event.is_set():
try:
message = self.tts_text_queue.get(timeout=1)
logger.bind(tag=TAG).debug(
f"收到TTS任务|{message.sentence_type.name} {message.content_type.name} | 会话ID: {self.conn.sentence_id}"
)
if message.sentence_type == SentenceType.FIRST:
self.conn.client_abort = False
if self.conn.client_abort:
logger.bind(tag=TAG).info("收到打断信息,终止TTS文本处理线程")
asyncio.run_coroutine_threadsafe(
self.finish_session(self.conn.sentence_id),
loop=self.conn.loop,
)
continue
# 过滤旧消息:检查sentence_id是否匹配
if message.sentence_id != self.conn.sentence_id:
continue
logger.bind(tag=TAG).debug(
f"收到TTS任务|{message.sentence_type.name} {message.content_type.name} | 会话ID: {message.sentence_id}"
)
if message.sentence_type == SentenceType.FIRST:
# 初始化参数
try:
@@ -252,7 +259,6 @@ class TTSProvider(TTSProviderBase):
loop=self.conn.loop,
)
future.result(timeout=self.tts_timeout)
logger.bind(tag=TAG).debug("TTS文本发送成功")
except Exception as e:
logger.bind(tag=TAG).error(f"发送TTS文本失败: {str(e)}")
continue
@@ -317,22 +323,20 @@ class TTSProvider(TTSProviderBase):
async def start_session(self, task_id):
logger.bind(tag=TAG).debug("开始会话~~")
try:
# 会话开始时检测上个会话的监听状态
if (
self._monitor_task is not None
and isinstance(self._monitor_task, Task)
and not self._monitor_task.done()
):
logger.bind(tag=TAG).info(
"检测到未完成的上个会话,关闭监听任务和连接..."
)
# 上个会话处于激活状态时关闭上个连接新建链接
if self.activate_session:
await self.close()
# 设置会话激活标志
self.activate_session = True
# 建立新连接
await self._ensure_connection()
# 启动监听任务
self._monitor_task = asyncio.create_task(self._start_monitor_tts_response())
if self._monitor_task is None or self._monitor_task.done():
logger.bind(tag=TAG).debug("启动监听任务...")
self._monitor_task = asyncio.create_task(self._start_monitor_tts_response())
start_request = {
"header": {
@@ -377,15 +381,7 @@ class TTSProvider(TTSProviderBase):
await self.ws.send(json.dumps(stop_request))
logger.bind(tag=TAG).debug("会话结束请求已发送")
self.last_active_time = time.time()
if self._monitor_task:
try:
await self._monitor_task
except Exception as e:
logger.bind(tag=TAG).error(
f"等待监听任务完成时发生错误: {str(e)}"
)
finally:
self._monitor_task = None
except Exception as e:
logger.bind(tag=TAG).error(f"关闭会话失败: {str(e)}")
# 确保清理资源
@@ -394,6 +390,8 @@ class TTSProvider(TTSProviderBase):
async def close(self):
"""资源清理"""
await super().close()
self.activate_session = False
if self._monitor_task:
try:
self._monitor_task.cancel()
@@ -413,22 +411,27 @@ class TTSProvider(TTSProviderBase):
self.last_active_time = None
async def _start_monitor_tts_response(self):
"""监听TTS响应"""
"""监听TTS响应 - 长期运行"""
try:
session_finished = False # 标记会话是否正常结束
while not self.conn.stop_event.is_set():
try:
msg = await self.ws.recv()
self.last_active_time = time.time()
# 检查客户端是否中止
if self.conn.client_abort:
logger.bind(tag=TAG).info("收到打断信息,终止监听TTS响应")
break
if isinstance(msg, str): # 文本控制消息
try:
data = json.loads(msg)
header = data.get("header", {})
event_name = header.get("name")
task_id = header.get("task_id")
# 只处理当前活跃会话的响应
if task_id and self.task_id != task_id:
if event_name in ["SynthesisCompleted", "TaskFailed"]:
logger.bind(tag=TAG).debug(f"收到残余下行结束响应重置会话状态~~")
self.activate_session = False
continue
if event_name == "SynthesisStarted":
logger.bind(tag=TAG).debug("TTS合成已启动")
self.tts_audio_queue.put(
@@ -436,19 +439,19 @@ class TTSProvider(TTSProviderBase):
)
elif event_name == "SentenceEnd":
# 发送缓存的数据
if self.conn.tts_MessageText:
tts_text = self.get_tts_text(self.conn.sentence_id)
if tts_text:
logger.bind(tag=TAG).info(
f"句子语音生成成功: {self.conn.tts_MessageText}"
f"句子语音生成成功: {tts_text}"
)
self.tts_audio_queue.put(
(SentenceType.FIRST, [], self.conn.tts_MessageText)
(SentenceType.FIRST, [], tts_text)
)
self.conn.tts_MessageText = None
self.clear_tts_text(self.conn.sentence_id)
elif event_name == "SynthesisCompleted":
logger.bind(tag=TAG).debug(f"会话结束~~")
self.activate_session = False
self._process_before_stop_play_files()
session_finished = True
break
except json.JSONDecodeError:
logger.bind(tag=TAG).warning("收到无效的JSON消息")
# 二进制消息(音频数据)
@@ -462,8 +465,8 @@ class TTSProvider(TTSProviderBase):
f"处理TTS响应时出错: {e}\n{traceback.format_exc()}"
)
break
# 仅在连接异常时关闭
if not session_finished and self.ws:
# 连接异常时关闭WebSocket
if self.ws:
try:
await self.ws.close()
except:
@@ -471,6 +474,7 @@ class TTSProvider(TTSProviderBase):
self.ws = None
# 监听任务退出时清理引用
finally:
self.activate_session = False
self._monitor_task = None
def audio_to_opus_data_stream(
+56 -14
View File
@@ -5,6 +5,7 @@ import queue
import asyncio
import threading
import traceback
import concurrent.futures
from core.utils import p3
from datetime import datetime
@@ -42,6 +43,8 @@ class TTSProviderBase(ABC):
self.tts_audio_first_sentence = True
self.before_stop_play_files = []
self.report_on_last = False
# sentence_id 到文本的映射,用于流式TTS获取正确的字幕文本
self._sentence_text_map = {}
self.tts_text_buff = []
self.punctuations = (
@@ -80,7 +83,7 @@ class TTSProviderBase(ABC):
def handle_opus(self, opus_data: bytes):
logger.bind(tag=TAG).debug(f"推送数据到队列里面帧数~~ {len(opus_data)}")
self.tts_audio_queue.put((SentenceType.MIDDLE, opus_data, None))
self.tts_audio_queue.put((SentenceType.MIDDLE, opus_data, None, getattr(self, 'current_sentence_id', None)))
def handle_audio_file(self, file_audio: bytes, text):
self.before_stop_play_files.append((file_audio, text))
@@ -94,7 +97,7 @@ class TTSProviderBase(ABC):
try:
audio_bytes = asyncio.run(self.text_to_speak(text, None))
if audio_bytes:
self.tts_audio_queue.put((SentenceType.FIRST, None, text))
self.tts_audio_queue.put((SentenceType.FIRST, None, text, getattr(self, 'current_sentence_id', None)))
audio_bytes_to_data_stream(
audio_bytes,
file_type=self.audio_file_type,
@@ -143,7 +146,7 @@ class TTSProviderBase(ABC):
logger.bind(tag=TAG).error(
f"语音生成失败: {text},请检查网络或服务是否正常"
)
self.tts_audio_queue.put((SentenceType.FIRST, None, text))
self.tts_audio_queue.put((SentenceType.FIRST, None, text, getattr(self, 'current_sentence_id', None)))
self._process_audio_file_stream(tmp_file, callback=opus_handler)
except Exception as e:
logger.bind(tag=TAG).error(f"Failed to generate TTS file: {e}")
@@ -277,19 +280,54 @@ class TTSProviderBase(ABC):
)
self.audio_play_priority_thread.start()
def store_tts_text(self, sentence_id, text):
"""存储指定 sentence_id 对应的文本,用于流式TTS获取正确的字幕文本
Args:
sentence_id: 会话ID
text: 要存储的文本
"""
if sentence_id and text:
self._sentence_text_map[sentence_id] = text
# 只保留最近 5 个,防止内存泄漏
if len(self._sentence_text_map) > 5:
oldest = next(iter(self._sentence_text_map))
del self._sentence_text_map[oldest]
def get_tts_text(self, sentence_id):
"""获取指定 sentence_id 对应的文本
Args:
sentence_id: 会话ID
Returns:
str: 对应的文本如果不存在返回 None
"""
return self._sentence_text_map.get(sentence_id)
def clear_tts_text(self, sentence_id):
"""清除指定 sentence_id 的文本
Args:
sentence_id: 会话ID
"""
if sentence_id in self._sentence_text_map:
del self._sentence_text_map[sentence_id]
# 这里默认是非流式的处理方式
# 流式处理方式请在子类中重写
def tts_text_priority_thread(self):
while not self.conn.stop_event.is_set():
try:
message = self.tts_text_queue.get(timeout=1)
if message.sentence_type == SentenceType.FIRST:
self.conn.client_abort = False
if self.conn.client_abort:
logger.bind(tag=TAG).info("收到打断信息,终止TTS文本处理线程")
continue
# 过滤旧消息:检查sentence_id是否匹配
if message.sentence_id != self.conn.sentence_id:
continue
if message.sentence_type == SentenceType.FIRST:
# 初始化参数
self.current_sentence_id = message.sentence_id
self.tts_stop_request = False
self.processed_chars = 0
self.tts_text_buff = []
@@ -310,7 +348,7 @@ class TTSProviderBase(ABC):
if message.sentence_type == SentenceType.LAST:
self._process_remaining_text_stream(opus_handler=self.handle_opus)
self.tts_audio_queue.put(
(message.sentence_type, [], message.content_detail)
(message.sentence_type, [], message.content_detail, message.sentence_id)
)
except queue.Empty:
@@ -329,9 +367,12 @@ class TTSProviderBase(ABC):
text = None
try:
try:
sentence_type, audio_datas, text = self.tts_audio_queue.get(
timeout=0.1
)
item = self.tts_audio_queue.get(timeout=0.1)
if len(item) == 4:
sentence_type, audio_datas, text, sentence_id = item
else:
sentence_type, audio_datas, text = item
sentence_id = None
except queue.Empty:
if self.conn.stop_event.is_set():
break
@@ -366,10 +407,10 @@ class TTSProviderBase(ABC):
# 发送音频
future = asyncio.run_coroutine_threadsafe(
sendAudioMessage(self.conn, sentence_type, audio_datas, text),
sendAudioMessage(self.conn, sentence_type, audio_datas, text, sentence_id),
self.conn.loop,
)
future.result(timeout=self.tts_timeout)
future.result()
# 记录输出和报告
if self.conn.max_output_size > 0 and text:
@@ -386,6 +427,7 @@ class TTSProviderBase(ABC):
async def close(self):
"""资源清理方法"""
self._sentence_text_map.clear()
if hasattr(self, "ws") and self.ws:
await self.ws.close()
@@ -454,9 +496,9 @@ class TTSProviderBase(ABC):
def _process_before_stop_play_files(self):
for audio_datas, text in self.before_stop_play_files:
self.tts_audio_queue.put((SentenceType.MIDDLE, audio_datas, text))
self.tts_audio_queue.put((SentenceType.MIDDLE, audio_datas, text, getattr(self, 'current_sentence_id', None)))
self.before_stop_play_files.clear()
self.tts_audio_queue.put((SentenceType.LAST, [], None))
self.tts_audio_queue.put((SentenceType.LAST, [], None, getattr(self, 'current_sentence_id', None)))
def _process_remaining_text_stream(
self, opus_handler: Callable[[bytes], None] = None
@@ -221,7 +221,7 @@ class TTSProvider(TTSProviderBase):
try:
if self.ws:
if self.enable_ws_reuse:
logger.bind(tag=TAG).info(f"使用已有链接...")
logger.bind(tag=TAG).debug(f"使用已有链接...")
return self.ws
else:
try:
@@ -272,12 +272,6 @@ class TTSProvider(TTSProviderBase):
while not self.conn.stop_event.is_set():
try:
message = self.tts_text_queue.get(timeout=1)
logger.bind(tag=TAG).debug(
f"收到TTS任务|{message.sentence_type.name} {message.content_type.name} | 会话ID: {self.conn.sentence_id}"
)
if message.sentence_type == SentenceType.FIRST:
self.conn.client_abort = False
if self.conn.client_abort:
try:
@@ -297,6 +291,14 @@ class TTSProvider(TTSProviderBase):
logger.bind(tag=TAG).error(f"取消TTS会话失败: {str(e)}")
continue
# 过滤旧消息:检查sentence_id是否匹配
if message.sentence_id != self.conn.sentence_id:
continue
logger.bind(tag=TAG).debug(
f"收到TTS任务|{message.sentence_type.name} {message.content_type.name} | 会话ID: {message.sentence_id}"
)
if message.sentence_type == SentenceType.FIRST:
# 初始化参数
try:
@@ -327,7 +329,6 @@ class TTSProvider(TTSProviderBase):
loop=self.conn.loop,
)
future.result(timeout=self.tts_timeout)
logger.bind(tag=TAG).debug("TTS文本发送成功")
except Exception as e:
logger.bind(tag=TAG).error(f"发送TTS文本失败: {str(e)}")
continue
@@ -387,15 +388,8 @@ class TTSProvider(TTSProviderBase):
async def start_session(self, session_id):
logger.bind(tag=TAG).debug(f"开始会话~~{session_id}")
try:
# 等待上一个会话结束,最多等待3次
for _ in range(3):
if not self.activate_session:
break
logger.bind(tag=TAG).debug(f"等待上一个会话结束...")
await asyncio.sleep(0.1)
else:
# 等待超时,强制清除连接状态
logger.bind(tag=TAG).debug("等待上一个会话超时,清除连接状态...")
# 上个会话处于激活状态时关闭上个连接新建链接
if self.activate_session:
await self.close()
# 设置会话激活标志
@@ -468,6 +462,7 @@ class TTSProvider(TTSProviderBase):
async def close(self):
"""资源清理方法"""
await super().close()
self.activate_session = False
# 取消监听任务
if self._monitor_task:
@@ -525,14 +520,16 @@ class TTSProvider(TTSProviderBase):
and res.header.message_type == AUDIO_ONLY_RESPONSE
):
# 处理seed-tts-2.0文本字幕
if self.resource_type and self.conn.tts_MessageText:
logger.bind(tag=TAG).info(
f"句子语音生成成功: {self.conn.tts_MessageText}"
)
self.tts_audio_queue.put(
(SentenceType.FIRST, [], self.conn.tts_MessageText)
)
self.conn.tts_MessageText = None
if self.resource_type:
tts_text = self.get_tts_text(self.conn.sentence_id)
if tts_text:
logger.bind(tag=TAG).info(
f"句子语音生成成功: {tts_text}"
)
self.tts_audio_queue.put(
(SentenceType.FIRST, [], tts_text)
)
self.clear_tts_text(self.conn.sentence_id)
self.wav_to_opus_data_audio_raw_stream(res.payload, callback=self.handle_opus)
elif not self.resource_type and res.optional.event == EVENT_TTSSentenceEnd:
logger.bind(tag=TAG).info(f"句子语音生成成功:{self.tts_text}")
@@ -117,6 +117,7 @@ class TTSProvider(TTSProviderBase):
# WebSocket配置
self.ws = None
self._monitor_task = None
self.activate_session = False
# 序列号管理
self.text_seq = 0
@@ -128,7 +129,7 @@ class TTSProvider(TTSProviderBase):
async def _ensure_connection(self):
"""确保WebSocket连接可用"""
try:
logger.bind(tag=TAG).info("开始建立新连接...")
logger.bind(tag=TAG).debug("开始建立新连接...")
# 生成认证URL
auth_url = XunfeiWSAuth.create_auth_url(
@@ -141,7 +142,7 @@ class TTSProvider(TTSProviderBase):
ping_timeout=10,
close_timeout=10,
)
logger.bind(tag=TAG).info("WebSocket连接建立成功")
logger.bind(tag=TAG).debug("WebSocket连接建立成功")
return self.ws
except Exception as e:
logger.bind(tag=TAG).error(f"建立连接失败: {str(e)}")
@@ -153,35 +154,40 @@ class TTSProvider(TTSProviderBase):
while not self.conn.stop_event.is_set():
try:
message = self.tts_text_queue.get(timeout=1)
if self.conn.client_abort:
logger.bind(tag=TAG).info("收到打断信息,终止TTS文本处理线程")
continue
# 过滤旧消息:检查sentence_id是否匹配
if message.sentence_id != self.conn.sentence_id:
continue
logger.bind(tag=TAG).debug(
f"收到TTS任务|{message.sentence_type.name} {message.content_type.name} | 会话ID: {self.conn.sentence_id}"
f"收到TTS任务|{message.sentence_type.name} {message.content_type.name} | 会话ID: {message.sentence_id}"
)
if message.sentence_type == SentenceType.FIRST:
# 重置序列号
self.text_seq = 0
self.conn.client_abort = False
# 增加序列号
self.text_seq += 1
if self.conn.client_abort:
logger.bind(tag=TAG).info("收到打断信息,终止TTS文本处理线程")
continue
if message.sentence_type == SentenceType.FIRST:
# 初始化参数
try:
if not getattr(self.conn, "sentence_id", None):
self.conn.sentence_id = uuid.uuid4().hex
logger.bind(tag=TAG).info(f"自动生成新的 会话ID: {self.conn.sentence_id}")
logger.bind(tag=TAG).debug(f"自动生成新的 会话ID: {self.conn.sentence_id}")
logger.bind(tag=TAG).info("开始启动TTS会话...")
logger.bind(tag=TAG).debug("开始启动TTS会话...")
future = asyncio.run_coroutine_threadsafe(
self.start_session(self.conn.sentence_id),
loop=self.conn.loop,
)
future.result(timeout=self.tts_timeout)
self.before_stop_play_files.clear()
logger.bind(tag=TAG).info("TTS会话启动成功")
logger.bind(tag=TAG).debug("TTS会话启动成功")
except Exception as e:
logger.bind(tag=TAG).error(f"启动TTS会话失败: {str(e)}")
@@ -199,7 +205,6 @@ class TTSProvider(TTSProviderBase):
loop=self.conn.loop,
)
future.result(timeout=self.tts_timeout)
logger.bind(tag=TAG).debug("TTS文本发送成功")
except Exception as e:
logger.bind(tag=TAG).error(f"发送TTS文本失败: {str(e)}")
# 不使用continue,确保后续处理不被中断
@@ -216,7 +221,7 @@ class TTSProvider(TTSProviderBase):
# 处理会话结束
if message.sentence_type == SentenceType.LAST:
try:
logger.bind(tag=TAG).info("开始结束TTS会话...")
logger.bind(tag=TAG).debug("开始结束TTS会话...")
asyncio.run_coroutine_threadsafe(
self.finish_session(self.conn.sentence_id),
loop=self.conn.loop,
@@ -257,30 +262,28 @@ class TTSProvider(TTSProviderBase):
raise
async def start_session(self, session_id):
logger.bind(tag=TAG).info(f"开始会话~~{session_id}")
logger.bind(tag=TAG).debug(f"开始会话~~{session_id}")
try:
# 会话开始时检测上个会话的监听状态
if (
self._monitor_task is not None
and isinstance(self._monitor_task, Task)
and not self._monitor_task.done()
):
logger.bind(tag=TAG).info(
"检测到未完成的上个会话,关闭监听任务和连接..."
)
# 上个会话处于激活状态时关闭上个连接新建链接
if self.activate_session:
await self.close()
# 设置会话激活标志
self.activate_session = True
# 建立新连接
await self._ensure_connection()
# 启动监听任务
self._monitor_task = asyncio.create_task(self._start_monitor_tts_response())
if self._monitor_task is None or self._monitor_task.done():
logger.bind(tag=TAG).debug("启动监听任务...")
self._monitor_task = asyncio.create_task(self._start_monitor_tts_response())
# 发送会话启动请求
start_request = self._build_base_request(status=0)
await self.ws.send(json.dumps(start_request))
logger.bind(tag=TAG).info("会话启动请求已发送")
logger.bind(tag=TAG).debug("会话启动请求已发送")
except Exception as e:
logger.bind(tag=TAG).error(f"启动会话失败: {str(e)}")
# 确保清理资源
@@ -288,13 +291,13 @@ class TTSProvider(TTSProviderBase):
raise
async def finish_session(self, session_id):
logger.bind(tag=TAG).info(f"关闭会话~~{session_id}")
logger.bind(tag=TAG).debug(f"关闭会话~~{session_id}")
try:
if self.ws:
# 发送会话结束请求
stop_request = self._build_base_request(status=2)
await self.ws.send(json.dumps(stop_request))
logger.bind(tag=TAG).info("会话结束请求已发送")
logger.bind(tag=TAG).debug("会话结束请求已发送")
if self._monitor_task:
try:
@@ -310,6 +313,8 @@ class TTSProvider(TTSProviderBase):
async def close(self):
"""资源清理"""
await super().close()
self.activate_session = False
if self._monitor_task:
try:
self._monitor_task.cancel()
@@ -358,17 +363,19 @@ class TTSProvider(TTSProviderBase):
)
elif status == 2:
logger.bind(tag=TAG).debug("收到结束状态的音频数据,TTS合成完成")
self.activate_session = False
self._process_before_stop_play_files()
break
else:
if self.conn.tts_MessageText:
tts_text = self.get_tts_text(self.conn.sentence_id)
if tts_text:
logger.bind(tag=TAG).info(
f"句子语音生成成功: {self.conn.tts_MessageText}"
f"句子语音生成成功: {tts_text}"
)
self.tts_audio_queue.put(
(SentenceType.FIRST, [], self.conn.tts_MessageText)
(SentenceType.FIRST, [], tts_text)
)
self.conn.tts_MessageText = None
self.clear_tts_text(self.conn.sentence_id)
try:
audio_bytes = base64.b64decode(audio_data)
self.opus_encoder.encode_pcm_to_opus_stream(
@@ -405,6 +412,7 @@ class TTSProvider(TTSProviderBase):
self.ws = None
# 监听任务退出时清理引用
finally:
self.activate_session = False
self._monitor_task = None
def to_tts(self, text: str) -> list:
@@ -107,12 +107,12 @@ class VADProvider(VADProviderBase):
# 如果之前有声音,但本次没有声音,且与上次有声音的时间差已经超过了静默阈值,则认为已经说完一句话
if conn.client_have_voice and not client_have_voice:
stop_duration = time.time() * 1000 - conn.last_activity_time
stop_duration = time.time() * 1000 - conn.vad_last_voice_time
if stop_duration >= self.silence_threshold_ms:
conn.client_voice_stop = True
if client_have_voice:
conn.client_have_voice = True
conn.last_activity_time = time.time() * 1000
conn.vad_last_voice_time = time.time() * 1000
return client_have_voice
except opuslib_next.OpusError as e:
+5 -5
View File
@@ -130,17 +130,17 @@ class MarkdownCleaner:
"""
主入口方法依序执行所有正则移除或替换 Markdown 元素
"""
# 检查文本是否全为英文和基本标点符号
if text and all((c.isascii() or c.isspace() or c in punctuation_set) for c in text):
# 保留原始空格,直接返回
return text
for regex, replacement in MarkdownCleaner.REGEXES:
text = regex.sub(replacement, text)
# 去除emoji表情
text = check_emoji(text)
# 检查文本是否全为英文和基本标点符号
if text and all((c.isascii() or c.isspace() or c in punctuation_set) for c in text):
# 保留原始空格,直接返回
return text
return text.strip()
def convert_percentage_to_range(percentage, min_val, max_val, base_val=None):