对话、重新生成、评价完成
This commit is contained in:
@@ -81,6 +81,8 @@ CREATE TABLE `tb_ai_conversation` (
|
|||||||
`last_message_time` TIMESTAMP NULL DEFAULT NULL COMMENT '最后消息时间',
|
`last_message_time` TIMESTAMP NULL DEFAULT NULL COMMENT '最后消息时间',
|
||||||
`create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
`create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
`update_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
`update_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
`delete_time` TIMESTAMP NULL DEFAULT NULL COMMENT '删除时间',
|
||||||
|
`deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否删除',
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
KEY `idx_user_createtime` (`user_id`, `create_time` DESC),
|
KEY `idx_user_createtime` (`user_id`, `create_time` DESC),
|
||||||
KEY `idx_user_favorite` (`user_id`, `is_favorite`),
|
KEY `idx_user_favorite` (`user_id`, `is_favorite`),
|
||||||
@@ -96,6 +98,7 @@ CREATE TABLE `tb_ai_message` (
|
|||||||
`id` VARCHAR(50) NOT NULL COMMENT '消息ID',
|
`id` VARCHAR(50) NOT NULL COMMENT '消息ID',
|
||||||
`conversation_id` VARCHAR(50) NOT NULL COMMENT '会话ID',
|
`conversation_id` VARCHAR(50) NOT NULL COMMENT '会话ID',
|
||||||
`user_id` VARCHAR(50) NOT NULL COMMENT '用户ID',
|
`user_id` VARCHAR(50) NOT NULL COMMENT '用户ID',
|
||||||
|
`agent_id` VARCHAR(50) DEFAULT NULL COMMENT '智能体ID',
|
||||||
`role` VARCHAR(20) NOT NULL COMMENT '角色(user用户 assistant助手 system系统)',
|
`role` VARCHAR(20) NOT NULL COMMENT '角色(user用户 assistant助手 system系统)',
|
||||||
`content` LONGTEXT NOT NULL COMMENT '消息内容',
|
`content` LONGTEXT NOT NULL COMMENT '消息内容',
|
||||||
`file_ids` VARCHAR(500) DEFAULT NULL COMMENT '关联文件ID(JSON数组)',
|
`file_ids` VARCHAR(500) DEFAULT NULL COMMENT '关联文件ID(JSON数组)',
|
||||||
@@ -103,12 +106,19 @@ CREATE TABLE `tb_ai_message` (
|
|||||||
`knowledge_refs` TEXT DEFAULT NULL COMMENT '知识库引用详情(JSON数组,包含title/snippet/score)',
|
`knowledge_refs` TEXT DEFAULT NULL COMMENT '知识库引用详情(JSON数组,包含title/snippet/score)',
|
||||||
`token_count` INT(11) DEFAULT 0 COMMENT 'Token数量',
|
`token_count` INT(11) DEFAULT 0 COMMENT 'Token数量',
|
||||||
`dify_message_id` VARCHAR(100) DEFAULT NULL COMMENT 'Dify消息ID',
|
`dify_message_id` VARCHAR(100) DEFAULT NULL COMMENT 'Dify消息ID',
|
||||||
|
`rating` INT(4) DEFAULT NULL COMMENT '评分(1好评 -1差评 0取消评价)',
|
||||||
|
`feedback` VARCHAR(1000) DEFAULT NULL COMMENT '反馈内容',
|
||||||
`create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
`create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`update_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
`delete_time` TIMESTAMP NULL DEFAULT NULL COMMENT '删除时间',
|
||||||
|
`deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否删除',
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
KEY `idx_conversation_createtime` (`conversation_id`, `create_time` ASC),
|
KEY `idx_conversation_createtime` (`conversation_id`, `create_time` ASC),
|
||||||
KEY `idx_user` (`user_id`),
|
KEY `idx_user` (`user_id`),
|
||||||
|
KEY `idx_agent` (`agent_id`),
|
||||||
KEY `idx_role` (`role`),
|
KEY `idx_role` (`role`),
|
||||||
KEY `idx_create_time` (`create_time`),
|
KEY `idx_create_time` (`create_time`),
|
||||||
|
KEY `idx_deleted` (`deleted`),
|
||||||
FULLTEXT KEY `ft_content` (`content`) WITH PARSER ngram
|
FULLTEXT KEY `ft_content` (`content`) WITH PARSER ngram
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='对话消息表(支持全文检索)';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='对话消息表(支持全文检索)';
|
||||||
|
|
||||||
@@ -132,13 +142,16 @@ CREATE TABLE `tb_ai_upload_file` (
|
|||||||
`error_message` VARCHAR(500) DEFAULT NULL COMMENT '错误信息',
|
`error_message` VARCHAR(500) DEFAULT NULL COMMENT '错误信息',
|
||||||
`create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '上传时间',
|
`create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '上传时间',
|
||||||
`update_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
`update_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
`delete_time` TIMESTAMP NULL DEFAULT NULL COMMENT '删除时间',
|
||||||
|
`deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否删除',
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
KEY `idx_user` (`user_id`),
|
KEY `idx_user` (`user_id`),
|
||||||
KEY `idx_knowledge` (`knowledge_id`),
|
KEY `idx_knowledge` (`knowledge_id`),
|
||||||
KEY `idx_conversation` (`conversation_id`),
|
KEY `idx_conversation` (`conversation_id`),
|
||||||
KEY `idx_dify_document` (`dify_document_id`),
|
KEY `idx_dify_document` (`dify_document_id`),
|
||||||
KEY `idx_status` (`status`),
|
KEY `idx_status` (`status`),
|
||||||
KEY `idx_create_time` (`create_time`)
|
KEY `idx_create_time` (`create_time`),
|
||||||
|
KEY `idx_deleted` (`deleted`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='上传文件表';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='上传文件表';
|
||||||
|
|
||||||
-- AI使用统计表
|
-- AI使用统计表
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ import com.fasterxml.jackson.databind.JsonNode;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import okhttp3.*;
|
import okhttp3.*;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.xyzh.ai.client.dto.*;
|
import org.xyzh.ai.client.dto.*;
|
||||||
@@ -26,10 +27,11 @@ import java.util.concurrent.TimeUnit;
|
|||||||
* @copyright xyzh
|
* @copyright xyzh
|
||||||
* @since 2025-11-04
|
* @since 2025-11-04
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
|
||||||
@Component
|
@Component
|
||||||
public class DifyApiClient {
|
public class DifyApiClient {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(DifyApiClient.class);
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private DifyConfig difyConfig;
|
private DifyConfig difyConfig;
|
||||||
|
|
||||||
@@ -55,7 +57,7 @@ public class DifyApiClient {
|
|||||||
.retryOnConnectionFailure(false) // 流式不重试
|
.retryOnConnectionFailure(false) // 流式不重试
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
log.info("DifyApiClient初始化完成,API地址: {}", difyConfig.getApiBaseUrl());
|
logger.info("DifyApiClient初始化完成,API地址: {}", difyConfig.getApiBaseUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===================== 知识库管理 API =====================
|
// ===================== 知识库管理 API =====================
|
||||||
@@ -79,14 +81,14 @@ public class DifyApiClient {
|
|||||||
String responseBody = response.body() != null ? response.body().string() : "";
|
String responseBody = response.body() != null ? response.body().string() : "";
|
||||||
|
|
||||||
if (!response.isSuccessful()) {
|
if (!response.isSuccessful()) {
|
||||||
log.error("创建知识库失败: {} - {}", response.code(), responseBody);
|
logger.error("创建知识库失败: {} - {}", response.code(), responseBody);
|
||||||
throw new DifyException("创建知识库失败: " + responseBody);
|
throw new DifyException("创建知识库失败: " + responseBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
return objectMapper.readValue(responseBody, DatasetCreateResponse.class);
|
return objectMapper.readValue(responseBody, DatasetCreateResponse.class);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error("创建知识库异常", e);
|
logger.error("创建知识库异常", e);
|
||||||
throw new DifyException("创建知识库异常: " + e.getMessage(), e);
|
throw new DifyException("创建知识库异常: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,14 +110,14 @@ public class DifyApiClient {
|
|||||||
String responseBody = response.body() != null ? response.body().string() : "";
|
String responseBody = response.body() != null ? response.body().string() : "";
|
||||||
|
|
||||||
if (!response.isSuccessful()) {
|
if (!response.isSuccessful()) {
|
||||||
log.error("查询知识库列表失败: {} - {}", response.code(), responseBody);
|
logger.error("查询知识库列表失败: {} - {}", response.code(), responseBody);
|
||||||
throw new DifyException("查询知识库列表失败: " + responseBody);
|
throw new DifyException("查询知识库列表失败: " + responseBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
return objectMapper.readValue(responseBody, DatasetListResponse.class);
|
return objectMapper.readValue(responseBody, DatasetListResponse.class);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error("查询知识库列表异常", e);
|
logger.error("查询知识库列表异常", e);
|
||||||
throw new DifyException("查询知识库列表异常: " + e.getMessage(), e);
|
throw new DifyException("查询知识库列表异常: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -137,14 +139,14 @@ public class DifyApiClient {
|
|||||||
String responseBody = response.body() != null ? response.body().string() : "";
|
String responseBody = response.body() != null ? response.body().string() : "";
|
||||||
|
|
||||||
if (!response.isSuccessful()) {
|
if (!response.isSuccessful()) {
|
||||||
log.error("查询知识库详情失败: {} - {}", response.code(), responseBody);
|
logger.error("查询知识库详情失败: {} - {}", response.code(), responseBody);
|
||||||
throw new DifyException("查询知识库详情失败: " + responseBody);
|
throw new DifyException("查询知识库详情失败: " + responseBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
return objectMapper.readValue(responseBody, DatasetDetailResponse.class);
|
return objectMapper.readValue(responseBody, DatasetDetailResponse.class);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error("查询知识库详情异常", e);
|
logger.error("查询知识库详情异常", e);
|
||||||
throw new DifyException("查询知识库详情异常: " + e.getMessage(), e);
|
throw new DifyException("查询知识库详情异常: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -168,13 +170,13 @@ public class DifyApiClient {
|
|||||||
try (Response response = httpClient.newCall(httpRequest).execute()) {
|
try (Response response = httpClient.newCall(httpRequest).execute()) {
|
||||||
if (!response.isSuccessful()) {
|
if (!response.isSuccessful()) {
|
||||||
String responseBody = response.body() != null ? response.body().string() : "";
|
String responseBody = response.body() != null ? response.body().string() : "";
|
||||||
log.error("更新知识库失败: {} - {}", response.code(), responseBody);
|
logger.error("更新知识库失败: {} - {}", response.code(), responseBody);
|
||||||
throw new DifyException("更新知识库失败: " + responseBody);
|
throw new DifyException("更新知识库失败: " + responseBody);
|
||||||
}
|
}
|
||||||
log.info("知识库更新成功: {}", datasetId);
|
logger.info("知识库更新成功: {}", datasetId);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error("更新知识库异常", e);
|
logger.error("更新知识库异常", e);
|
||||||
throw new DifyException("更新知识库异常: " + e.getMessage(), e);
|
throw new DifyException("更新知识库异常: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -194,13 +196,13 @@ public class DifyApiClient {
|
|||||||
try (Response response = httpClient.newCall(httpRequest).execute()) {
|
try (Response response = httpClient.newCall(httpRequest).execute()) {
|
||||||
if (!response.isSuccessful()) {
|
if (!response.isSuccessful()) {
|
||||||
String responseBody = response.body() != null ? response.body().string() : "";
|
String responseBody = response.body() != null ? response.body().string() : "";
|
||||||
log.error("删除知识库失败: {} - {}", response.code(), responseBody);
|
logger.error("删除知识库失败: {} - {}", response.code(), responseBody);
|
||||||
throw new DifyException("删除知识库失败: " + responseBody);
|
throw new DifyException("删除知识库失败: " + responseBody);
|
||||||
}
|
}
|
||||||
log.info("知识库删除成功: {}", datasetId);
|
logger.info("知识库删除成功: {}", datasetId);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error("删除知识库异常", e);
|
logger.error("删除知识库异常", e);
|
||||||
throw new DifyException("删除知识库异常: " + e.getMessage(), e);
|
throw new DifyException("删除知识库异常: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -246,14 +248,14 @@ public class DifyApiClient {
|
|||||||
String responseBody = response.body() != null ? response.body().string() : "";
|
String responseBody = response.body() != null ? response.body().string() : "";
|
||||||
|
|
||||||
if (!response.isSuccessful()) {
|
if (!response.isSuccessful()) {
|
||||||
log.error("上传文档失败: {} - {}", response.code(), responseBody);
|
logger.error("上传文档失败: {} - {}", response.code(), responseBody);
|
||||||
throw new DifyException("上传文档失败: " + responseBody);
|
throw new DifyException("上传文档失败: " + responseBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
return objectMapper.readValue(responseBody, DocumentUploadResponse.class);
|
return objectMapper.readValue(responseBody, DocumentUploadResponse.class);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error("上传文档异常", e);
|
logger.error("上传文档异常", e);
|
||||||
throw new DifyException("上传文档异常: " + e.getMessage(), e);
|
throw new DifyException("上传文档异常: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -275,14 +277,14 @@ public class DifyApiClient {
|
|||||||
String responseBody = response.body() != null ? response.body().string() : "";
|
String responseBody = response.body() != null ? response.body().string() : "";
|
||||||
|
|
||||||
if (!response.isSuccessful()) {
|
if (!response.isSuccessful()) {
|
||||||
log.error("查询文档状态失败: {} - {}", response.code(), responseBody);
|
logger.error("查询文档状态失败: {} - {}", response.code(), responseBody);
|
||||||
throw new DifyException("查询文档状态失败: " + responseBody);
|
throw new DifyException("查询文档状态失败: " + responseBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
return objectMapper.readValue(responseBody, DocumentStatusResponse.class);
|
return objectMapper.readValue(responseBody, DocumentStatusResponse.class);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error("查询文档状态异常", e);
|
logger.error("查询文档状态异常", e);
|
||||||
throw new DifyException("查询文档状态异常: " + e.getMessage(), e);
|
throw new DifyException("查询文档状态异常: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -304,14 +306,14 @@ public class DifyApiClient {
|
|||||||
String responseBody = response.body() != null ? response.body().string() : "";
|
String responseBody = response.body() != null ? response.body().string() : "";
|
||||||
|
|
||||||
if (!response.isSuccessful()) {
|
if (!response.isSuccessful()) {
|
||||||
log.error("查询文档列表失败: {} - {}", response.code(), responseBody);
|
logger.error("查询文档列表失败: {} - {}", response.code(), responseBody);
|
||||||
throw new DifyException("查询文档列表失败: " + responseBody);
|
throw new DifyException("查询文档列表失败: " + responseBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
return objectMapper.readValue(responseBody, DocumentListResponse.class);
|
return objectMapper.readValue(responseBody, DocumentListResponse.class);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error("查询文档列表异常", e);
|
logger.error("查询文档列表异常", e);
|
||||||
throw new DifyException("查询文档列表异常: " + e.getMessage(), e);
|
throw new DifyException("查询文档列表异常: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -332,13 +334,13 @@ public class DifyApiClient {
|
|||||||
try (Response response = httpClient.newCall(httpRequest).execute()) {
|
try (Response response = httpClient.newCall(httpRequest).execute()) {
|
||||||
if (!response.isSuccessful()) {
|
if (!response.isSuccessful()) {
|
||||||
String responseBody = response.body() != null ? response.body().string() : "";
|
String responseBody = response.body() != null ? response.body().string() : "";
|
||||||
log.error("删除文档失败: {} - {}", response.code(), responseBody);
|
logger.error("删除文档失败: {} - {}", response.code(), responseBody);
|
||||||
throw new DifyException("删除文档失败: " + responseBody);
|
throw new DifyException("删除文档失败: " + responseBody);
|
||||||
}
|
}
|
||||||
log.info("文档删除成功: {}", documentId);
|
logger.info("文档删除成功: {}", documentId);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error("删除文档异常", e);
|
logger.error("删除文档异常", e);
|
||||||
throw new DifyException("删除文档异常: " + e.getMessage(), e);
|
throw new DifyException("删除文档异常: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -364,14 +366,14 @@ public class DifyApiClient {
|
|||||||
String responseBody = response.body() != null ? response.body().string() : "";
|
String responseBody = response.body() != null ? response.body().string() : "";
|
||||||
|
|
||||||
if (!response.isSuccessful()) {
|
if (!response.isSuccessful()) {
|
||||||
log.error("知识库检索失败: {} - {}", response.code(), responseBody);
|
logger.error("知识库检索失败: {} - {}", response.code(), responseBody);
|
||||||
throw new DifyException("知识库检索失败: " + responseBody);
|
throw new DifyException("知识库检索失败: " + responseBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
return objectMapper.readValue(responseBody, RetrievalResponse.class);
|
return objectMapper.readValue(responseBody, RetrievalResponse.class);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error("知识库检索异常", e);
|
logger.error("知识库检索异常", e);
|
||||||
throw new DifyException("知识库检索异常: " + e.getMessage(), e);
|
throw new DifyException("知识库检索异常: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -423,6 +425,9 @@ public class DifyApiClient {
|
|||||||
JsonNode jsonNode = objectMapper.readTree(data);
|
JsonNode jsonNode = objectMapper.readTree(data);
|
||||||
String event = jsonNode.has("event") ? jsonNode.get("event").asText() : "";
|
String event = jsonNode.has("event") ? jsonNode.get("event").asText() : "";
|
||||||
|
|
||||||
|
// 转发所有事件到回调(包含完整数据)
|
||||||
|
callback.onEvent(event, data);
|
||||||
|
|
||||||
switch (event) {
|
switch (event) {
|
||||||
case "message":
|
case "message":
|
||||||
case "agent_message":
|
case "agent_message":
|
||||||
@@ -441,25 +446,27 @@ public class DifyApiClient {
|
|||||||
jsonNode.get("message").asText() : "未知错误";
|
jsonNode.get("message").asText() : "未知错误";
|
||||||
callback.onError(new DifyException(errorMsg));
|
callback.onError(new DifyException(errorMsg));
|
||||||
return;
|
return;
|
||||||
|
// 其他事件(workflow_started、node_started、node_finished等)
|
||||||
|
// 已通过onEvent转发,这里不需要额外处理
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("流式响应处理异常", e);
|
logger.error("流式响应处理异常", e);
|
||||||
callback.onError(e);
|
callback.onError(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(Call call, IOException e) {
|
public void onFailure(Call call, IOException e) {
|
||||||
log.error("流式对话请求失败", e);
|
logger.error("流式对话请求失败", e);
|
||||||
callback.onError(e);
|
callback.onError(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("流式对话异常", e);
|
logger.error("流式对话异常", e);
|
||||||
callback.onError(e);
|
callback.onError(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -486,14 +493,14 @@ public class DifyApiClient {
|
|||||||
String responseBody = response.body() != null ? response.body().string() : "";
|
String responseBody = response.body() != null ? response.body().string() : "";
|
||||||
|
|
||||||
if (!response.isSuccessful()) {
|
if (!response.isSuccessful()) {
|
||||||
log.error("阻塞式对话失败: {} - {}", response.code(), responseBody);
|
logger.error("阻塞式对话失败: {} - {}", response.code(), responseBody);
|
||||||
throw new DifyException("阻塞式对话失败: " + responseBody);
|
throw new DifyException("阻塞式对话失败: " + responseBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
return objectMapper.readValue(responseBody, ChatResponse.class);
|
return objectMapper.readValue(responseBody, ChatResponse.class);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error("阻塞式对话异常", e);
|
logger.error("阻塞式对话异常", e);
|
||||||
throw new DifyException("阻塞式对话异常: " + e.getMessage(), e);
|
throw new DifyException("阻塞式对话异常: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -516,17 +523,57 @@ public class DifyApiClient {
|
|||||||
try (Response response = httpClient.newCall(httpRequest).execute()) {
|
try (Response response = httpClient.newCall(httpRequest).execute()) {
|
||||||
if (!response.isSuccessful()) {
|
if (!response.isSuccessful()) {
|
||||||
String responseBody = response.body() != null ? response.body().string() : "";
|
String responseBody = response.body() != null ? response.body().string() : "";
|
||||||
log.error("停止对话失败: {} - {}", response.code(), responseBody);
|
logger.error("停止对话失败: {} - {}", response.code(), responseBody);
|
||||||
throw new DifyException("停止对话失败: " + responseBody);
|
throw new DifyException("停止对话失败: " + responseBody);
|
||||||
}
|
}
|
||||||
log.info("对话停止成功: {}", taskId);
|
logger.info("对话停止成功: {}", taskId);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error("停止对话异常", e);
|
logger.error("停止对话异常", e);
|
||||||
throw new DifyException("停止对话异常: " + e.getMessage(), e);
|
throw new DifyException("停止对话异常: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交消息反馈
|
||||||
|
* @param messageId Dify消息ID
|
||||||
|
* @param rating 评分(like/dislike/null)
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param apiKey API密钥
|
||||||
|
*/
|
||||||
|
public void submitMessageFeedback(String messageId, String rating, String userId, String feedback, String apiKey) {
|
||||||
|
String url = difyConfig.getFullApiUrl("/messages/" + messageId + "/feedbacks");
|
||||||
|
|
||||||
|
try {
|
||||||
|
FeedbackRequest feedbackRequest = new FeedbackRequest(rating, userId, feedback);
|
||||||
|
String jsonBody = objectMapper.writeValueAsString(feedbackRequest);
|
||||||
|
|
||||||
|
Request httpRequest = new Request.Builder()
|
||||||
|
.url(url)
|
||||||
|
.header("Authorization", "Bearer " + getApiKey(apiKey))
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.post(RequestBody.create(jsonBody, MediaType.parse("application/json")))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
try (Response response = httpClient.newCall(httpRequest).execute()) {
|
||||||
|
String responseBody = response.body() != null ? response.body().string() : "";
|
||||||
|
|
||||||
|
if (!response.isSuccessful()) {
|
||||||
|
logger.error("提交消息反馈失败: {} - {}", response.code(), responseBody);
|
||||||
|
throw new DifyException("提交消息反馈失败: " + responseBody);
|
||||||
|
}
|
||||||
|
if (responseBody!="success") {
|
||||||
|
logger.error("提交消息反馈失败: {} - {}", response.code(), responseBody);
|
||||||
|
throw new DifyException("提交消息反馈失败: " + responseBody);
|
||||||
|
}
|
||||||
|
logger.info("消息反馈提交成功: {} - {}", messageId, rating);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.error("提交消息反馈异常", e);
|
||||||
|
throw new DifyException("提交消息反馈异常: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ===================== 对话历史 API =====================
|
// ===================== 对话历史 API =====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -563,14 +610,14 @@ public class DifyApiClient {
|
|||||||
String responseBody = response.body() != null ? response.body().string() : "";
|
String responseBody = response.body() != null ? response.body().string() : "";
|
||||||
|
|
||||||
if (!response.isSuccessful()) {
|
if (!response.isSuccessful()) {
|
||||||
log.error("获取对话历史失败: {} - {}", response.code(), responseBody);
|
logger.error("获取对话历史失败: {} - {}", response.code(), responseBody);
|
||||||
throw new DifyException("获取对话历史失败: " + responseBody);
|
throw new DifyException("获取对话历史失败: " + responseBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
return objectMapper.readValue(responseBody, MessageHistoryResponse.class);
|
return objectMapper.readValue(responseBody, MessageHistoryResponse.class);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error("获取对话历史异常", e);
|
logger.error("获取对话历史异常", e);
|
||||||
throw new DifyException("获取对话历史异常: " + e.getMessage(), e);
|
throw new DifyException("获取对话历史异常: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -605,14 +652,14 @@ public class DifyApiClient {
|
|||||||
String responseBody = response.body() != null ? response.body().string() : "";
|
String responseBody = response.body() != null ? response.body().string() : "";
|
||||||
|
|
||||||
if (!response.isSuccessful()) {
|
if (!response.isSuccessful()) {
|
||||||
log.error("获取对话列表失败: {} - {}", response.code(), responseBody);
|
logger.error("获取对话列表失败: {} - {}", response.code(), responseBody);
|
||||||
throw new DifyException("获取对话列表失败: " + responseBody);
|
throw new DifyException("获取对话列表失败: " + responseBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
return objectMapper.readValue(responseBody, ConversationListResponse.class);
|
return objectMapper.readValue(responseBody, ConversationListResponse.class);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error("获取对话列表异常", e);
|
logger.error("获取对话列表异常", e);
|
||||||
throw new DifyException("获取对话列表异常: " + e.getMessage(), e);
|
throw new DifyException("获取对话列表异常: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -639,14 +686,14 @@ public class DifyApiClient {
|
|||||||
String responseBody = response.body() != null ? response.body().string() : "";
|
String responseBody = response.body() != null ? response.body().string() : "";
|
||||||
|
|
||||||
if (!response.isSuccessful()) {
|
if (!response.isSuccessful()) {
|
||||||
log.error("GET请求失败: {} - {} - {}", url, response.code(), responseBody);
|
logger.error("GET请求失败: {} - {} - {}", url, response.code(), responseBody);
|
||||||
throw new DifyException("GET请求失败[" + response.code() + "]: " + responseBody);
|
throw new DifyException("GET请求失败[" + response.code() + "]: " + responseBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
return responseBody;
|
return responseBody;
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error("GET请求异常: {}", url, e);
|
logger.error("GET请求异常: {}", url, e);
|
||||||
throw new DifyException("GET请求异常: " + e.getMessage(), e);
|
throw new DifyException("GET请求异常: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -676,14 +723,14 @@ public class DifyApiClient {
|
|||||||
String responseBody = response.body() != null ? response.body().string() : "";
|
String responseBody = response.body() != null ? response.body().string() : "";
|
||||||
|
|
||||||
if (!response.isSuccessful()) {
|
if (!response.isSuccessful()) {
|
||||||
log.error("POST请求失败: {} - {} - {}", url, response.code(), responseBody);
|
logger.error("POST请求失败: {} - {} - {}", url, response.code(), responseBody);
|
||||||
throw new DifyException("POST请求失败[" + response.code() + "]: " + responseBody);
|
throw new DifyException("POST请求失败[" + response.code() + "]: " + responseBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
return responseBody;
|
return responseBody;
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error("POST请求异常: {}", url, e);
|
logger.error("POST请求异常: {}", url, e);
|
||||||
throw new DifyException("POST请求异常: " + e.getMessage(), e);
|
throw new DifyException("POST请求异常: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -713,14 +760,14 @@ public class DifyApiClient {
|
|||||||
String responseBody = response.body() != null ? response.body().string() : "";
|
String responseBody = response.body() != null ? response.body().string() : "";
|
||||||
|
|
||||||
if (!response.isSuccessful()) {
|
if (!response.isSuccessful()) {
|
||||||
log.error("PATCH请求失败: {} - {} - {}", url, response.code(), responseBody);
|
logger.error("PATCH请求失败: {} - {} - {}", url, response.code(), responseBody);
|
||||||
throw new DifyException("PATCH请求失败[" + response.code() + "]: " + responseBody);
|
throw new DifyException("PATCH请求失败[" + response.code() + "]: " + responseBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
return responseBody;
|
return responseBody;
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error("PATCH请求异常: {}", url, e);
|
logger.error("PATCH请求异常: {}", url, e);
|
||||||
throw new DifyException("PATCH请求异常: " + e.getMessage(), e);
|
throw new DifyException("PATCH请求异常: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -745,14 +792,14 @@ public class DifyApiClient {
|
|||||||
String responseBody = response.body() != null ? response.body().string() : "";
|
String responseBody = response.body() != null ? response.body().string() : "";
|
||||||
|
|
||||||
if (!response.isSuccessful()) {
|
if (!response.isSuccessful()) {
|
||||||
log.error("DELETE请求失败: {} - {} - {}", url, response.code(), responseBody);
|
logger.error("DELETE请求失败: {} - {} - {}", url, response.code(), responseBody);
|
||||||
throw new DifyException("DELETE请求失败[" + response.code() + "]: " + responseBody);
|
throw new DifyException("DELETE请求失败[" + response.code() + "]: " + responseBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
return responseBody;
|
return responseBody;
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error("DELETE请求异常: {}", url, e);
|
logger.error("DELETE请求异常: {}", url, e);
|
||||||
throw new DifyException("DELETE请求异常: " + e.getMessage(), e);
|
throw new DifyException("DELETE请求异常: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -790,5 +837,45 @@ public class DifyApiClient {
|
|||||||
this.user = user;
|
this.user = user;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 反馈请求的内部类
|
||||||
|
*/
|
||||||
|
private static class FeedbackRequest {
|
||||||
|
private String rating;
|
||||||
|
private String user;
|
||||||
|
|
||||||
|
private String feedback;
|
||||||
|
|
||||||
|
public FeedbackRequest(String rating, String user, String feedback) {
|
||||||
|
this.rating = rating;
|
||||||
|
this.user = user;
|
||||||
|
this.feedback = feedback;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRating() {
|
||||||
|
return rating;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRating(String rating) {
|
||||||
|
this.rating = rating;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUser() {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUser(String user) {
|
||||||
|
this.user = user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFeedback() {
|
||||||
|
return feedback;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFeedback(String feedback) {
|
||||||
|
this.feedback = feedback;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,15 @@ public interface StreamCallback {
|
|||||||
*/
|
*/
|
||||||
void onMessageEnd(String metadata);
|
void onMessageEnd(String metadata);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接收到Dify原始事件(用于转发完整事件数据)
|
||||||
|
* @param eventType 事件类型(如workflow_started、node_started等)
|
||||||
|
* @param eventData 完整的事件JSON数据
|
||||||
|
*/
|
||||||
|
default void onEvent(String eventType, String eventData) {
|
||||||
|
// 默认实现:不处理
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 流式响应完成
|
* 流式响应完成
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import org.xyzh.common.core.page.PageRequest;
|
|||||||
import org.xyzh.common.dto.ai.TbAiAgentConfig;
|
import org.xyzh.common.dto.ai.TbAiAgentConfig;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description AI智能体配置控制器
|
* @description AI智能体配置控制器
|
||||||
@@ -61,7 +62,7 @@ public class AiAgentConfigController {
|
|||||||
* @since 2025-11-04
|
* @since 2025-11-04
|
||||||
*/
|
*/
|
||||||
@DeleteMapping("/{id}")
|
@DeleteMapping("/{id}")
|
||||||
public ResultDomain<Boolean> deleteAgent(@PathVariable String id) {
|
public ResultDomain<Boolean> deleteAgent(@PathVariable(name = "id") String id) {
|
||||||
log.info("删除智能体: id={}", id);
|
log.info("删除智能体: id={}", id);
|
||||||
return agentConfigService.deleteAgent(id);
|
return agentConfigService.deleteAgent(id);
|
||||||
}
|
}
|
||||||
@@ -74,19 +75,19 @@ public class AiAgentConfigController {
|
|||||||
* @since 2025-11-04
|
* @since 2025-11-04
|
||||||
*/
|
*/
|
||||||
@GetMapping("/{id}")
|
@GetMapping("/{id}")
|
||||||
public ResultDomain<TbAiAgentConfig> getAgent(@PathVariable String id) {
|
public ResultDomain<TbAiAgentConfig> getAgent(@PathVariable(name = "id") String id) {
|
||||||
log.info("获取智能体: id={}", id);
|
log.info("获取智能体: id={}", id);
|
||||||
return agentConfigService.getAgentById(id);
|
return agentConfigService.getAgentById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 获取启用的智能体列表
|
* @description 获取启用的智能体列表
|
||||||
* @return ResultDomain<List<TbAiAgentConfig>>
|
* @return ResultDomain<TbAiAgentConfig>
|
||||||
* @author AI Assistant
|
* @author AI Assistant
|
||||||
* @since 2025-11-04
|
* @since 2025-11-04
|
||||||
*/
|
*/
|
||||||
@GetMapping("/enabled")
|
@GetMapping("/enabled")
|
||||||
public ResultDomain<List<TbAiAgentConfig>> getEnabledAgents() {
|
public ResultDomain<TbAiAgentConfig> getEnabledAgents() {
|
||||||
log.info("获取启用的智能体列表");
|
log.info("获取启用的智能体列表");
|
||||||
return agentConfigService.listEnabledAgents();
|
return agentConfigService.listEnabledAgents();
|
||||||
}
|
}
|
||||||
@@ -94,12 +95,12 @@ public class AiAgentConfigController {
|
|||||||
/**
|
/**
|
||||||
* @description 查询智能体列表
|
* @description 查询智能体列表
|
||||||
* @param agentConfig 智能体配置
|
* @param agentConfig 智能体配置
|
||||||
* @return ResultDomain<List<TbAiAgentConfig>>
|
* @return ResultDomain<TbAiAgentConfig>
|
||||||
* @author AI Assistant
|
* @author AI Assistant
|
||||||
* @since 2025-11-04
|
* @since 2025-11-04
|
||||||
*/
|
*/
|
||||||
@PostMapping("/list")
|
@PostMapping("/list")
|
||||||
public ResultDomain<List<TbAiAgentConfig>> listAgents(
|
public ResultDomain<TbAiAgentConfig> listAgents(
|
||||||
@RequestBody TbAiAgentConfig agentConfig) {
|
@RequestBody TbAiAgentConfig agentConfig) {
|
||||||
log.info("查询智能体列表: agentConfig={}", agentConfig);
|
log.info("查询智能体列表: agentConfig={}", agentConfig);
|
||||||
return agentConfigService.listAgents(agentConfig);
|
return agentConfigService.listAgents(agentConfig);
|
||||||
@@ -121,34 +122,31 @@ public class AiAgentConfigController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 更新智能体状态
|
* @description 更新智能体状态
|
||||||
* @param id 智能体ID
|
* @param requestBody 请求体(id, status)
|
||||||
* @param status 状态(0禁用 1启用)
|
|
||||||
* @return ResultDomain<Boolean>
|
* @return ResultDomain<Boolean>
|
||||||
* @author AI Assistant
|
* @author AI Assistant
|
||||||
* @since 2025-11-04
|
* @since 2025-11-04
|
||||||
*/
|
*/
|
||||||
@PutMapping("/{id}/status")
|
@PutMapping("/status")
|
||||||
public ResultDomain<Boolean> updateStatus(
|
public ResultDomain<Boolean> updateStatus(@RequestBody Map<String, Object> requestBody) {
|
||||||
@PathVariable String id,
|
String id = (String) requestBody.get("id");
|
||||||
@RequestParam Integer status) {
|
Integer status = (Integer) requestBody.get("status");
|
||||||
log.info("更新智能体状态: id={}, status={}", id, status);
|
log.info("更新智能体状态: id={}, status={}", id, status);
|
||||||
return agentConfigService.updateAgentStatus(id, status);
|
return agentConfigService.updateAgentStatus(id, status);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 更新Dify配置
|
* @description 更新Dify配置
|
||||||
* @param id 智能体ID
|
* @param requestBody 请求体(id, difyAppId, difyApiKey)
|
||||||
* @param difyAppId Dify应用ID
|
|
||||||
* @param difyApiKey Dify API Key
|
|
||||||
* @return ResultDomain<Boolean>
|
* @return ResultDomain<Boolean>
|
||||||
* @author AI Assistant
|
* @author AI Assistant
|
||||||
* @since 2025-11-04
|
* @since 2025-11-04
|
||||||
*/
|
*/
|
||||||
@PutMapping("/{id}/dify")
|
@PutMapping("/dify")
|
||||||
public ResultDomain<Boolean> updateDifyConfig(
|
public ResultDomain<Boolean> updateDifyConfig(@RequestBody Map<String, Object> requestBody) {
|
||||||
@PathVariable String id,
|
String id = (String) requestBody.get("id");
|
||||||
@RequestParam String difyAppId,
|
String difyAppId = (String) requestBody.get("difyAppId");
|
||||||
@RequestParam String difyApiKey) {
|
String difyApiKey = (String) requestBody.get("difyApiKey");
|
||||||
log.info("更新Dify配置: id={}, difyAppId={}", id, difyAppId);
|
log.info("更新Dify配置: id={}, difyAppId={}", id, difyAppId);
|
||||||
return agentConfigService.updateDifyConfig(id, difyAppId, difyApiKey);
|
return agentConfigService.updateDifyConfig(id, difyAppId, difyApiKey);
|
||||||
}
|
}
|
||||||
@@ -163,8 +161,8 @@ public class AiAgentConfigController {
|
|||||||
*/
|
*/
|
||||||
@GetMapping("/check-name")
|
@GetMapping("/check-name")
|
||||||
public ResultDomain<Boolean> checkNameExists(
|
public ResultDomain<Boolean> checkNameExists(
|
||||||
@RequestParam String name,
|
@RequestParam(name = "name") String name,
|
||||||
@RequestParam(required = false) String excludeId) {
|
@RequestParam(name = "excludeId", required = false) String excludeId) {
|
||||||
log.info("检查名称是否存在: name={}, excludeId={}", name, excludeId);
|
log.info("检查名称是否存在: name={}, excludeId={}", name, excludeId);
|
||||||
return agentConfigService.checkNameExists(name, excludeId);
|
return agentConfigService.checkNameExists(name, excludeId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
import org.xyzh.api.ai.chat.AiChatService;
|
import org.xyzh.api.ai.chat.AiChatService;
|
||||||
import org.xyzh.api.ai.history.AiChatHistoryService;
|
import org.xyzh.api.ai.history.AiChatHistoryService;
|
||||||
import org.xyzh.common.core.domain.ResultDomain;
|
import org.xyzh.common.core.domain.ResultDomain;
|
||||||
@@ -12,6 +13,7 @@ import org.xyzh.common.core.page.PageParam;
|
|||||||
import org.xyzh.common.dto.ai.TbAiConversation;
|
import org.xyzh.common.dto.ai.TbAiConversation;
|
||||||
import org.xyzh.common.dto.ai.TbAiMessage;
|
import org.xyzh.common.dto.ai.TbAiMessage;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@@ -37,22 +39,29 @@ public class AiChatController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 流式对话(SSE)
|
* @description 流式对话(SSE)
|
||||||
* @param requestBody 请求体(agentId, conversationId, query, knowledgeIds)
|
* @param agentId 智能体ID
|
||||||
* @return ResultDomain<TbAiMessage>
|
* @param conversationId 会话ID
|
||||||
|
* @param query 用户问题
|
||||||
|
* @param knowledgeIds 知识库ID列表(逗号分隔)
|
||||||
|
* @return SseEmitter SSE流式推送对象
|
||||||
* @author AI Assistant
|
* @author AI Assistant
|
||||||
* @since 2025-11-04
|
* @since 2025-11-04
|
||||||
*/
|
*/
|
||||||
@PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||||
public ResultDomain<TbAiMessage> streamChat(@RequestBody Map<String, Object> requestBody) {
|
public SseEmitter streamChat(
|
||||||
String agentId = (String) requestBody.get("agentId");
|
@RequestParam(name = "agentId") String agentId,
|
||||||
String conversationId = (String) requestBody.get("conversationId");
|
@RequestParam(name = "conversationId", required = false) String conversationId,
|
||||||
String query = (String) requestBody.get("query");
|
@RequestParam(name = "query") String query,
|
||||||
@SuppressWarnings("unchecked")
|
@RequestParam(name = "knowledgeIds", required = false) String knowledgeIds) {
|
||||||
List<String> knowledgeIds = (List<String>) requestBody.get("knowledgeIds");
|
|
||||||
Object callback = requestBody.get("callback");
|
// 解析knowledgeIds
|
||||||
|
List<String> knowledgeIdList = null;
|
||||||
|
if (knowledgeIds != null && !knowledgeIds.isEmpty()) {
|
||||||
|
knowledgeIdList = Arrays.asList(knowledgeIds.split(","));
|
||||||
|
}
|
||||||
|
|
||||||
log.info("流式对话: agentId={}, conversationId={}, query={}", agentId, conversationId, query);
|
log.info("流式对话: agentId={}, conversationId={}, query={}", agentId, conversationId, query);
|
||||||
return chatService.streamChat(agentId, conversationId, query, knowledgeIds, callback);
|
return chatService.streamChatWithSse(agentId, conversationId, query, knowledgeIdList);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -74,34 +83,33 @@ public class AiChatController {
|
|||||||
return chatService.blockingChat(agentId, conversationId, query, knowledgeIds);
|
return chatService.blockingChat(agentId, conversationId, query, knowledgeIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 停止对话生成
|
* @description 停止对话生成(通过Dify TaskID)
|
||||||
* @param messageId 消息ID
|
* @param requestBody 请求体(taskId, agentId)
|
||||||
* @return ResultDomain<Boolean>
|
* @return ResultDomain<Boolean>
|
||||||
* @author AI Assistant
|
* @author AI Assistant
|
||||||
* @since 2025-11-04
|
* @since 2025-11-05
|
||||||
*/
|
*/
|
||||||
@PostMapping("/stop/{messageId}")
|
@PostMapping("/stop-by-taskid")
|
||||||
public ResultDomain<Boolean> stopChat(@PathVariable String messageId) {
|
public ResultDomain<Boolean> stopChatByTaskId(@RequestBody Map<String, Object> requestBody) {
|
||||||
log.info("停止对话生成: messageId={}", messageId);
|
String taskId = (String) requestBody.get("taskId");
|
||||||
return chatService.stopChat(messageId);
|
String agentId = (String) requestBody.get("agentId");
|
||||||
|
log.info("停止对话生成(通过TaskID): taskId={}, agentId={}", taskId, agentId);
|
||||||
|
return chatService.stopChatByTaskId(taskId, agentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 重新生成回答
|
* @description 重新生成回答(SSE流式)
|
||||||
* @param messageId 原消息ID
|
* @param messageId 原消息ID
|
||||||
* @param requestBody 请求体(可包含callback)
|
* @return SseEmitter SSE流式推送对象
|
||||||
* @return ResultDomain<TbAiMessage>
|
|
||||||
* @author AI Assistant
|
* @author AI Assistant
|
||||||
* @since 2025-11-04
|
* @since 2025-11-05
|
||||||
*/
|
*/
|
||||||
@PostMapping("/regenerate/{messageId}")
|
@GetMapping(value = "/regenerate/{messageId}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||||
public ResultDomain<TbAiMessage> regenerateAnswer(
|
public SseEmitter regenerateAnswer(@PathVariable(name = "messageId") String messageId) {
|
||||||
@PathVariable String messageId,
|
|
||||||
@RequestBody(required = false) Map<String, Object> requestBody) {
|
|
||||||
log.info("重新生成回答: messageId={}", messageId);
|
log.info("重新生成回答: messageId={}", messageId);
|
||||||
Object callback = requestBody != null ? requestBody.get("callback") : null;
|
return chatService.regenerateAnswerWithSse(messageId);
|
||||||
return chatService.regenerateAnswer(messageId, callback);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -114,7 +122,7 @@ public class AiChatController {
|
|||||||
*/
|
*/
|
||||||
@PostMapping("/message/{messageId}/rate")
|
@PostMapping("/message/{messageId}/rate")
|
||||||
public ResultDomain<Boolean> rateMessage(
|
public ResultDomain<Boolean> rateMessage(
|
||||||
@PathVariable String messageId,
|
@PathVariable(name = "messageId") String messageId,
|
||||||
@RequestBody Map<String, Object> requestBody) {
|
@RequestBody Map<String, Object> requestBody) {
|
||||||
Integer rating = (Integer) requestBody.get("rating");
|
Integer rating = (Integer) requestBody.get("rating");
|
||||||
String feedback = (String) requestBody.get("feedback");
|
String feedback = (String) requestBody.get("feedback");
|
||||||
@@ -148,7 +156,7 @@ public class AiChatController {
|
|||||||
* @since 2025-11-04
|
* @since 2025-11-04
|
||||||
*/
|
*/
|
||||||
@GetMapping("/conversation/{conversationId}")
|
@GetMapping("/conversation/{conversationId}")
|
||||||
public ResultDomain<TbAiConversation> getConversation(@PathVariable String conversationId) {
|
public ResultDomain<TbAiConversation> getConversation(@PathVariable(name = "conversationId") String conversationId) {
|
||||||
log.info("获取会话信息: conversationId={}", conversationId);
|
log.info("获取会话信息: conversationId={}", conversationId);
|
||||||
return chatService.getConversation(conversationId);
|
return chatService.getConversation(conversationId);
|
||||||
}
|
}
|
||||||
@@ -174,7 +182,7 @@ public class AiChatController {
|
|||||||
* @since 2025-11-04
|
* @since 2025-11-04
|
||||||
*/
|
*/
|
||||||
@DeleteMapping("/conversation/{conversationId}")
|
@DeleteMapping("/conversation/{conversationId}")
|
||||||
public ResultDomain<Boolean> deleteConversation(@PathVariable String conversationId) {
|
public ResultDomain<Boolean> deleteConversation(@PathVariable(name = "conversationId") String conversationId) {
|
||||||
log.info("删除会话: conversationId={}", conversationId);
|
log.info("删除会话: conversationId={}", conversationId);
|
||||||
return chatService.deleteConversation(conversationId);
|
return chatService.deleteConversation(conversationId);
|
||||||
}
|
}
|
||||||
@@ -182,13 +190,13 @@ public class AiChatController {
|
|||||||
/**
|
/**
|
||||||
* @description 获取用户的会话列表
|
* @description 获取用户的会话列表
|
||||||
* @param agentId 智能体ID(可选)
|
* @param agentId 智能体ID(可选)
|
||||||
* @return ResultDomain<List<TbAiConversation>>
|
* @return ResultDomain<TbAiConversation>
|
||||||
* @author AI Assistant
|
* @author AI Assistant
|
||||||
* @since 2025-11-04
|
* @since 2025-11-04
|
||||||
*/
|
*/
|
||||||
@GetMapping("/conversations")
|
@GetMapping("/conversations")
|
||||||
public ResultDomain<List<TbAiConversation>> listUserConversations(
|
public ResultDomain<TbAiConversation> listUserConversations(
|
||||||
@RequestParam(required = false) String agentId) {
|
@RequestParam(name = "agentId", required = false) String agentId) {
|
||||||
log.info("获取用户会话列表: agentId={}", agentId);
|
log.info("获取用户会话列表: agentId={}", agentId);
|
||||||
return chatService.listUserConversations(agentId);
|
return chatService.listUserConversations(agentId);
|
||||||
}
|
}
|
||||||
@@ -198,12 +206,12 @@ public class AiChatController {
|
|||||||
/**
|
/**
|
||||||
* @description 获取会话的消息列表
|
* @description 获取会话的消息列表
|
||||||
* @param conversationId 会话ID
|
* @param conversationId 会话ID
|
||||||
* @return ResultDomain<List<TbAiMessage>>
|
* @return ResultDomain<TbAiMessage>
|
||||||
* @author AI Assistant
|
* @author AI Assistant
|
||||||
* @since 2025-11-04
|
* @since 2025-11-04
|
||||||
*/
|
*/
|
||||||
@GetMapping("/conversation/{conversationId}/messages")
|
@GetMapping("/conversation/{conversationId}/messages")
|
||||||
public ResultDomain<List<TbAiMessage>> listMessages(@PathVariable String conversationId) {
|
public ResultDomain<TbAiMessage> listMessages(@PathVariable(name = "conversationId") String conversationId) {
|
||||||
log.info("获取会话消息列表: conversationId={}", conversationId);
|
log.info("获取会话消息列表: conversationId={}", conversationId);
|
||||||
return chatService.listMessages(conversationId);
|
return chatService.listMessages(conversationId);
|
||||||
}
|
}
|
||||||
@@ -216,7 +224,7 @@ public class AiChatController {
|
|||||||
* @since 2025-11-04
|
* @since 2025-11-04
|
||||||
*/
|
*/
|
||||||
@GetMapping("/message/{messageId}")
|
@GetMapping("/message/{messageId}")
|
||||||
public ResultDomain<TbAiMessage> getMessage(@PathVariable String messageId) {
|
public ResultDomain<TbAiMessage> getMessage(@PathVariable(name = "messageId") String messageId) {
|
||||||
log.info("获取消息: messageId={}", messageId);
|
log.info("获取消息: messageId={}", messageId);
|
||||||
return chatService.getMessage(messageId);
|
return chatService.getMessage(messageId);
|
||||||
}
|
}
|
||||||
@@ -229,7 +237,7 @@ public class AiChatController {
|
|||||||
* @since 2025-11-04
|
* @since 2025-11-04
|
||||||
*/
|
*/
|
||||||
@PostMapping("/conversation/{conversationId}/summary")
|
@PostMapping("/conversation/{conversationId}/summary")
|
||||||
public ResultDomain<Boolean> generateSummary(@PathVariable String conversationId) {
|
public ResultDomain<Boolean> generateSummary(@PathVariable(name = "conversationId") String conversationId) {
|
||||||
log.info("生成会话摘要: conversationId={}", conversationId);
|
log.info("生成会话摘要: conversationId={}", conversationId);
|
||||||
return chatService.generateSummaryAsync(conversationId);
|
return chatService.generateSummaryAsync(conversationId);
|
||||||
}
|
}
|
||||||
@@ -264,7 +272,7 @@ public class AiChatController {
|
|||||||
*/
|
*/
|
||||||
@PostMapping("/history/search")
|
@PostMapping("/history/search")
|
||||||
public PageDomain<TbAiConversation> searchConversations(
|
public PageDomain<TbAiConversation> searchConversations(
|
||||||
@RequestParam String keyword,
|
@RequestParam(name = "keyword") String keyword,
|
||||||
@RequestBody PageParam pageParam) {
|
@RequestBody PageParam pageParam) {
|
||||||
log.info("搜索会话: keyword={}", keyword);
|
log.info("搜索会话: keyword={}", keyword);
|
||||||
return chatHistoryService.searchConversations(keyword, pageParam);
|
return chatHistoryService.searchConversations(keyword, pageParam);
|
||||||
@@ -272,32 +280,30 @@ public class AiChatController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 收藏/取消收藏会话
|
* @description 收藏/取消收藏会话
|
||||||
* @param conversationId 会话ID
|
* @param requestBody 请求体(conversationId, isFavorite)
|
||||||
* @param isFavorite 是否收藏
|
|
||||||
* @return ResultDomain<Boolean>
|
* @return ResultDomain<Boolean>
|
||||||
* @author AI Assistant
|
* @author AI Assistant
|
||||||
* @since 2025-11-04
|
* @since 2025-11-04
|
||||||
*/
|
*/
|
||||||
@PutMapping("/history/conversation/{conversationId}/favorite")
|
@PutMapping("/history/conversation/favorite")
|
||||||
public ResultDomain<Boolean> toggleFavorite(
|
public ResultDomain<Boolean> toggleFavorite(@RequestBody Map<String, Object> requestBody) {
|
||||||
@PathVariable String conversationId,
|
String conversationId = (String) requestBody.get("conversationId");
|
||||||
@RequestParam Boolean isFavorite) {
|
Boolean isFavorite = (Boolean) requestBody.get("isFavorite");
|
||||||
log.info("{}收藏会话: conversationId={}", isFavorite ? "添加" : "取消", conversationId);
|
log.info("{}收藏会话: conversationId={}", isFavorite ? "添加" : "取消", conversationId);
|
||||||
return chatHistoryService.toggleFavorite(conversationId, isFavorite);
|
return chatHistoryService.toggleFavorite(conversationId, isFavorite);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 置顶/取消置顶会话
|
* @description 置顶/取消置顶会话
|
||||||
* @param conversationId 会话ID
|
* @param requestBody 请求体(conversationId, isPinned)
|
||||||
* @param isPinned 是否置顶
|
|
||||||
* @return ResultDomain<Boolean>
|
* @return ResultDomain<Boolean>
|
||||||
* @author AI Assistant
|
* @author AI Assistant
|
||||||
* @since 2025-11-04
|
* @since 2025-11-04
|
||||||
*/
|
*/
|
||||||
@PutMapping("/history/conversation/{conversationId}/pin")
|
@PutMapping("/history/conversation/pin")
|
||||||
public ResultDomain<Boolean> togglePin(
|
public ResultDomain<Boolean> togglePin(@RequestBody Map<String, Object> requestBody) {
|
||||||
@PathVariable String conversationId,
|
String conversationId = (String) requestBody.get("conversationId");
|
||||||
@RequestParam Boolean isPinned) {
|
Boolean isPinned = (Boolean) requestBody.get("isPinned");
|
||||||
log.info("{}置顶会话: conversationId={}", isPinned ? "添加" : "取消", conversationId);
|
log.info("{}置顶会话: conversationId={}", isPinned ? "添加" : "取消", conversationId);
|
||||||
return chatHistoryService.togglePin(conversationId, isPinned);
|
return chatHistoryService.togglePin(conversationId, isPinned);
|
||||||
}
|
}
|
||||||
@@ -325,7 +331,7 @@ public class AiChatController {
|
|||||||
* @since 2025-11-04
|
* @since 2025-11-04
|
||||||
*/
|
*/
|
||||||
@GetMapping("/history/export/markdown/{conversationId}")
|
@GetMapping("/history/export/markdown/{conversationId}")
|
||||||
public ResultDomain<String> exportAsMarkdown(@PathVariable String conversationId) {
|
public ResultDomain<String> exportAsMarkdown(@PathVariable(name = "conversationId") String conversationId) {
|
||||||
log.info("导出会话(Markdown): conversationId={}", conversationId);
|
log.info("导出会话(Markdown): conversationId={}", conversationId);
|
||||||
return chatHistoryService.exportConversationAsMarkdown(conversationId);
|
return chatHistoryService.exportConversationAsMarkdown(conversationId);
|
||||||
}
|
}
|
||||||
@@ -338,7 +344,7 @@ public class AiChatController {
|
|||||||
* @since 2025-11-04
|
* @since 2025-11-04
|
||||||
*/
|
*/
|
||||||
@GetMapping("/history/export/json/{conversationId}")
|
@GetMapping("/history/export/json/{conversationId}")
|
||||||
public ResultDomain<String> exportAsJson(@PathVariable String conversationId) {
|
public ResultDomain<String> exportAsJson(@PathVariable(name = "conversationId") String conversationId) {
|
||||||
log.info("导出会话(JSON): conversationId={}", conversationId);
|
log.info("导出会话(JSON): conversationId={}", conversationId);
|
||||||
return chatHistoryService.exportConversationAsJson(conversationId);
|
return chatHistoryService.exportConversationAsJson(conversationId);
|
||||||
}
|
}
|
||||||
@@ -346,13 +352,13 @@ public class AiChatController {
|
|||||||
/**
|
/**
|
||||||
* @description 获取最近对话列表
|
* @description 获取最近对话列表
|
||||||
* @param limit 限制数量(可选,默认10)
|
* @param limit 限制数量(可选,默认10)
|
||||||
* @return ResultDomain<List<TbAiConversation>>
|
* @return ResultDomain<TbAiConversation>
|
||||||
* @author AI Assistant
|
* @author AI Assistant
|
||||||
* @since 2025-11-04
|
* @since 2025-11-04
|
||||||
*/
|
*/
|
||||||
@GetMapping("/history/recent")
|
@GetMapping("/history/recent")
|
||||||
public ResultDomain<TbAiConversation> getRecentConversations(
|
public ResultDomain<TbAiConversation> getRecentConversations(
|
||||||
@RequestParam(defaultValue = "10") Integer limit) {
|
@RequestParam(name = "limit", defaultValue = "10") Integer limit) {
|
||||||
log.info("获取最近对话列表: limit={}", limit);
|
log.info("获取最近对话列表: limit={}", limit);
|
||||||
return chatHistoryService.getRecentConversations(limit);
|
return chatHistoryService.getRecentConversations(limit);
|
||||||
}
|
}
|
||||||
@@ -366,7 +372,7 @@ public class AiChatController {
|
|||||||
*/
|
*/
|
||||||
@GetMapping("/history/statistics")
|
@GetMapping("/history/statistics")
|
||||||
public ResultDomain<Map<String, Object>> getUserChatStatistics(
|
public ResultDomain<Map<String, Object>> getUserChatStatistics(
|
||||||
@RequestParam(required = false) String userId) {
|
@RequestParam(name = "userId", required = false) String userId) {
|
||||||
log.info("获取用户对话统计: userId={}", userId);
|
log.info("获取用户对话统计: userId={}", userId);
|
||||||
return chatHistoryService.getUserChatStatistics(userId);
|
return chatHistoryService.getUserChatStatistics(userId);
|
||||||
}
|
}
|
||||||
@@ -379,7 +385,7 @@ public class AiChatController {
|
|||||||
* @since 2025-11-04
|
* @since 2025-11-04
|
||||||
*/
|
*/
|
||||||
@GetMapping("/history/conversation/{conversationId}/statistics")
|
@GetMapping("/history/conversation/{conversationId}/statistics")
|
||||||
public ResultDomain<Map<String, Object>> getConversationStatistics(@PathVariable String conversationId) {
|
public ResultDomain<Map<String, Object>> getConversationStatistics(@PathVariable(name = "conversationId") String conversationId) {
|
||||||
log.info("获取会话统计: conversationId={}", conversationId);
|
log.info("获取会话统计: conversationId={}", conversationId);
|
||||||
return chatHistoryService.getConversationStatistics(conversationId);
|
return chatHistoryService.getConversationStatistics(conversationId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,9 +40,9 @@ public class AiFileUploadController {
|
|||||||
*/
|
*/
|
||||||
@PostMapping("/upload")
|
@PostMapping("/upload")
|
||||||
public ResultDomain<TbAiUploadFile> uploadFile(
|
public ResultDomain<TbAiUploadFile> uploadFile(
|
||||||
@RequestParam String knowledgeId,
|
@RequestParam(name = "knowledgeId") String knowledgeId,
|
||||||
@RequestParam("file") MultipartFile file,
|
@RequestParam(name = "file") MultipartFile file,
|
||||||
@RequestParam(required = false) String indexingTechnique) {
|
@RequestParam(name = "indexingTechnique", required = false) String indexingTechnique) {
|
||||||
log.info("上传文件到知识库: knowledgeId={}, fileName={}", knowledgeId, file.getOriginalFilename());
|
log.info("上传文件到知识库: knowledgeId={}, fileName={}", knowledgeId, file.getOriginalFilename());
|
||||||
return uploadFileService.uploadToKnowledge(knowledgeId, file, indexingTechnique);
|
return uploadFileService.uploadToKnowledge(knowledgeId, file, indexingTechnique);
|
||||||
}
|
}
|
||||||
@@ -52,15 +52,15 @@ public class AiFileUploadController {
|
|||||||
* @param knowledgeId 知识库ID
|
* @param knowledgeId 知识库ID
|
||||||
* @param files 文件列表
|
* @param files 文件列表
|
||||||
* @param indexingTechnique 索引方式(可选)
|
* @param indexingTechnique 索引方式(可选)
|
||||||
* @return ResultDomain<List<TbAiUploadFile>>
|
* @return ResultDomain<TbAiUploadFile>
|
||||||
* @author AI Assistant
|
* @author AI Assistant
|
||||||
* @since 2025-11-04
|
* @since 2025-11-04
|
||||||
*/
|
*/
|
||||||
@PostMapping("/upload/batch")
|
@PostMapping("/upload/batch")
|
||||||
public ResultDomain<List<TbAiUploadFile>> batchUploadFiles(
|
public ResultDomain<TbAiUploadFile> batchUploadFiles(
|
||||||
@RequestParam String knowledgeId,
|
@RequestParam(name = "knowledgeId") String knowledgeId,
|
||||||
@RequestParam("files") MultipartFile[] files,
|
@RequestParam(name = "files") MultipartFile[] files,
|
||||||
@RequestParam(required = false) String indexingTechnique) {
|
@RequestParam(name = "indexingTechnique", required = false) String indexingTechnique) {
|
||||||
log.info("批量上传文件: knowledgeId={}, fileCount={}", knowledgeId, files.length);
|
log.info("批量上传文件: knowledgeId={}, fileCount={}", knowledgeId, files.length);
|
||||||
return uploadFileService.batchUploadToKnowledge(knowledgeId, Arrays.asList(files), indexingTechnique);
|
return uploadFileService.batchUploadToKnowledge(knowledgeId, Arrays.asList(files), indexingTechnique);
|
||||||
}
|
}
|
||||||
@@ -73,7 +73,7 @@ public class AiFileUploadController {
|
|||||||
* @since 2025-11-04
|
* @since 2025-11-04
|
||||||
*/
|
*/
|
||||||
@GetMapping("/{fileId}")
|
@GetMapping("/{fileId}")
|
||||||
public ResultDomain<TbAiUploadFile> getFile(@PathVariable String fileId) {
|
public ResultDomain<TbAiUploadFile> getFile(@PathVariable(name = "fileId") String fileId) {
|
||||||
log.info("获取文件信息: fileId={}", fileId);
|
log.info("获取文件信息: fileId={}", fileId);
|
||||||
return uploadFileService.getFileById(fileId);
|
return uploadFileService.getFileById(fileId);
|
||||||
}
|
}
|
||||||
@@ -81,12 +81,12 @@ public class AiFileUploadController {
|
|||||||
/**
|
/**
|
||||||
* @description 查询知识库的文件列表
|
* @description 查询知识库的文件列表
|
||||||
* @param knowledgeId 知识库ID
|
* @param knowledgeId 知识库ID
|
||||||
* @return ResultDomain<List<TbAiUploadFile>>
|
* @return ResultDomain<TbAiUploadFile>
|
||||||
* @author AI Assistant
|
* @author AI Assistant
|
||||||
* @since 2025-11-04
|
* @since 2025-11-04
|
||||||
*/
|
*/
|
||||||
@GetMapping("/list")
|
@GetMapping("/list")
|
||||||
public ResultDomain<List<TbAiUploadFile>> listFiles(@RequestParam String knowledgeId) {
|
public ResultDomain<TbAiUploadFile> listFiles(@RequestParam(name = "knowledgeId") String knowledgeId) {
|
||||||
log.info("查询知识库文件列表: knowledgeId={}", knowledgeId);
|
log.info("查询知识库文件列表: knowledgeId={}", knowledgeId);
|
||||||
return uploadFileService.listFilesByKnowledge(knowledgeId);
|
return uploadFileService.listFilesByKnowledge(knowledgeId);
|
||||||
}
|
}
|
||||||
@@ -112,7 +112,7 @@ public class AiFileUploadController {
|
|||||||
* @since 2025-11-04
|
* @since 2025-11-04
|
||||||
*/
|
*/
|
||||||
@DeleteMapping("/{fileId}")
|
@DeleteMapping("/{fileId}")
|
||||||
public ResultDomain<Boolean> deleteFile(@PathVariable String fileId) {
|
public ResultDomain<Boolean> deleteFile(@PathVariable(name = "fileId") String fileId) {
|
||||||
log.info("删除文件: fileId={}", fileId);
|
log.info("删除文件: fileId={}", fileId);
|
||||||
return uploadFileService.deleteFile(fileId);
|
return uploadFileService.deleteFile(fileId);
|
||||||
}
|
}
|
||||||
@@ -125,7 +125,7 @@ public class AiFileUploadController {
|
|||||||
* @since 2025-11-04
|
* @since 2025-11-04
|
||||||
*/
|
*/
|
||||||
@GetMapping("/{fileId}/status")
|
@GetMapping("/{fileId}/status")
|
||||||
public ResultDomain<TbAiUploadFile> getFileStatus(@PathVariable String fileId) {
|
public ResultDomain<TbAiUploadFile> getFileStatus(@PathVariable(name = "fileId") String fileId) {
|
||||||
log.info("查询文件处理状态: fileId={}", fileId);
|
log.info("查询文件处理状态: fileId={}", fileId);
|
||||||
return uploadFileService.getFileStatus(fileId);
|
return uploadFileService.getFileStatus(fileId);
|
||||||
}
|
}
|
||||||
@@ -138,7 +138,7 @@ public class AiFileUploadController {
|
|||||||
* @since 2025-11-04
|
* @since 2025-11-04
|
||||||
*/
|
*/
|
||||||
@PostMapping("/{fileId}/sync")
|
@PostMapping("/{fileId}/sync")
|
||||||
public ResultDomain<TbAiUploadFile> syncFileStatus(@PathVariable String fileId) {
|
public ResultDomain<TbAiUploadFile> syncFileStatus(@PathVariable(name = "fileId") String fileId) {
|
||||||
log.info("同步文件状态: fileId={}", fileId);
|
log.info("同步文件状态: fileId={}", fileId);
|
||||||
return uploadFileService.syncFileStatus(fileId);
|
return uploadFileService.syncFileStatus(fileId);
|
||||||
}
|
}
|
||||||
@@ -146,12 +146,12 @@ public class AiFileUploadController {
|
|||||||
/**
|
/**
|
||||||
* @description 批量同步知识库的所有文件状态
|
* @description 批量同步知识库的所有文件状态
|
||||||
* @param knowledgeId 知识库ID
|
* @param knowledgeId 知识库ID
|
||||||
* @return ResultDomain<List<TbAiUploadFile>>
|
* @return ResultDomain<TbAiUploadFile>
|
||||||
* @author AI Assistant
|
* @author AI Assistant
|
||||||
* @since 2025-11-04
|
* @since 2025-11-04
|
||||||
*/
|
*/
|
||||||
@PostMapping("/sync/knowledge/{knowledgeId}")
|
@PostMapping("/sync/knowledge/{knowledgeId}")
|
||||||
public ResultDomain<List<TbAiUploadFile>> syncKnowledgeFiles(@PathVariable String knowledgeId) {
|
public ResultDomain<TbAiUploadFile> syncKnowledgeFiles(@PathVariable(name = "knowledgeId") String knowledgeId) {
|
||||||
log.info("批量同步知识库文件状态: knowledgeId={}", knowledgeId);
|
log.info("批量同步知识库文件状态: knowledgeId={}", knowledgeId);
|
||||||
return uploadFileService.syncKnowledgeFiles(knowledgeId);
|
return uploadFileService.syncKnowledgeFiles(knowledgeId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ public class AiKnowledgeController {
|
|||||||
* @since 2025-11-04
|
* @since 2025-11-04
|
||||||
*/
|
*/
|
||||||
@DeleteMapping("/{id}")
|
@DeleteMapping("/{id}")
|
||||||
public ResultDomain<Boolean> deleteKnowledge(@PathVariable String id) {
|
public ResultDomain<Boolean> deleteKnowledge(@PathVariable(name = "id") String id) {
|
||||||
log.info("删除知识库: id={}", id);
|
log.info("删除知识库: id={}", id);
|
||||||
return knowledgeService.deleteKnowledge(id);
|
return knowledgeService.deleteKnowledge(id);
|
||||||
}
|
}
|
||||||
@@ -82,7 +82,7 @@ public class AiKnowledgeController {
|
|||||||
* @since 2025-11-04
|
* @since 2025-11-04
|
||||||
*/
|
*/
|
||||||
@GetMapping("/{id}")
|
@GetMapping("/{id}")
|
||||||
public ResultDomain<TbAiKnowledge> getKnowledge(@PathVariable String id) {
|
public ResultDomain<TbAiKnowledge> getKnowledge(@PathVariable(name = "id") String id) {
|
||||||
log.info("获取知识库: id={}", id);
|
log.info("获取知识库: id={}", id);
|
||||||
return knowledgeService.getKnowledgeById(id);
|
return knowledgeService.getKnowledgeById(id);
|
||||||
}
|
}
|
||||||
@@ -90,12 +90,12 @@ public class AiKnowledgeController {
|
|||||||
/**
|
/**
|
||||||
* @description 查询知识库列表
|
* @description 查询知识库列表
|
||||||
* @param filter 过滤条件
|
* @param filter 过滤条件
|
||||||
* @return ResultDomain<List<TbAiKnowledge>>
|
* @return ResultDomain<TbAiKnowledge>
|
||||||
* @author AI Assistant
|
* @author AI Assistant
|
||||||
* @since 2025-11-04
|
* @since 2025-11-04
|
||||||
*/
|
*/
|
||||||
@PostMapping("/list")
|
@PostMapping("/list")
|
||||||
public ResultDomain<List<TbAiKnowledge>> listKnowledges(
|
public ResultDomain<TbAiKnowledge> listKnowledges(
|
||||||
@RequestBody(required = false) TbAiKnowledge filter) {
|
@RequestBody(required = false) TbAiKnowledge filter) {
|
||||||
log.info("查询知识库列表");
|
log.info("查询知识库列表");
|
||||||
return knowledgeService.listKnowledges(filter);
|
return knowledgeService.listKnowledges(filter);
|
||||||
@@ -122,23 +122,21 @@ public class AiKnowledgeController {
|
|||||||
* @since 2025-11-04
|
* @since 2025-11-04
|
||||||
*/
|
*/
|
||||||
@PostMapping("/{id}/sync")
|
@PostMapping("/{id}/sync")
|
||||||
public ResultDomain<TbAiKnowledge> syncFromDify(@PathVariable String id) {
|
public ResultDomain<TbAiKnowledge> syncFromDify(@PathVariable(name = "id") String id) {
|
||||||
log.info("同步Dify知识库信息: id={}", id);
|
log.info("同步Dify知识库信息: id={}", id);
|
||||||
return knowledgeService.syncFromDify(id);
|
return knowledgeService.syncFromDify(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 更新知识库权限
|
* @description 更新知识库权限
|
||||||
* @param knowledgeId 知识库ID
|
* @param requestBody 请求体(knowledgeId, permissionType, deptIds, roleIds)
|
||||||
* @param requestBody 请求体(permissionType, deptIds, roleIds)
|
|
||||||
* @return ResultDomain<Boolean>
|
* @return ResultDomain<Boolean>
|
||||||
* @author AI Assistant
|
* @author AI Assistant
|
||||||
* @since 2025-11-04
|
* @since 2025-11-04
|
||||||
*/
|
*/
|
||||||
@PutMapping("/{knowledgeId}/permission")
|
@PutMapping("/permission")
|
||||||
public ResultDomain<Boolean> updatePermission(
|
public ResultDomain<Boolean> updatePermission(@RequestBody Map<String, Object> requestBody) {
|
||||||
@PathVariable String knowledgeId,
|
String knowledgeId = (String) requestBody.get("knowledgeId");
|
||||||
@RequestBody Map<String, Object> requestBody) {
|
|
||||||
String permissionType = (String) requestBody.get("permissionType");
|
String permissionType = (String) requestBody.get("permissionType");
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
List<String> deptIds = (List<String>) requestBody.get("deptIds");
|
List<String> deptIds = (List<String>) requestBody.get("deptIds");
|
||||||
@@ -159,8 +157,8 @@ public class AiKnowledgeController {
|
|||||||
*/
|
*/
|
||||||
@GetMapping("/{knowledgeId}/permission")
|
@GetMapping("/{knowledgeId}/permission")
|
||||||
public ResultDomain<Boolean> checkPermission(
|
public ResultDomain<Boolean> checkPermission(
|
||||||
@PathVariable String knowledgeId,
|
@PathVariable(name = "knowledgeId") String knowledgeId,
|
||||||
@RequestParam String operationType) {
|
@RequestParam(name = "operationType") String operationType) {
|
||||||
log.info("检查知识库权限: knowledgeId={}, operationType={}", knowledgeId, operationType);
|
log.info("检查知识库权限: knowledgeId={}, operationType={}", knowledgeId, operationType);
|
||||||
return knowledgeService.checkKnowledgePermission(knowledgeId, operationType);
|
return knowledgeService.checkKnowledgePermission(knowledgeId, operationType);
|
||||||
}
|
}
|
||||||
@@ -173,7 +171,7 @@ public class AiKnowledgeController {
|
|||||||
* @since 2025-11-04
|
* @since 2025-11-04
|
||||||
*/
|
*/
|
||||||
@GetMapping("/{id}/stats")
|
@GetMapping("/{id}/stats")
|
||||||
public ResultDomain<TbAiKnowledge> getKnowledgeStats(@PathVariable String id) {
|
public ResultDomain<TbAiKnowledge> getKnowledgeStats(@PathVariable(name = "id") String id) {
|
||||||
log.info("获取知识库统计信息: id={}", id);
|
log.info("获取知识库统计信息: id={}", id);
|
||||||
return knowledgeService.getKnowledgeStats(id);
|
return knowledgeService.getKnowledgeStats(id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ public class DifyProxyController {
|
|||||||
*/
|
*/
|
||||||
@GetMapping("/datasets/{datasetId}/documents/{documentId}/segments")
|
@GetMapping("/datasets/{datasetId}/documents/{documentId}/segments")
|
||||||
public ResultDomain<String> getDocumentSegments(
|
public ResultDomain<String> getDocumentSegments(
|
||||||
@PathVariable String datasetId,
|
@PathVariable(name = "datasetId") String datasetId,
|
||||||
@PathVariable String documentId) {
|
@PathVariable(name = "documentId") String documentId) {
|
||||||
ResultDomain<String> result = new ResultDomain<>();
|
ResultDomain<String> result = new ResultDomain<>();
|
||||||
log.info("获取文档分段列表: datasetId={}, documentId={}", datasetId, documentId);
|
log.info("获取文档分段列表: datasetId={}, documentId={}", datasetId, documentId);
|
||||||
|
|
||||||
@@ -65,9 +65,9 @@ public class DifyProxyController {
|
|||||||
*/
|
*/
|
||||||
@GetMapping("/datasets/{datasetId}/documents/{documentId}/segments/{segmentId}/child_chunks")
|
@GetMapping("/datasets/{datasetId}/documents/{documentId}/segments/{segmentId}/child_chunks")
|
||||||
public ResultDomain<String> getChildChunks(
|
public ResultDomain<String> getChildChunks(
|
||||||
@PathVariable String datasetId,
|
@PathVariable(name = "datasetId") String datasetId,
|
||||||
@PathVariable String documentId,
|
@PathVariable(name = "documentId") String documentId,
|
||||||
@PathVariable String segmentId) {
|
@PathVariable(name = "segmentId") String segmentId) {
|
||||||
|
|
||||||
log.info("获取子块列表: datasetId={}, documentId={}, segmentId={}",
|
log.info("获取子块列表: datasetId={}, documentId={}, segmentId={}",
|
||||||
datasetId, documentId, segmentId);
|
datasetId, documentId, segmentId);
|
||||||
@@ -101,10 +101,10 @@ public class DifyProxyController {
|
|||||||
*/
|
*/
|
||||||
@PatchMapping("/datasets/{datasetId}/documents/{documentId}/segments/{segmentId}/child_chunks/{childChunkId}")
|
@PatchMapping("/datasets/{datasetId}/documents/{documentId}/segments/{segmentId}/child_chunks/{childChunkId}")
|
||||||
public ResultDomain<String> updateChildChunk(
|
public ResultDomain<String> updateChildChunk(
|
||||||
@PathVariable String datasetId,
|
@PathVariable(name = "datasetId") String datasetId,
|
||||||
@PathVariable String documentId,
|
@PathVariable(name = "documentId") String documentId,
|
||||||
@PathVariable String segmentId,
|
@PathVariable(name = "segmentId") String segmentId,
|
||||||
@PathVariable String childChunkId,
|
@PathVariable(name = "childChunkId") String childChunkId,
|
||||||
@RequestBody Map<String, Object> requestBody) {
|
@RequestBody Map<String, Object> requestBody) {
|
||||||
|
|
||||||
log.info("更新子块: datasetId={}, documentId={}, segmentId={}, childChunkId={}",
|
log.info("更新子块: datasetId={}, documentId={}, segmentId={}, childChunkId={}",
|
||||||
@@ -138,9 +138,9 @@ public class DifyProxyController {
|
|||||||
*/
|
*/
|
||||||
@PostMapping("/datasets/{datasetId}/documents/{documentId}/segments/{segmentId}/child_chunks")
|
@PostMapping("/datasets/{datasetId}/documents/{documentId}/segments/{segmentId}/child_chunks")
|
||||||
public ResultDomain<String> createChildChunk(
|
public ResultDomain<String> createChildChunk(
|
||||||
@PathVariable String datasetId,
|
@PathVariable(name = "datasetId") String datasetId,
|
||||||
@PathVariable String documentId,
|
@PathVariable(name = "documentId") String documentId,
|
||||||
@PathVariable String segmentId,
|
@PathVariable(name = "segmentId") String segmentId,
|
||||||
@RequestBody Map<String, Object> requestBody) {
|
@RequestBody Map<String, Object> requestBody) {
|
||||||
|
|
||||||
log.info("创建子块: datasetId={}, documentId={}, segmentId={}",
|
log.info("创建子块: datasetId={}, documentId={}, segmentId={}",
|
||||||
@@ -174,10 +174,10 @@ public class DifyProxyController {
|
|||||||
*/
|
*/
|
||||||
@DeleteMapping("/datasets/{datasetId}/documents/{documentId}/segments/{segmentId}/child_chunks/{childChunkId}")
|
@DeleteMapping("/datasets/{datasetId}/documents/{documentId}/segments/{segmentId}/child_chunks/{childChunkId}")
|
||||||
public ResultDomain<String> deleteChildChunk(
|
public ResultDomain<String> deleteChildChunk(
|
||||||
@PathVariable String datasetId,
|
@PathVariable(name = "datasetId") String datasetId,
|
||||||
@PathVariable String documentId,
|
@PathVariable(name = "documentId") String documentId,
|
||||||
@PathVariable String segmentId,
|
@PathVariable(name = "segmentId") String segmentId,
|
||||||
@PathVariable String childChunkId) {
|
@PathVariable(name = "childChunkId") String childChunkId) {
|
||||||
|
|
||||||
log.info("删除子块: datasetId={}, documentId={}, segmentId={}, childChunkId={}",
|
log.info("删除子块: datasetId={}, documentId={}, segmentId={}, childChunkId={}",
|
||||||
datasetId, documentId, segmentId, childChunkId);
|
datasetId, documentId, segmentId, childChunkId);
|
||||||
|
|||||||
@@ -240,8 +240,8 @@ public class AiAgentConfigServiceImpl implements AiAgentConfigService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ResultDomain<List<TbAiAgentConfig>> listEnabledAgents() {
|
public ResultDomain<TbAiAgentConfig> listEnabledAgents() {
|
||||||
ResultDomain<List<TbAiAgentConfig>> resultDomain = new ResultDomain<>();
|
ResultDomain<TbAiAgentConfig> resultDomain = new ResultDomain<>();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
TbAiAgentConfig filter = new TbAiAgentConfig();
|
TbAiAgentConfig filter = new TbAiAgentConfig();
|
||||||
@@ -259,8 +259,8 @@ public class AiAgentConfigServiceImpl implements AiAgentConfigService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ResultDomain<List<TbAiAgentConfig>> listAgents(TbAiAgentConfig filter) {
|
public ResultDomain<TbAiAgentConfig> listAgents(TbAiAgentConfig filter) {
|
||||||
ResultDomain<List<TbAiAgentConfig>> resultDomain = new ResultDomain<>();
|
ResultDomain<TbAiAgentConfig> resultDomain = new ResultDomain<>();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
List<TbAiAgentConfig> agents = agentConfigMapper.selectAgentConfigs(filter);
|
List<TbAiAgentConfig> agents = agentConfigMapper.selectAgentConfigs(filter);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
import org.xyzh.ai.client.DifyApiClient;
|
import org.xyzh.ai.client.DifyApiClient;
|
||||||
import org.xyzh.ai.client.callback.StreamCallback;
|
import org.xyzh.ai.client.callback.StreamCallback;
|
||||||
import org.xyzh.ai.client.dto.ChatRequest;
|
import org.xyzh.ai.client.dto.ChatRequest;
|
||||||
@@ -24,6 +25,7 @@ import org.xyzh.common.dto.ai.TbAiMessage;
|
|||||||
import org.xyzh.common.dto.user.TbSysUser;
|
import org.xyzh.common.dto.user.TbSysUser;
|
||||||
import org.xyzh.system.utils.LoginUtil;
|
import org.xyzh.system.utils.LoginUtil;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
@@ -58,48 +60,46 @@ public class AiChatServiceImpl implements AiChatService {
|
|||||||
|
|
||||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
// 异步任务线程池
|
// 异步任务线程池(用于异步生成摘要等后台任务)
|
||||||
private final ExecutorService executorService = Executors.newFixedThreadPool(3);
|
private final ExecutorService executorService = Executors.newFixedThreadPool(3);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
public SseEmitter streamChatWithSse(String agentId, String conversationId, String query, List<String> knowledgeIds) {
|
||||||
public ResultDomain<TbAiMessage> streamChat(
|
// 创建SseEmitter,设置超时时间为5分钟
|
||||||
String agentId,
|
SseEmitter emitter = new SseEmitter(5 * 60 * 1000L);
|
||||||
String conversationId,
|
|
||||||
String query,
|
|
||||||
List<String> knowledgeIds,
|
|
||||||
Object callbackObj) {
|
|
||||||
|
|
||||||
ResultDomain<TbAiMessage> resultDomain = new ResultDomain<>();
|
|
||||||
StreamCallback callback = (callbackObj instanceof StreamCallback) ? (StreamCallback) callbackObj : null;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. 参数验证
|
// 1. 参数验证
|
||||||
if (!StringUtils.hasText(agentId)) {
|
if (!StringUtils.hasText(agentId)) {
|
||||||
resultDomain.fail("智能体ID不能为空");
|
emitter.send(SseEmitter.event().name("error").data("智能体ID不能为空"));
|
||||||
return resultDomain;
|
emitter.complete();
|
||||||
|
return emitter;
|
||||||
}
|
}
|
||||||
if (!StringUtils.hasText(query)) {
|
if (!StringUtils.hasText(query)) {
|
||||||
resultDomain.fail("问题不能为空");
|
emitter.send(SseEmitter.event().name("error").data("问题不能为空"));
|
||||||
return resultDomain;
|
emitter.complete();
|
||||||
|
return emitter;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 获取当前用户
|
// 2. 获取当前用户(在主线程中,可以正常获取)
|
||||||
TbSysUser currentUser = LoginUtil.getCurrentUser();
|
TbSysUser currentUser = LoginUtil.getCurrentUser();
|
||||||
if (currentUser == null) {
|
if (currentUser == null) {
|
||||||
resultDomain.fail("用户未登录");
|
emitter.send(SseEmitter.event().name("error").data("用户未登录"));
|
||||||
return resultDomain;
|
emitter.complete();
|
||||||
|
return emitter;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 查询智能体配置
|
// 3. 查询智能体配置
|
||||||
TbAiAgentConfig agent = agentConfigMapper.selectAgentConfigById(agentId);
|
TbAiAgentConfig agent = agentConfigMapper.selectAgentConfigById(agentId);
|
||||||
if (agent == null || agent.getDeleted()) {
|
if (agent == null || agent.getDeleted()) {
|
||||||
resultDomain.fail("智能体不存在");
|
emitter.send(SseEmitter.event().name("error").data("智能体不存在"));
|
||||||
return resultDomain;
|
emitter.complete();
|
||||||
|
return emitter;
|
||||||
}
|
}
|
||||||
if (agent.getStatus() != 1) {
|
if (agent.getStatus() != 1) {
|
||||||
resultDomain.fail("智能体未启用");
|
emitter.send(SseEmitter.event().name("error").data("智能体未启用"));
|
||||||
return resultDomain;
|
emitter.complete();
|
||||||
|
return emitter;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 获取或创建会话
|
// 4. 获取或创建会话
|
||||||
@@ -107,168 +107,255 @@ public class AiChatServiceImpl implements AiChatService {
|
|||||||
if (StringUtils.hasText(conversationId)) {
|
if (StringUtils.hasText(conversationId)) {
|
||||||
conversation = conversationMapper.selectConversationById(conversationId);
|
conversation = conversationMapper.selectConversationById(conversationId);
|
||||||
if (conversation == null || conversation.getDeleted()) {
|
if (conversation == null || conversation.getDeleted()) {
|
||||||
resultDomain.fail("会话不存在");
|
emitter.send(SseEmitter.event().name("error").data("会话不存在"));
|
||||||
return resultDomain;
|
emitter.complete();
|
||||||
|
return emitter;
|
||||||
}
|
}
|
||||||
// 验证会话所属权
|
|
||||||
if (!conversation.getUserID().equals(currentUser.getID())) {
|
if (!conversation.getUserID().equals(currentUser.getID())) {
|
||||||
resultDomain.fail("无权访问此会话");
|
emitter.send(SseEmitter.event().name("error").data("无权访问此会话"));
|
||||||
return resultDomain;
|
emitter.complete();
|
||||||
|
return emitter;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 创建新会话
|
// 创建新会话
|
||||||
ResultDomain<TbAiConversation> createResult = createConversation(agentId, null);
|
ResultDomain<TbAiConversation> createResult = createConversation(agentId, null);
|
||||||
if (!createResult.isSuccess()) {
|
if (!createResult.isSuccess()) {
|
||||||
resultDomain.fail(createResult.getMessage());
|
emitter.send(SseEmitter.event().name("error").data(createResult.getMessage()));
|
||||||
return resultDomain;
|
emitter.complete();
|
||||||
|
return emitter;
|
||||||
}
|
}
|
||||||
conversation = createResult.getData();
|
conversation = createResult.getData();
|
||||||
conversationId = conversation.getID();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final String finalConversationId = conversation.getID();
|
||||||
|
|
||||||
// 5. 创建用户消息记录
|
// 5. 创建用户消息记录
|
||||||
TbAiMessage userMessage = new TbAiMessage();
|
TbAiMessage userMessage = new TbAiMessage();
|
||||||
userMessage.setID(UUID.randomUUID().toString());
|
userMessage.setID(UUID.randomUUID().toString());
|
||||||
userMessage.setConversationID(conversationId);
|
userMessage.setConversationID(finalConversationId);
|
||||||
userMessage.setAgentID(agentId);
|
userMessage.setAgentID(agentId);
|
||||||
userMessage.setRole("user");
|
userMessage.setRole("user");
|
||||||
userMessage.setContent(query);
|
userMessage.setContent(query);
|
||||||
userMessage.setCreateTime(new Date());
|
userMessage.setCreateTime(new Date());
|
||||||
userMessage.setUpdateTime(new Date());
|
userMessage.setUpdateTime(new Date());
|
||||||
userMessage.setDeleted(false);
|
userMessage.setDeleted(false);
|
||||||
|
userMessage.setUserID(currentUser.getID());
|
||||||
messageMapper.insertMessage(userMessage);
|
messageMapper.insertMessage(userMessage);
|
||||||
|
|
||||||
// 6. 创建AI回复消息记录(初始为空)
|
// 注意:AI消息记录将在获取到Dify的task_id后创建
|
||||||
TbAiMessage aiMessage = new TbAiMessage();
|
|
||||||
aiMessage.setID(UUID.randomUUID().toString());
|
|
||||||
aiMessage.setConversationID(conversationId);
|
|
||||||
aiMessage.setAgentID(agentId);
|
|
||||||
aiMessage.setRole("assistant");
|
|
||||||
aiMessage.setContent(""); // 初始为空,流式更新
|
|
||||||
aiMessage.setCreateTime(new Date());
|
|
||||||
aiMessage.setUpdateTime(new Date());
|
|
||||||
aiMessage.setDeleted(false);
|
|
||||||
|
|
||||||
messageMapper.insertMessage(aiMessage);
|
|
||||||
|
|
||||||
// 7. 构建Dify请求
|
// 7. 构建Dify请求
|
||||||
ChatRequest chatRequest = new ChatRequest();
|
ChatRequest chatRequest = new ChatRequest();
|
||||||
chatRequest.setQuery(query);
|
chatRequest.setQuery(query);
|
||||||
chatRequest.setUser(currentUser.getID());
|
chatRequest.setUser(currentUser.getID());
|
||||||
|
|
||||||
// 设置会话ID(如果是继续对话)
|
|
||||||
if (StringUtils.hasText(conversation.getDifyConversationId())) {
|
if (StringUtils.hasText(conversation.getDifyConversationId())) {
|
||||||
chatRequest.setConversationId(conversation.getDifyConversationId());
|
chatRequest.setConversationId(conversation.getDifyConversationId());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置知识库检索(如果指定)
|
|
||||||
if (knowledgeIds != null && !knowledgeIds.isEmpty()) {
|
if (knowledgeIds != null && !knowledgeIds.isEmpty()) {
|
||||||
chatRequest.setDatasetIds(knowledgeIds);
|
chatRequest.setDatasetIds(knowledgeIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用agent配置的参数
|
chatRequest.setTemperature(agent.getTemperature() != null ?
|
||||||
if (agent.getTemperature() != null) {
|
agent.getTemperature().doubleValue() : difyConfig.getChat().getDefaultTemperature());
|
||||||
chatRequest.setTemperature(agent.getTemperature().doubleValue());
|
chatRequest.setMaxTokens(agent.getMaxTokens() != null ?
|
||||||
} else {
|
agent.getMaxTokens() : difyConfig.getChat().getDefaultMaxTokens());
|
||||||
chatRequest.setTemperature(difyConfig.getChat().getDefaultTemperature());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (agent.getMaxTokens() != null) {
|
// 6. 调用Dify流式对话
|
||||||
chatRequest.setMaxTokens(agent.getMaxTokens());
|
final TbAiConversation finalConversation = conversation;
|
||||||
} else {
|
|
||||||
chatRequest.setMaxTokens(difyConfig.getChat().getDefaultMaxTokens());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 8. 调用Dify流式对话
|
|
||||||
final String finalConversationId = conversationId;
|
|
||||||
final String finalAiMessageId = aiMessage.getID();
|
|
||||||
StringBuilder fullAnswer = new StringBuilder();
|
StringBuilder fullAnswer = new StringBuilder();
|
||||||
AtomicReference<String> difyConversationId = new AtomicReference<>();
|
AtomicReference<String> difyConversationId = new AtomicReference<>();
|
||||||
AtomicReference<String> difyMessageId = new AtomicReference<>();
|
AtomicReference<String> difyMessageId = new AtomicReference<>();
|
||||||
|
AtomicReference<String> taskId = new AtomicReference<>(); // 用于存储Dify的task_id
|
||||||
try {
|
AtomicReference<Boolean> messageCreated = new AtomicReference<>(false); // 标记消息是否已创建
|
||||||
difyApiClient.streamChat(chatRequest, agent.getDifyApiKey(), new StreamCallback() {
|
AtomicReference<Boolean> isStopped = new AtomicReference<>(false); // 标记是否已停止
|
||||||
@Override
|
difyApiClient.streamChat(chatRequest, agent.getDifyApiKey(), new StreamCallback() {
|
||||||
public void onMessage(String message) {
|
@Override
|
||||||
fullAnswer.append(message);
|
public void onMessage(String message) {
|
||||||
// 转发给前端回调
|
if (isStopped.get()) {
|
||||||
if (callback != null) {
|
return; // 已停止,不再处理
|
||||||
callback.onMessage(message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
fullAnswer.append(message);
|
||||||
|
// 发送消息片段给前端
|
||||||
|
emitter.send(SseEmitter.event().name("message").data(message));
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
// SseEmitter已关闭(用户停止生成),标记为已停止
|
||||||
|
isStopped.set(true);
|
||||||
|
log.debug("SSE连接已关闭,停止发送消息");
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("发送SSE消息失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onMessageEnd(String metadata) {
|
public void onMessageEnd(String metadata) {
|
||||||
try {
|
if (isStopped.get()) {
|
||||||
// 解析metadata获取会话ID和消息ID
|
return; // 已停止,不再处理
|
||||||
JsonNode json = objectMapper.readTree(metadata);
|
}
|
||||||
if (json.has("conversation_id")) {
|
try {
|
||||||
difyConversationId.set(json.get("conversation_id").asText());
|
// 解析metadata
|
||||||
}
|
JsonNode json = objectMapper.readTree(metadata);
|
||||||
if (json.has("id")) {
|
if (json.has("conversation_id")) {
|
||||||
difyMessageId.set(json.get("id").asText());
|
difyConversationId.set(json.get("conversation_id").asText());
|
||||||
}
|
}
|
||||||
|
if (json.has("id")) {
|
||||||
|
difyMessageId.set(json.get("id").asText());
|
||||||
|
}
|
||||||
|
|
||||||
// 更新AI消息内容
|
// 更新AI消息内容(使用task_id作为消息ID)
|
||||||
|
if (taskId.get() != null) {
|
||||||
TbAiMessage updateMessage = new TbAiMessage();
|
TbAiMessage updateMessage = new TbAiMessage();
|
||||||
updateMessage.setID(finalAiMessageId);
|
updateMessage.setID(taskId.get());
|
||||||
updateMessage.setContent(fullAnswer.toString());
|
updateMessage.setContent(fullAnswer.toString());
|
||||||
updateMessage.setDifyMessageId(difyMessageId.get());
|
updateMessage.setDifyMessageId(difyMessageId.get());
|
||||||
updateMessage.setUpdateTime(new Date());
|
updateMessage.setUpdateTime(new Date());
|
||||||
messageMapper.updateMessage(updateMessage);
|
messageMapper.updateMessage(updateMessage);
|
||||||
|
|
||||||
// 更新会话的Dify会话ID
|
|
||||||
if (StringUtils.hasText(difyConversationId.get())) {
|
|
||||||
TbAiConversation updateConv = new TbAiConversation();
|
|
||||||
updateConv.setID(finalConversationId);
|
|
||||||
updateConv.setDifyConversationId(difyConversationId.get());
|
|
||||||
updateConv.setMessageCount((conversation.getMessageCount() != null ?
|
|
||||||
conversation.getMessageCount() : 0) + 2); // 用户问题+AI回答
|
|
||||||
updateConv.setUpdateTime(new Date());
|
|
||||||
conversationMapper.updateConversation(updateConv);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (callback != null) {
|
|
||||||
callback.onMessageEnd(metadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("处理流式响应metadata失败", e);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
// 更新会话的Dify会话ID
|
||||||
public void onComplete() {
|
if (StringUtils.hasText(difyConversationId.get())) {
|
||||||
log.info("流式对话完成: {} - {}", finalConversationId, finalAiMessageId);
|
TbAiConversation updateConv = new TbAiConversation();
|
||||||
if (callback != null) {
|
updateConv.setID(finalConversationId);
|
||||||
callback.onComplete();
|
updateConv.setDifyConversationId(difyConversationId.get());
|
||||||
|
updateConv.setMessageCount((finalConversation.getMessageCount() != null ?
|
||||||
|
finalConversation.getMessageCount() : 0) + 2);
|
||||||
|
updateConv.setUpdateTime(new Date());
|
||||||
|
conversationMapper.updateConversation(updateConv);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
// 发送结束事件(保持兼容)
|
||||||
public void onError(Throwable error) {
|
emitter.send(SseEmitter.event().name("end").data(metadata));
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
// SseEmitter已关闭,标记为已停止
|
||||||
|
isStopped.set(true);
|
||||||
|
log.debug("SSE连接已关闭,停止处理");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("处理流式响应metadata失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEvent(String eventType, String eventData) {
|
||||||
|
if (isStopped.get()) {
|
||||||
|
return; // 已停止,不再处理
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// 转发所有Dify原始事件到前端(包含task_id等完整信息)
|
||||||
|
emitter.send(SseEmitter.event().name("dify_" + eventType).data(eventData));
|
||||||
|
log.debug("转发Dify事件: {} - {}", eventType, eventData);
|
||||||
|
|
||||||
|
// 如果还没有创建消息记录,尝试从任何事件中提取task_id
|
||||||
|
if (!messageCreated.get()) {
|
||||||
|
JsonNode json = objectMapper.readTree(eventData);
|
||||||
|
if (json.has("task_id")) {
|
||||||
|
String difyTaskId = json.get("task_id").asText();
|
||||||
|
|
||||||
|
// 只有在taskId为空时才设置并创建消息
|
||||||
|
if (taskId.get() == null) {
|
||||||
|
taskId.set(difyTaskId);
|
||||||
|
|
||||||
|
// 使用task_id作为消息ID,创建AI消息记录
|
||||||
|
TbAiMessage aiMessage = new TbAiMessage();
|
||||||
|
aiMessage.setID(difyTaskId); // 使用Dify的task_id作为消息ID
|
||||||
|
aiMessage.setConversationID(finalConversationId);
|
||||||
|
aiMessage.setAgentID(agentId);
|
||||||
|
aiMessage.setRole("assistant");
|
||||||
|
aiMessage.setContent("");
|
||||||
|
aiMessage.setCreateTime(new Date());
|
||||||
|
aiMessage.setUpdateTime(new Date());
|
||||||
|
aiMessage.setDeleted(false);
|
||||||
|
aiMessage.setUserID("assistant");
|
||||||
|
messageMapper.insertMessage(aiMessage);
|
||||||
|
|
||||||
|
messageCreated.set(true);
|
||||||
|
log.info("AI消息记录已创建(从{}事件提取task_id): {}", eventType, difyTaskId);
|
||||||
|
|
||||||
|
// 发送init事件给前端,包含task_id(作为messageId)
|
||||||
|
Map<String, String> initData = new HashMap<>();
|
||||||
|
initData.put("conversationId", finalConversationId);
|
||||||
|
initData.put("messageId", difyTaskId);
|
||||||
|
emitter.send(SseEmitter.event().name("init").data(initData));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
// SseEmitter已关闭,标记为已停止
|
||||||
|
isStopped.set(true);
|
||||||
|
log.debug("SSE连接已关闭,停止转发事件");
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("转发Dify事件失败: {}", eventType, e);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("处理Dify事件异常: {}", eventType, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onComplete() {
|
||||||
|
if (isStopped.get()) {
|
||||||
|
log.debug("SSE连接已关闭,跳过完成处理");
|
||||||
|
return; // 已停止,不再处理
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
log.info("流式对话完成: {} - {}", finalConversationId, taskId.get());
|
||||||
|
emitter.send(SseEmitter.event().name("complete").data("对话完成"));
|
||||||
|
emitter.complete();
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
// SseEmitter已关闭
|
||||||
|
isStopped.set(true);
|
||||||
|
log.debug("SSE连接已关闭");
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("发送完成事件失败", e);
|
||||||
|
emitter.completeWithError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(Throwable error) {
|
||||||
|
if (isStopped.get()) {
|
||||||
|
log.debug("SSE连接已关闭,跳过错误处理");
|
||||||
|
return; // 已停止,不再处理
|
||||||
|
}
|
||||||
|
try {
|
||||||
log.error("流式对话失败", error);
|
log.error("流式对话失败", error);
|
||||||
if (callback != null) {
|
emitter.send(SseEmitter.event().name("error").data(error.getMessage()));
|
||||||
callback.onError(error);
|
emitter.completeWithError(error);
|
||||||
}
|
} catch (IllegalStateException e) {
|
||||||
|
// SseEmitter已关闭
|
||||||
|
isStopped.set(true);
|
||||||
|
log.debug("SSE连接已关闭");
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("发送错误事件失败", e);
|
||||||
|
emitter.completeWithError(e);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
|
||||||
resultDomain.success("对话成功", aiMessage);
|
// 设置超时和错误回调
|
||||||
return resultDomain;
|
emitter.onTimeout(() -> {
|
||||||
|
isStopped.set(true);
|
||||||
|
log.warn("SSE连接超时");
|
||||||
|
emitter.complete();
|
||||||
|
});
|
||||||
|
|
||||||
} catch (DifyException e) {
|
emitter.onError(e -> {
|
||||||
log.error("Dify对话失败", e);
|
isStopped.set(true);
|
||||||
resultDomain.fail("对话失败: " + e.getMessage());
|
log.error("SSE连接错误(可能是用户停止生成)", e);
|
||||||
return resultDomain;
|
emitter.completeWithError(e);
|
||||||
}
|
});
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("流式对话异常", e);
|
log.error("流式对话异常", e);
|
||||||
resultDomain.fail("对话异常: " + e.getMessage());
|
try {
|
||||||
return resultDomain;
|
emitter.send(SseEmitter.event().name("error").data(e.getMessage()));
|
||||||
|
emitter.completeWithError(e);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
log.error("发送异常事件失败", ex);
|
||||||
|
emitter.completeWithError(ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return emitter;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -396,24 +483,22 @@ public class AiChatServiceImpl implements AiChatService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ResultDomain<Boolean> stopChat(String messageId) {
|
public ResultDomain<Boolean> stopChatByTaskId(String taskId, String agentId) {
|
||||||
ResultDomain<Boolean> resultDomain = new ResultDomain<>();
|
ResultDomain<Boolean> resultDomain = new ResultDomain<>();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!StringUtils.hasText(messageId)) {
|
if (!StringUtils.hasText(taskId)) {
|
||||||
resultDomain.fail("消息ID不能为空");
|
resultDomain.fail("任务ID不能为空");
|
||||||
return resultDomain;
|
return resultDomain;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询消息
|
if (!StringUtils.hasText(agentId)) {
|
||||||
TbAiMessage message = messageMapper.selectMessageById(messageId);
|
resultDomain.fail("智能体ID不能为空");
|
||||||
if (message == null || message.getDeleted()) {
|
|
||||||
resultDomain.fail("消息不存在");
|
|
||||||
return resultDomain;
|
return resultDomain;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取智能体API Key
|
// 获取智能体API Key
|
||||||
TbAiAgentConfig agent = agentConfigMapper.selectAgentConfigById(message.getAgentID());
|
TbAiAgentConfig agent = agentConfigMapper.selectAgentConfigById(agentId);
|
||||||
if (agent == null) {
|
if (agent == null) {
|
||||||
resultDomain.fail("智能体不存在");
|
resultDomain.fail("智能体不存在");
|
||||||
return resultDomain;
|
return resultDomain;
|
||||||
@@ -421,21 +506,21 @@ public class AiChatServiceImpl implements AiChatService {
|
|||||||
|
|
||||||
// 调用Dify停止API
|
// 调用Dify停止API
|
||||||
TbSysUser currentUser = LoginUtil.getCurrentUser();
|
TbSysUser currentUser = LoginUtil.getCurrentUser();
|
||||||
if (currentUser != null && StringUtils.hasText(message.getDifyMessageId())) {
|
if (currentUser != null) {
|
||||||
try {
|
try {
|
||||||
difyApiClient.stopChatMessage(
|
difyApiClient.stopChatMessage(
|
||||||
message.getDifyMessageId(),
|
taskId,
|
||||||
currentUser.getID(),
|
currentUser.getID(),
|
||||||
agent.getDifyApiKey()
|
agent.getDifyApiKey()
|
||||||
);
|
);
|
||||||
log.info("对话停止成功: {}", messageId);
|
log.info("对话停止成功,task_id: {}", taskId);
|
||||||
resultDomain.success("停止成功", true);
|
resultDomain.success("停止成功", true);
|
||||||
} catch (DifyException e) {
|
} catch (DifyException e) {
|
||||||
log.error("停止对话失败", e);
|
log.error("停止对话失败", e);
|
||||||
resultDomain.fail("停止失败: " + e.getMessage());
|
resultDomain.fail("停止失败: " + e.getMessage());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
resultDomain.fail("消息未关联Dify或用户未登录");
|
resultDomain.fail("用户未登录");
|
||||||
}
|
}
|
||||||
|
|
||||||
return resultDomain;
|
return resultDomain;
|
||||||
@@ -618,8 +703,8 @@ public class AiChatServiceImpl implements AiChatService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ResultDomain<List<TbAiConversation>> listUserConversations(String agentId) {
|
public ResultDomain<TbAiConversation> listUserConversations(String agentId) {
|
||||||
ResultDomain<List<TbAiConversation>> resultDomain = new ResultDomain<>();
|
ResultDomain<TbAiConversation> resultDomain = new ResultDomain<>();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
TbSysUser currentUser = LoginUtil.getCurrentUser();
|
TbSysUser currentUser = LoginUtil.getCurrentUser();
|
||||||
@@ -643,8 +728,8 @@ public class AiChatServiceImpl implements AiChatService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ResultDomain<List<TbAiMessage>> listMessages(String conversationId) {
|
public ResultDomain<TbAiMessage> listMessages(String conversationId) {
|
||||||
ResultDomain<List<TbAiMessage>> resultDomain = new ResultDomain<>();
|
ResultDomain<TbAiMessage> resultDomain = new ResultDomain<>();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!StringUtils.hasText(conversationId)) {
|
if (!StringUtils.hasText(conversationId)) {
|
||||||
@@ -703,17 +788,17 @@ public class AiChatServiceImpl implements AiChatService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
public SseEmitter regenerateAnswerWithSse(String messageId) {
|
||||||
public ResultDomain<TbAiMessage> regenerateAnswer(String messageId, Object callbackObj) {
|
// 创建SseEmitter,设置超时时间为5分钟
|
||||||
ResultDomain<TbAiMessage> resultDomain = new ResultDomain<>();
|
SseEmitter emitter = new SseEmitter(5 * 60 * 1000L);
|
||||||
StreamCallback callback = (callbackObj instanceof StreamCallback) ? (StreamCallback) callbackObj : null;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 查询原消息
|
// 查询原消息
|
||||||
TbAiMessage originalMessage = messageMapper.selectMessageById(messageId);
|
TbAiMessage originalMessage = messageMapper.selectMessageById(messageId);
|
||||||
if (originalMessage == null || originalMessage.getDeleted()) {
|
if (originalMessage == null || originalMessage.getDeleted()) {
|
||||||
resultDomain.fail("消息不存在");
|
emitter.send(SseEmitter.event().name("error").data("消息不存在"));
|
||||||
return resultDomain;
|
emitter.complete();
|
||||||
|
return emitter;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 找到用户的原始问题(上一条消息)
|
// 找到用户的原始问题(上一条消息)
|
||||||
@@ -731,32 +816,29 @@ public class AiChatServiceImpl implements AiChatService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (userQuestion == null) {
|
if (userQuestion == null) {
|
||||||
resultDomain.fail("找不到原始问题");
|
emitter.send(SseEmitter.event().name("error").data("找不到原始问题"));
|
||||||
return resultDomain;
|
emitter.complete();
|
||||||
|
return emitter;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重新发起对话
|
// 直接返回streamChatWithSse的结果
|
||||||
if (callback != null) {
|
return streamChatWithSse(
|
||||||
return streamChat(
|
originalMessage.getAgentID(),
|
||||||
originalMessage.getAgentID(),
|
originalMessage.getConversationID(),
|
||||||
originalMessage.getConversationID(),
|
userQuestion.getContent(),
|
||||||
userQuestion.getContent(),
|
null
|
||||||
null,
|
);
|
||||||
callback
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return blockingChat(
|
|
||||||
originalMessage.getAgentID(),
|
|
||||||
originalMessage.getConversationID(),
|
|
||||||
userQuestion.getContent(),
|
|
||||||
null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("重新生成回答异常", e);
|
log.error("重新生成回答异常", e);
|
||||||
resultDomain.fail("重新生成失败: " + e.getMessage());
|
try {
|
||||||
return resultDomain;
|
emitter.send(SseEmitter.event().name("error").data(e.getMessage()));
|
||||||
|
emitter.completeWithError(e);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
log.error("发送异常事件失败", ex);
|
||||||
|
emitter.completeWithError(ex);
|
||||||
|
}
|
||||||
|
return emitter;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -829,7 +911,49 @@ public class AiChatServiceImpl implements AiChatService {
|
|||||||
return resultDomain;
|
return resultDomain;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新评价
|
// 获取智能体配置(需要API Key)
|
||||||
|
TbAiAgentConfig agent = agentConfigMapper.selectAgentConfigById(message.getAgentID());
|
||||||
|
if (agent == null) {
|
||||||
|
resultDomain.fail("智能体不存在");
|
||||||
|
return resultDomain;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前用户
|
||||||
|
TbSysUser currentUser = LoginUtil.getCurrentUser();
|
||||||
|
if (currentUser == null) {
|
||||||
|
resultDomain.fail("用户未登录");
|
||||||
|
return resultDomain;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有Dify消息ID,同步反馈到Dify
|
||||||
|
if (StringUtils.hasText(message.getDifyMessageId())) {
|
||||||
|
try {
|
||||||
|
// 将评分转换为Dify格式:1=like, -1=dislike, 0=null
|
||||||
|
String difyRating = null;
|
||||||
|
if (rating != null) {
|
||||||
|
if (rating > 0) {
|
||||||
|
difyRating = "like";
|
||||||
|
} else if (rating < 0) {
|
||||||
|
difyRating = "dislike";
|
||||||
|
}
|
||||||
|
// rating == 0 时,difyRating 为 null(取消评价)
|
||||||
|
}
|
||||||
|
|
||||||
|
difyApiClient.submitMessageFeedback(
|
||||||
|
message.getDifyMessageId(),
|
||||||
|
difyRating,
|
||||||
|
currentUser.getID(),
|
||||||
|
feedback,
|
||||||
|
agent.getDifyApiKey()
|
||||||
|
);
|
||||||
|
log.info("Dify消息反馈提交成功: {} - {}", message.getDifyMessageId(), difyRating);
|
||||||
|
} catch (DifyException e) {
|
||||||
|
log.error("提交Dify反馈失败", e);
|
||||||
|
// 不影响本地评价,继续执行
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新本地评价
|
||||||
TbAiMessage update = new TbAiMessage();
|
TbAiMessage update = new TbAiMessage();
|
||||||
update.setID(messageId);
|
update.setID(messageId);
|
||||||
update.setRating(rating);
|
update.setRating(rating);
|
||||||
|
|||||||
@@ -354,8 +354,8 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ResultDomain<List<TbAiKnowledge>> listKnowledges(TbAiKnowledge filter) {
|
public ResultDomain<TbAiKnowledge> listKnowledges(TbAiKnowledge filter) {
|
||||||
ResultDomain<List<TbAiKnowledge>> resultDomain = new ResultDomain<>();
|
ResultDomain<TbAiKnowledge> resultDomain = new ResultDomain<>();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole();
|
List<UserDeptRoleVO> userDeptRoles = LoginUtil.getCurrentDeptRole();
|
||||||
|
|||||||
@@ -186,12 +186,12 @@ public class AiUploadFileServiceImpl implements AiUploadFileService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public ResultDomain<List<TbAiUploadFile>> batchUploadToKnowledge(
|
public ResultDomain<TbAiUploadFile> batchUploadToKnowledge(
|
||||||
String knowledgeId,
|
String knowledgeId,
|
||||||
List<MultipartFile> files,
|
List<MultipartFile> files,
|
||||||
String indexingTechnique) {
|
String indexingTechnique) {
|
||||||
|
|
||||||
ResultDomain<List<TbAiUploadFile>> resultDomain = new ResultDomain<>();
|
ResultDomain<TbAiUploadFile> resultDomain = new ResultDomain<>();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (files == null || files.isEmpty()) {
|
if (files == null || files.isEmpty()) {
|
||||||
@@ -322,8 +322,8 @@ public class AiUploadFileServiceImpl implements AiUploadFileService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ResultDomain<List<TbAiUploadFile>> listFilesByKnowledge(String knowledgeId) {
|
public ResultDomain<TbAiUploadFile> listFilesByKnowledge(String knowledgeId) {
|
||||||
ResultDomain<List<TbAiUploadFile>> resultDomain = new ResultDomain<>();
|
ResultDomain<TbAiUploadFile> resultDomain = new ResultDomain<>();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!StringUtils.hasText(knowledgeId)) {
|
if (!StringUtils.hasText(knowledgeId)) {
|
||||||
@@ -443,8 +443,8 @@ public class AiUploadFileServiceImpl implements AiUploadFileService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ResultDomain<List<TbAiUploadFile>> syncKnowledgeFiles(String knowledgeId) {
|
public ResultDomain<TbAiUploadFile> syncKnowledgeFiles(String knowledgeId) {
|
||||||
ResultDomain<List<TbAiUploadFile>> resultDomain = new ResultDomain<>();
|
ResultDomain<TbAiUploadFile> resultDomain = new ResultDomain<>();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 查询知识库的所有文件
|
// 查询知识库的所有文件
|
||||||
|
|||||||
@@ -87,10 +87,7 @@
|
|||||||
|
|
||||||
<!-- 逻辑删除会话 -->
|
<!-- 逻辑删除会话 -->
|
||||||
<update id="deleteConversation" parameterType="org.xyzh.common.dto.ai.TbAiConversation">
|
<update id="deleteConversation" parameterType="org.xyzh.common.dto.ai.TbAiConversation">
|
||||||
UPDATE tb_ai_conversation
|
DELETE FROM tb_ai_conversation WHERE id = #{ID}
|
||||||
SET deleted = 1,
|
|
||||||
delete_time = NOW()
|
|
||||||
WHERE id = #{ID} AND deleted = 0
|
|
||||||
</update>
|
</update>
|
||||||
|
|
||||||
<!-- 根据ID查询会话 -->
|
<!-- 根据ID查询会话 -->
|
||||||
|
|||||||
@@ -48,14 +48,14 @@ public interface AiAgentConfigService {
|
|||||||
* 查询所有启用的智能体列表
|
* 查询所有启用的智能体列表
|
||||||
* @return 智能体列表
|
* @return 智能体列表
|
||||||
*/
|
*/
|
||||||
ResultDomain<List<TbAiAgentConfig>> listEnabledAgents();
|
ResultDomain<TbAiAgentConfig> listEnabledAgents();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询智能体列表(支持过滤)
|
* 查询智能体列表(支持过滤)
|
||||||
* @param filter 过滤条件
|
* @param filter 过滤条件
|
||||||
* @return 智能体列表
|
* @return 智能体列表
|
||||||
*/
|
*/
|
||||||
ResultDomain<List<TbAiAgentConfig>> listAgents(TbAiAgentConfig filter);
|
ResultDomain<TbAiAgentConfig> listAgents(TbAiAgentConfig filter);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页查询智能体列表
|
* 分页查询智能体列表
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.xyzh.api.ai.chat;
|
package org.xyzh.api.ai.chat;
|
||||||
|
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
import org.xyzh.common.core.domain.ResultDomain;
|
import org.xyzh.common.core.domain.ResultDomain;
|
||||||
import org.xyzh.common.dto.ai.TbAiConversation;
|
import org.xyzh.common.dto.ai.TbAiConversation;
|
||||||
import org.xyzh.common.dto.ai.TbAiMessage;
|
import org.xyzh.common.dto.ai.TbAiMessage;
|
||||||
@@ -16,20 +17,18 @@ import java.util.List;
|
|||||||
public interface AiChatService {
|
public interface AiChatService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 流式对话(SSE)
|
* 流式对话(SSE)- 使用SseEmitter实现真正的流式推送
|
||||||
* @param agentId 智能体ID
|
* @param agentId 智能体ID
|
||||||
* @param conversationId 会话ID(可选,为空则创建新会话)
|
* @param conversationId 会话ID(可选,为空则创建新会话)
|
||||||
* @param query 用户问题
|
* @param query 用户问题
|
||||||
* @param knowledgeIds 使用的知识库ID列表(可选,用于知识库隔离)
|
* @param knowledgeIds 使用的知识库ID列表(可选,用于知识库隔离)
|
||||||
* @param callback 流式响应回调(StreamCallback类型,需在实现层处理)
|
* @return SseEmitter 流式推送对象
|
||||||
* @return 对话结果(包含会话ID和消息ID)
|
|
||||||
*/
|
*/
|
||||||
ResultDomain<TbAiMessage> streamChat(
|
SseEmitter streamChatWithSse(
|
||||||
String agentId,
|
String agentId,
|
||||||
String conversationId,
|
String conversationId,
|
||||||
String query,
|
String query,
|
||||||
List<String> knowledgeIds,
|
List<String> knowledgeIds
|
||||||
Object callback // 使用Object避免跨模块依赖
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,12 +46,15 @@ public interface AiChatService {
|
|||||||
List<String> knowledgeIds
|
List<String> knowledgeIds
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 停止对话生成
|
* 停止对话生成(通过Dify TaskID)
|
||||||
* @param messageId 消息ID
|
* @param taskId Dify任务ID
|
||||||
|
* @param agentId 智能体ID
|
||||||
* @return 停止结果
|
* @return 停止结果
|
||||||
*/
|
*/
|
||||||
ResultDomain<Boolean> stopChat(String messageId);
|
ResultDomain<Boolean> stopChatByTaskId(String taskId, String agentId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建新会话
|
* 创建新会话
|
||||||
@@ -88,14 +90,14 @@ public interface AiChatService {
|
|||||||
* @param agentId 智能体ID(可选)
|
* @param agentId 智能体ID(可选)
|
||||||
* @return 会话列表
|
* @return 会话列表
|
||||||
*/
|
*/
|
||||||
ResultDomain<List<TbAiConversation>> listUserConversations(String agentId);
|
ResultDomain<TbAiConversation> listUserConversations(String agentId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询会话的消息列表
|
* 查询会话的消息列表
|
||||||
* @param conversationId 会话ID
|
* @param conversationId 会话ID
|
||||||
* @return 消息列表
|
* @return 消息列表
|
||||||
*/
|
*/
|
||||||
ResultDomain<List<TbAiMessage>> listMessages(String conversationId);
|
ResultDomain<TbAiMessage> listMessages(String conversationId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取单条消息
|
* 获取单条消息
|
||||||
@@ -105,12 +107,11 @@ public interface AiChatService {
|
|||||||
ResultDomain<TbAiMessage> getMessage(String messageId);
|
ResultDomain<TbAiMessage> getMessage(String messageId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 重新生成回答
|
* 重新生成回答(SSE流式)
|
||||||
* @param messageId 原消息ID
|
* @param messageId 原消息ID
|
||||||
* @param callback 流式回调(可选,StreamCallback类型)
|
* @return SseEmitter 流式推送对象
|
||||||
* @return 新消息
|
|
||||||
*/
|
*/
|
||||||
ResultDomain<TbAiMessage> regenerateAnswer(String messageId, Object callback);
|
SseEmitter regenerateAnswerWithSse(String messageId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 异步生成会话摘要
|
* 异步生成会话摘要
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ public interface AiUploadFileService {
|
|||||||
* @param indexingTechnique 索引方式(可选)
|
* @param indexingTechnique 索引方式(可选)
|
||||||
* @return 上传结果列表
|
* @return 上传结果列表
|
||||||
*/
|
*/
|
||||||
ResultDomain<List<TbAiUploadFile>> batchUploadToKnowledge(
|
ResultDomain<TbAiUploadFile> batchUploadToKnowledge(
|
||||||
String knowledgeId,
|
String knowledgeId,
|
||||||
List<MultipartFile> files,
|
List<MultipartFile> files,
|
||||||
String indexingTechnique
|
String indexingTechnique
|
||||||
@@ -69,7 +69,7 @@ public interface AiUploadFileService {
|
|||||||
* @param knowledgeId 知识库ID
|
* @param knowledgeId 知识库ID
|
||||||
* @return 文件列表
|
* @return 文件列表
|
||||||
*/
|
*/
|
||||||
ResultDomain<List<TbAiUploadFile>> listFilesByKnowledge(String knowledgeId);
|
ResultDomain<TbAiUploadFile> listFilesByKnowledge(String knowledgeId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页查询文件列表
|
* 分页查询文件列表
|
||||||
@@ -91,5 +91,5 @@ public interface AiUploadFileService {
|
|||||||
* @param knowledgeId 知识库ID
|
* @param knowledgeId 知识库ID
|
||||||
* @return 同步结果
|
* @return 同步结果
|
||||||
*/
|
*/
|
||||||
ResultDomain<List<TbAiUploadFile>> syncKnowledgeFiles(String knowledgeId);
|
ResultDomain<TbAiUploadFile> syncKnowledgeFiles(String knowledgeId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ public interface AiKnowledgeService {
|
|||||||
* @param filter 过滤条件
|
* @param filter 过滤条件
|
||||||
* @return 知识库列表
|
* @return 知识库列表
|
||||||
*/
|
*/
|
||||||
ResultDomain<List<TbAiKnowledge>> listKnowledges(TbAiKnowledge filter);
|
ResultDomain<TbAiKnowledge> listKnowledges(TbAiKnowledge filter);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页查询知识库
|
* 分页查询知识库
|
||||||
|
|||||||
@@ -104,16 +104,34 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 从请求中获取Token
|
* @description 从请求中获取Token
|
||||||
|
* 支持以下方式获取Token:
|
||||||
|
* 1. Header中的Authorization: Bearer {token}
|
||||||
|
* 2. URL参数中的token: ?token={token}
|
||||||
|
* 3. URL参数中的access_token: ?access_token={token}
|
||||||
* @param request HTTP请求
|
* @param request HTTP请求
|
||||||
* @return String Token
|
* @return String Token
|
||||||
* @author yslg
|
* @author yslg
|
||||||
* @since 2025-09-28
|
* @since 2025-09-28
|
||||||
*/
|
*/
|
||||||
private String getTokenFromRequest(HttpServletRequest request) {
|
private String getTokenFromRequest(HttpServletRequest request) {
|
||||||
|
// 1. 优先从Header中获取Token
|
||||||
String bearerToken = request.getHeader("Authorization");
|
String bearerToken = request.getHeader("Authorization");
|
||||||
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
|
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
|
||||||
return bearerToken.substring(7);
|
return bearerToken.substring(7);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. 从URL参数token中获取
|
||||||
|
String token = request.getParameter("token");
|
||||||
|
if (StringUtils.hasText(token)) {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 从URL参数access_token中获取
|
||||||
|
String accessToken = request.getParameter("access_token");
|
||||||
|
if (StringUtils.hasText(accessToken)) {
|
||||||
|
return accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -62,10 +62,10 @@ public class LearningHistoryController {
|
|||||||
* POST /study/history/list
|
* POST /study/history/list
|
||||||
*
|
*
|
||||||
* @param filter 过滤条件
|
* @param filter 过滤条件
|
||||||
* @return ResultDomain<List<LearningHistoryVO>> 学习历史列表
|
* @return ResultDomain<LearningHistoryVO> 学习历史列表
|
||||||
*/
|
*/
|
||||||
@PostMapping("/list")
|
@PostMapping("/list")
|
||||||
public ResultDomain<List<LearningHistoryVO>> getLearningHistories(@RequestBody TbLearningHistory filter) {
|
public ResultDomain<LearningHistoryVO> getLearningHistories(@RequestBody TbLearningHistory filter) {
|
||||||
logger.info("查询学习历史列表,用户ID: {}", filter != null ? filter.getUserID() : null);
|
logger.info("查询学习历史列表,用户ID: {}", filter != null ? filter.getUserID() : null);
|
||||||
return learningHistoryService.getLearningHistories(filter);
|
return learningHistoryService.getLearningHistories(filter);
|
||||||
}
|
}
|
||||||
@@ -103,10 +103,10 @@ public class LearningHistoryController {
|
|||||||
* POST /study/history/my-histories
|
* POST /study/history/my-histories
|
||||||
*
|
*
|
||||||
* @param filter 过滤条件
|
* @param filter 过滤条件
|
||||||
* @return ResultDomain<List<LearningHistoryVO>> 学习历史列表
|
* @return ResultDomain<LearningHistoryVO> 学习历史列表
|
||||||
*/
|
*/
|
||||||
@PostMapping("/my-histories")
|
@PostMapping("/my-histories")
|
||||||
public ResultDomain<List<LearningHistoryVO>> getCurrentUserLearningHistories(@RequestBody(required = false) TbLearningHistory filter) {
|
public ResultDomain<LearningHistoryVO> getCurrentUserLearningHistories(@RequestBody(required = false) TbLearningHistory filter) {
|
||||||
logger.info("查询当前用户学习历史");
|
logger.info("查询当前用户学习历史");
|
||||||
return learningHistoryService.getCurrentUserLearningHistories(filter);
|
return learningHistoryService.getCurrentUserLearningHistories(filter);
|
||||||
}
|
}
|
||||||
@@ -116,10 +116,10 @@ public class LearningHistoryController {
|
|||||||
* GET /study/history/recent
|
* GET /study/history/recent
|
||||||
*
|
*
|
||||||
* @param limit 限制数量(可选,默认10)
|
* @param limit 限制数量(可选,默认10)
|
||||||
* @return ResultDomain<List<LearningHistoryVO>> 学习历史列表
|
* @return ResultDomain<LearningHistoryVO> 学习历史列表
|
||||||
*/
|
*/
|
||||||
@GetMapping("/recent")
|
@GetMapping("/recent")
|
||||||
public ResultDomain<List<LearningHistoryVO>> getRecentLearningHistories(@RequestParam(required = false, defaultValue = "10") Integer limit) {
|
public ResultDomain<LearningHistoryVO> getRecentLearningHistories(@RequestParam(required = false, defaultValue = "10") Integer limit) {
|
||||||
logger.info("查询最近学习历史,限制数量: {}", limit);
|
logger.info("查询最近学习历史,限制数量: {}", limit);
|
||||||
// 从当前登录用户获取
|
// 从当前登录用户获取
|
||||||
return learningHistoryService.getRecentLearningHistories(null, limit);
|
return learningHistoryService.getRecentLearningHistories(null, limit);
|
||||||
|
|||||||
@@ -23,11 +23,11 @@ public interface SCLearningHistoryService extends LearningHistoryAPI {
|
|||||||
/**
|
/**
|
||||||
* @description 查询学习历史列表
|
* @description 查询学习历史列表
|
||||||
* @param filter 过滤条件
|
* @param filter 过滤条件
|
||||||
* @return ResultDomain<List<LearningHistoryVO>> 学习历史列表
|
* @return ResultDomain<LearningHistoryVO> 学习历史列表
|
||||||
* @author yslg
|
* @author yslg
|
||||||
* @since 2025-10-27
|
* @since 2025-10-27
|
||||||
*/
|
*/
|
||||||
ResultDomain<List<LearningHistoryVO>> getLearningHistories(TbLearningHistory filter);
|
ResultDomain<LearningHistoryVO> getLearningHistories(TbLearningHistory filter);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 分页查询学习历史
|
* @description 分页查询学习历史
|
||||||
@@ -71,11 +71,11 @@ public interface SCLearningHistoryService extends LearningHistoryAPI {
|
|||||||
/**
|
/**
|
||||||
* @description 获取当前用户的学习历史
|
* @description 获取当前用户的学习历史
|
||||||
* @param filter 过滤条件
|
* @param filter 过滤条件
|
||||||
* @return ResultDomain<List<LearningHistoryVO>> 学习历史列表
|
* @return ResultDomain<LearningHistoryVO> 学习历史列表
|
||||||
* @author yslg
|
* @author yslg
|
||||||
* @since 2025-10-27
|
* @since 2025-10-27
|
||||||
*/
|
*/
|
||||||
ResultDomain<List<LearningHistoryVO>> getCurrentUserLearningHistories(TbLearningHistory filter);
|
ResultDomain<LearningHistoryVO> getCurrentUserLearningHistories(TbLearningHistory filter);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 获取当前用户的学习统计
|
* @description 获取当前用户的学习统计
|
||||||
@@ -108,10 +108,10 @@ public interface SCLearningHistoryService extends LearningHistoryAPI {
|
|||||||
* @description 获取用户最近的学习历史
|
* @description 获取用户最近的学习历史
|
||||||
* @param userId 用户ID
|
* @param userId 用户ID
|
||||||
* @param limit 限制数量
|
* @param limit 限制数量
|
||||||
* @return ResultDomain<List<LearningHistoryVO>> 学习历史列表
|
* @return ResultDomain<LearningHistoryVO> 学习历史列表
|
||||||
* @author yslg
|
* @author yslg
|
||||||
* @since 2025-10-27
|
* @since 2025-10-27
|
||||||
*/
|
*/
|
||||||
ResultDomain<List<LearningHistoryVO>> getRecentLearningHistories(String userId, Integer limit);
|
ResultDomain<LearningHistoryVO> getRecentLearningHistories(String userId, Integer limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -176,8 +176,8 @@ public class SCLearningHistoryServiceImpl implements SCLearningHistoryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ResultDomain<List<LearningHistoryVO>> getLearningHistories(TbLearningHistory filter) {
|
public ResultDomain<LearningHistoryVO> getLearningHistories(TbLearningHistory filter) {
|
||||||
ResultDomain<List<LearningHistoryVO>> resultDomain = new ResultDomain<>();
|
ResultDomain<LearningHistoryVO> resultDomain = new ResultDomain<>();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
List<LearningHistoryVO> historyVOList = learningHistoryMapper.selectLearningHistoriesWithDetails(filter);
|
List<LearningHistoryVO> historyVOList = learningHistoryMapper.selectLearningHistoriesWithDetails(filter);
|
||||||
@@ -351,8 +351,8 @@ public class SCLearningHistoryServiceImpl implements SCLearningHistoryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ResultDomain<List<LearningHistoryVO>> getCurrentUserLearningHistories(TbLearningHistory filter) {
|
public ResultDomain<LearningHistoryVO> getCurrentUserLearningHistories(TbLearningHistory filter) {
|
||||||
ResultDomain<List<LearningHistoryVO>> resultDomain = new ResultDomain<>();
|
ResultDomain<LearningHistoryVO> resultDomain = new ResultDomain<>();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
TbSysUser currentUser = LoginUtil.getCurrentUser();
|
TbSysUser currentUser = LoginUtil.getCurrentUser();
|
||||||
@@ -449,8 +449,8 @@ public class SCLearningHistoryServiceImpl implements SCLearningHistoryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ResultDomain<List<LearningHistoryVO>> getRecentLearningHistories(String userId, Integer limit) {
|
public ResultDomain<LearningHistoryVO> getRecentLearningHistories(String userId, Integer limit) {
|
||||||
ResultDomain<List<LearningHistoryVO>> resultDomain = new ResultDomain<>();
|
ResultDomain<LearningHistoryVO> resultDomain = new ResultDomain<>();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (userId == null || userId.isEmpty()) {
|
if (userId == null || userId.isEmpty()) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import org.apache.logging.log4j.core.config.plugins.Plugin;
|
|||||||
import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
|
import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
|
||||||
import org.apache.logging.log4j.core.config.plugins.PluginElement;
|
import org.apache.logging.log4j.core.config.plugins.PluginElement;
|
||||||
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
|
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
|
||||||
|
import org.xyzh.common.utils.StringUtils;
|
||||||
import org.xyzh.common.utils.spring.SpringContextUtil;
|
import org.xyzh.common.utils.spring.SpringContextUtil;
|
||||||
|
|
||||||
import java.util.concurrent.ArrayBlockingQueue;
|
import java.util.concurrent.ArrayBlockingQueue;
|
||||||
@@ -78,12 +79,20 @@ public class DatabaseAppender extends AbstractAppender {
|
|||||||
// 尝试获取请求信息
|
// 尝试获取请求信息
|
||||||
Class<?> servletUtilsClass = Class.forName("org.xyzh.common.utils.ServletUtils");
|
Class<?> servletUtilsClass = Class.forName("org.xyzh.common.utils.ServletUtils");
|
||||||
requestUrl = (String) servletUtilsClass.getMethod("getRequestUrl").invoke(null);
|
requestUrl = (String) servletUtilsClass.getMethod("getRequestUrl").invoke(null);
|
||||||
|
if(StringUtils.isNotBlank(requestUrl)){
|
||||||
|
// 移除URL参数部分(使用indexOf避免正则表达式问题)
|
||||||
|
int questionMarkIndex = requestUrl.indexOf('?');
|
||||||
|
if(questionMarkIndex != -1) {
|
||||||
|
requestUrl = requestUrl.substring(0, questionMarkIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
requestMethod = (String) servletUtilsClass.getMethod("getHeader", String.class).invoke(null, "X-HTTP-Method-Override");
|
requestMethod = (String) servletUtilsClass.getMethod("getHeader", String.class).invoke(null, "X-HTTP-Method-Override");
|
||||||
ipAddress = (String) servletUtilsClass.getMethod("getClientIp").invoke(null);
|
ipAddress = (String) servletUtilsClass.getMethod("getClientIp").invoke(null);
|
||||||
ipSource = (String) servletUtilsClass.getMethod("getIpSource").invoke(null);
|
ipSource = (String) servletUtilsClass.getMethod("getIpSource").invoke(null);
|
||||||
browser = (String) servletUtilsClass.getMethod("getBrowser").invoke(null);
|
browser = (String) servletUtilsClass.getMethod("getBrowser").invoke(null);
|
||||||
os = (String) servletUtilsClass.getMethod("getOs").invoke(null);
|
os = (String) servletUtilsClass.getMethod("getOs").invoke(null);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
// 非HTTP请求上下文或类不存在,忽略
|
// 非HTTP请求上下文或类不存在,忽略
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,8 +55,8 @@ export const aiAgentConfigApi = {
|
|||||||
* 获取启用的智能体列表
|
* 获取启用的智能体列表
|
||||||
* @returns Promise<ResultDomain<AiAgentConfig[]>>
|
* @returns Promise<ResultDomain<AiAgentConfig[]>>
|
||||||
*/
|
*/
|
||||||
async listEnabledAgents(): Promise<ResultDomain<AiAgentConfig[]>> {
|
async listEnabledAgents(): Promise<ResultDomain<AiAgentConfig>> {
|
||||||
const response = await api.get<AiAgentConfig[]>('/ai/agent/enabled');
|
const response = await api.get<AiAgentConfig>('/ai/agent/enabled');
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -76,8 +76,8 @@ export const aiAgentConfigApi = {
|
|||||||
* @param pageParam 分页参数
|
* @param pageParam 分页参数
|
||||||
* @returns Promise<PageDomain<AiAgentConfig>>
|
* @returns Promise<PageDomain<AiAgentConfig>>
|
||||||
*/
|
*/
|
||||||
async pageAgents(filter: Partial<AiAgentConfig>, pageParam: PageParam): Promise<PageDomain<AiAgentConfig>> {
|
async pageAgents(filter: Partial<AiAgentConfig>, pageParam: PageParam): Promise<ResultDomain<AiAgentConfig>> {
|
||||||
const response = await api.post<PageDomain<AiAgentConfig>>('/ai/agent/page', {
|
const response = await api.post<AiAgentConfig>('/ai/agent/page', {
|
||||||
filter,
|
filter,
|
||||||
pageParam
|
pageParam
|
||||||
});
|
});
|
||||||
@@ -91,7 +91,9 @@ export const aiAgentConfigApi = {
|
|||||||
* @returns Promise<ResultDomain<boolean>>
|
* @returns Promise<ResultDomain<boolean>>
|
||||||
*/
|
*/
|
||||||
async updateAgentStatus(agentId: string, status: number): Promise<ResultDomain<boolean>> {
|
async updateAgentStatus(agentId: string, status: number): Promise<ResultDomain<boolean>> {
|
||||||
const response = await api.put<boolean>(`/ai/agent/${agentId}/status`, { status });
|
const response = await api.put<boolean>(`/ai/agent/${agentId}/status`, null, {
|
||||||
|
params: { status }
|
||||||
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -103,9 +105,8 @@ export const aiAgentConfigApi = {
|
|||||||
* @returns Promise<ResultDomain<boolean>>
|
* @returns Promise<ResultDomain<boolean>>
|
||||||
*/
|
*/
|
||||||
async updateDifyConfig(agentId: string, difyAppId: string, difyApiKey: string): Promise<ResultDomain<boolean>> {
|
async updateDifyConfig(agentId: string, difyAppId: string, difyApiKey: string): Promise<ResultDomain<boolean>> {
|
||||||
const response = await api.put<boolean>(`/ai/agent/${agentId}/dify`, {
|
const response = await api.put<boolean>(`/ai/agent/${agentId}/dify`, null, {
|
||||||
difyAppId,
|
params: { difyAppId, difyApiKey }
|
||||||
difyApiKey
|
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,12 +7,8 @@
|
|||||||
import { api } from '@/apis/index';
|
import { api } from '@/apis/index';
|
||||||
import type {
|
import type {
|
||||||
AiConversation,
|
AiConversation,
|
||||||
AiMessage,
|
|
||||||
ConversationSearchParams,
|
ConversationSearchParams,
|
||||||
MessageSearchParams,
|
MessageSearchParams,
|
||||||
UserChatStatistics,
|
|
||||||
ConversationStatistics,
|
|
||||||
BatchExportParams,
|
|
||||||
ResultDomain,
|
ResultDomain,
|
||||||
PageDomain
|
PageDomain
|
||||||
} from '@/types';
|
} from '@/types';
|
||||||
@@ -27,8 +23,9 @@ export const chatHistoryApi = {
|
|||||||
* @returns Promise<PageDomain<AiConversation>>
|
* @returns Promise<PageDomain<AiConversation>>
|
||||||
*/
|
*/
|
||||||
async pageUserConversations(params: ConversationSearchParams): Promise<PageDomain<AiConversation>> {
|
async pageUserConversations(params: ConversationSearchParams): Promise<PageDomain<AiConversation>> {
|
||||||
const response = await api.post<PageDomain<AiConversation>>('/ai/history/conversations/page', params);
|
// 后端直接返回PageDomain,不需要再次包装
|
||||||
return response.data;
|
const response = await api.post('/ai/chat/history/conversations/page', params);
|
||||||
|
return response.data as PageDomain<AiConversation>;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,18 +34,9 @@ export const chatHistoryApi = {
|
|||||||
* @returns Promise<PageDomain<AiConversation>>
|
* @returns Promise<PageDomain<AiConversation>>
|
||||||
*/
|
*/
|
||||||
async searchConversations(params: MessageSearchParams): Promise<PageDomain<AiConversation>> {
|
async searchConversations(params: MessageSearchParams): Promise<PageDomain<AiConversation>> {
|
||||||
const response = await api.post<PageDomain<AiConversation>>('/ai/history/conversations/search', params);
|
// 后端直接返回PageDomain,不需要再次包装
|
||||||
return response.data;
|
const response = await api.post('/ai/chat/history/search', params);
|
||||||
},
|
return response.data as PageDomain<AiConversation>;
|
||||||
|
|
||||||
/**
|
|
||||||
* 搜索消息内容(全文搜索)
|
|
||||||
* @param params 搜索参数
|
|
||||||
* @returns Promise<PageDomain<AiMessage>>
|
|
||||||
*/
|
|
||||||
async searchMessages(params: MessageSearchParams): Promise<PageDomain<AiMessage>> {
|
|
||||||
const response = await api.post<PageDomain<AiMessage>>('/ai/history/messages/search', params);
|
|
||||||
return response.data;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -58,7 +46,8 @@ export const chatHistoryApi = {
|
|||||||
* @returns Promise<ResultDomain<boolean>>
|
* @returns Promise<ResultDomain<boolean>>
|
||||||
*/
|
*/
|
||||||
async toggleFavorite(conversationId: string, isFavorite: boolean): Promise<ResultDomain<boolean>> {
|
async toggleFavorite(conversationId: string, isFavorite: boolean): Promise<ResultDomain<boolean>> {
|
||||||
const response = await api.put<boolean>(`/ai/history/conversation/${conversationId}/favorite`, {
|
const response = await api.put<boolean>('/ai/chat/history/conversation/favorite', {
|
||||||
|
conversationId,
|
||||||
isFavorite
|
isFavorite
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
@@ -71,7 +60,8 @@ export const chatHistoryApi = {
|
|||||||
* @returns Promise<ResultDomain<boolean>>
|
* @returns Promise<ResultDomain<boolean>>
|
||||||
*/
|
*/
|
||||||
async togglePin(conversationId: string, isPinned: boolean): Promise<ResultDomain<boolean>> {
|
async togglePin(conversationId: string, isPinned: boolean): Promise<ResultDomain<boolean>> {
|
||||||
const response = await api.put<boolean>(`/ai/history/conversation/${conversationId}/pin`, {
|
const response = await api.put<boolean>('/ai/chat/history/conversation/pin', {
|
||||||
|
conversationId,
|
||||||
isPinned
|
isPinned
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
@@ -83,8 +73,8 @@ export const chatHistoryApi = {
|
|||||||
* @returns Promise<ResultDomain<number>>
|
* @returns Promise<ResultDomain<number>>
|
||||||
*/
|
*/
|
||||||
async batchDeleteConversations(conversationIds: string[]): Promise<ResultDomain<number>> {
|
async batchDeleteConversations(conversationIds: string[]): Promise<ResultDomain<number>> {
|
||||||
const response = await api.post<number>('/ai/history/conversations/batch-delete', {
|
const response = await api.delete<number>('/ai/chat/history/conversations/batch', {
|
||||||
conversationIds
|
data: { conversationIds }
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
@@ -92,10 +82,10 @@ export const chatHistoryApi = {
|
|||||||
/**
|
/**
|
||||||
* 获取用户的对话统计信息
|
* 获取用户的对话统计信息
|
||||||
* @param userId 用户ID(可选,默认当前用户)
|
* @param userId 用户ID(可选,默认当前用户)
|
||||||
* @returns Promise<ResultDomain<UserChatStatistics>>
|
* @returns Promise<ResultDomain<any>>
|
||||||
*/
|
*/
|
||||||
async getUserChatStatistics(userId?: string): Promise<ResultDomain<UserChatStatistics>> {
|
async getUserChatStatistics(userId?: string): Promise<ResultDomain<any>> {
|
||||||
const response = await api.get<UserChatStatistics>('/ai/history/statistics/user', {
|
const response = await api.get<any>('/ai/chat/history/statistics', {
|
||||||
params: { userId }
|
params: { userId }
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
@@ -104,10 +94,10 @@ export const chatHistoryApi = {
|
|||||||
/**
|
/**
|
||||||
* 获取会话的详细统计
|
* 获取会话的详细统计
|
||||||
* @param conversationId 会话ID
|
* @param conversationId 会话ID
|
||||||
* @returns Promise<ResultDomain<ConversationStatistics>>
|
* @returns Promise<ResultDomain<any>>
|
||||||
*/
|
*/
|
||||||
async getConversationStatistics(conversationId: string): Promise<ResultDomain<ConversationStatistics>> {
|
async getConversationStatistics(conversationId: string): Promise<ResultDomain<any>> {
|
||||||
const response = await api.get<ConversationStatistics>(`/ai/history/statistics/conversation/${conversationId}`);
|
const response = await api.get<any>(`/ai/chat/history/conversation/${conversationId}/statistics`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -117,7 +107,7 @@ export const chatHistoryApi = {
|
|||||||
* @returns Promise<ResultDomain<string>>
|
* @returns Promise<ResultDomain<string>>
|
||||||
*/
|
*/
|
||||||
async exportConversationAsMarkdown(conversationId: string): Promise<ResultDomain<string>> {
|
async exportConversationAsMarkdown(conversationId: string): Promise<ResultDomain<string>> {
|
||||||
const response = await api.get<string>(`/ai/history/export/markdown/${conversationId}`);
|
const response = await api.get<string>(`/ai/chat/history/export/markdown/${conversationId}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -127,47 +117,7 @@ export const chatHistoryApi = {
|
|||||||
* @returns Promise<ResultDomain<string>>
|
* @returns Promise<ResultDomain<string>>
|
||||||
*/
|
*/
|
||||||
async exportConversationAsJson(conversationId: string): Promise<ResultDomain<string>> {
|
async exportConversationAsJson(conversationId: string): Promise<ResultDomain<string>> {
|
||||||
const response = await api.get<string>(`/ai/history/export/json/${conversationId}`);
|
const response = await api.get<string>(`/ai/chat/history/export/json/${conversationId}`);
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量导出会话
|
|
||||||
* @param params 导出参数
|
|
||||||
* @returns Promise<ResultDomain<string>>
|
|
||||||
*/
|
|
||||||
async batchExportConversations(params: BatchExportParams): Promise<ResultDomain<string>> {
|
|
||||||
const response = await api.post<string>('/ai/history/export/batch', params);
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 下载导出文件
|
|
||||||
* @param conversationId 会话ID
|
|
||||||
* @param format 格式(markdown/json)
|
|
||||||
*/
|
|
||||||
downloadExport(conversationId: string, format: 'markdown' | 'json'): void {
|
|
||||||
const url = `${api.defaults.baseURL}/ai/history/export/download/${conversationId}?format=${format}`;
|
|
||||||
window.open(url, '_blank');
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量下载导出文件
|
|
||||||
* @param conversationIds 会话ID列表
|
|
||||||
* @param format 格式(markdown/json)
|
|
||||||
*/
|
|
||||||
batchDownloadExport(conversationIds: string[], format: 'markdown' | 'json'): void {
|
|
||||||
const url = `${api.defaults.baseURL}/ai/history/export/batch-download?format=${format}&ids=${conversationIds.join(',')}`;
|
|
||||||
window.open(url, '_blank');
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清理过期会话(软删除超过N天的会话)
|
|
||||||
* @param days 天数
|
|
||||||
* @returns Promise<ResultDomain<number>>
|
|
||||||
*/
|
|
||||||
async cleanExpiredConversations(days: number): Promise<ResultDomain<number>> {
|
|
||||||
const response = await api.post<number>('/ai/history/clean', { days });
|
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -176,20 +126,8 @@ export const chatHistoryApi = {
|
|||||||
* @param limit 数量限制(默认10)
|
* @param limit 数量限制(默认10)
|
||||||
* @returns Promise<ResultDomain<AiConversation>>
|
* @returns Promise<ResultDomain<AiConversation>>
|
||||||
*/
|
*/
|
||||||
async getRecentConversations(limit?: number): Promise<ResultDomain<AiConversation>> {
|
async getRecentConversations(limit = 10): Promise<ResultDomain<AiConversation>> {
|
||||||
const response = await api.get<AiConversation>('/ai/history/recent', {
|
const response = await api.get<AiConversation>('/ai/chat/history/recent', {
|
||||||
params: { limit }
|
|
||||||
});
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取热门会话(基于消息数或Token数)
|
|
||||||
* @param limit 数量限制(默认10)
|
|
||||||
* @returns Promise<ResultDomain<AiConversation>>
|
|
||||||
*/
|
|
||||||
async getPopularConversations(limit?: number): Promise<ResultDomain<AiConversation>> {
|
|
||||||
const response = await api.get<AiConversation>('/ai/history/popular', {
|
|
||||||
params: { limit }
|
params: { limit }
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import type {
|
|||||||
AiConversation,
|
AiConversation,
|
||||||
AiMessage,
|
AiMessage,
|
||||||
ChatRequest,
|
ChatRequest,
|
||||||
ChatResponse,
|
|
||||||
ResultDomain,
|
ResultDomain,
|
||||||
StreamCallback
|
StreamCallback
|
||||||
} from '@/types';
|
} from '@/types';
|
||||||
@@ -19,43 +18,102 @@ import type {
|
|||||||
*/
|
*/
|
||||||
export const chatApi = {
|
export const chatApi = {
|
||||||
/**
|
/**
|
||||||
* 流式对话(SSE)
|
* 流式对话(SSE)- 使用fetch支持Authorization
|
||||||
* @param request 对话请求
|
* @param request 对话请求
|
||||||
* @param callback 流式回调
|
* @param callback 流式回调
|
||||||
* @returns Promise<ResultDomain<AiMessage>>
|
* @returns Promise<ResultDomain<AiMessage>>
|
||||||
*/
|
*/
|
||||||
async streamChat(request: ChatRequest, callback?: StreamCallback): Promise<ResultDomain<AiMessage>> {
|
async streamChat(request: ChatRequest, callback?: StreamCallback): Promise<ResultDomain<AiMessage>> {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
const tokenData = token ? JSON.parse(token) : null;
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
// 使用相对路径走Vite代理,避免跨域
|
||||||
const eventSource = new EventSource(
|
const eventSource = new EventSource(
|
||||||
`${api.defaults.baseURL}/ai/chat/stream?` +
|
`/api/ai/chat/stream?` +
|
||||||
new URLSearchParams({
|
new URLSearchParams({
|
||||||
agentId: request.agentId,
|
agentId: request.agentId,
|
||||||
conversationId: request.conversationId || '',
|
conversationId: request.conversationId || '',
|
||||||
query: request.query,
|
query: request.query,
|
||||||
knowledgeIds: request.knowledgeIds?.join(',') || ''
|
knowledgeIds: request.knowledgeIds?.join(',') || '',
|
||||||
|
token: tokenData?.value || ''
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
let fullMessage = '';
|
// 通知外部EventSource已创建
|
||||||
|
callback?.onStart?.(eventSource);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
let fullMessage = ''; // 累积完整消息内容
|
||||||
|
|
||||||
|
// 监听初始化事件(包含messageId和conversationId)
|
||||||
|
eventSource.addEventListener('init', (event) => {
|
||||||
|
try {
|
||||||
|
const initData = JSON.parse(event.data);
|
||||||
|
console.log('[初始化数据]', initData);
|
||||||
|
// 通知外部保存messageId(用于停止生成)
|
||||||
|
if (callback?.onInit) {
|
||||||
|
callback.onInit(initData);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('解析init事件失败:', event.data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听标准消息事件
|
||||||
eventSource.addEventListener('message', (event) => {
|
eventSource.addEventListener('message', (event) => {
|
||||||
const data = event.data;
|
const data = event.data;
|
||||||
fullMessage += data;
|
fullMessage += data;
|
||||||
callback?.onMessage?.(data);
|
callback?.onMessage?.(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 监听结束事件
|
||||||
eventSource.addEventListener('end', (event) => {
|
eventSource.addEventListener('end', (event) => {
|
||||||
const metadata = JSON.parse(event.data);
|
const metadata = JSON.parse(event.data);
|
||||||
callback?.onMessageEnd?.(metadata);
|
callback?.onMessageEnd?.(metadata);
|
||||||
eventSource.close();
|
eventSource.close();
|
||||||
|
|
||||||
resolve({
|
resolve({
|
||||||
|
code: 200,
|
||||||
success: true,
|
success: true,
|
||||||
|
login: true,
|
||||||
|
auth: true,
|
||||||
data: metadata as AiMessage,
|
data: metadata as AiMessage,
|
||||||
message: '对话成功'
|
message: '对话成功'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 监听所有Dify原始事件(workflow_started, node_started等)
|
||||||
|
const difyEventTypes = [
|
||||||
|
'dify_workflow_started',
|
||||||
|
'dify_node_started',
|
||||||
|
'dify_node_finished',
|
||||||
|
'dify_workflow_finished',
|
||||||
|
'dify_message',
|
||||||
|
'dify_agent_message',
|
||||||
|
'dify_message_end',
|
||||||
|
'dify_message_file',
|
||||||
|
'dify_agent_thought',
|
||||||
|
'dify_ping'
|
||||||
|
];
|
||||||
|
|
||||||
|
difyEventTypes.forEach(eventType => {
|
||||||
|
eventSource.addEventListener(eventType, (event: any) => {
|
||||||
|
try {
|
||||||
|
const eventData = JSON.parse(event.data);
|
||||||
|
console.log(`[Dify事件] ${eventType}:`, eventData);
|
||||||
|
|
||||||
|
// 调用自定义的Dify事件回调
|
||||||
|
if (callback?.onDifyEvent) {
|
||||||
|
const cleanEventType = eventType.replace('dify_', '');
|
||||||
|
callback.onDifyEvent(cleanEventType, eventData);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`解析Dify事件失败 ${eventType}:`, event.data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听错误事件
|
||||||
eventSource.addEventListener('error', (event: any) => {
|
eventSource.addEventListener('error', (event: any) => {
|
||||||
const error = new Error(event.data || '对话失败');
|
const error = new Error(event.data || '对话失败');
|
||||||
callback?.onError?.(error);
|
callback?.onError?.(error);
|
||||||
@@ -64,7 +122,7 @@ export const chatApi = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
eventSource.onerror = (error) => {
|
eventSource.onerror = (error) => {
|
||||||
callback?.onError?.(error as Error);
|
callback?.onError?.(error as unknown as Error);
|
||||||
eventSource.close();
|
eventSource.close();
|
||||||
reject(error);
|
reject(error);
|
||||||
};
|
};
|
||||||
@@ -82,7 +140,7 @@ export const chatApi = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 停止对话生成
|
* 停止对话生成(通过消息ID)
|
||||||
* @param messageId 消息ID
|
* @param messageId 消息ID
|
||||||
* @returns Promise<ResultDomain<boolean>>
|
* @returns Promise<ResultDomain<boolean>>
|
||||||
*/
|
*/
|
||||||
@@ -91,6 +149,20 @@ export const chatApi = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止对话生成(通过Dify TaskID)
|
||||||
|
* @param taskId Dify任务ID
|
||||||
|
* @param agentId 智能体ID
|
||||||
|
* @returns Promise<ResultDomain<boolean>>
|
||||||
|
*/
|
||||||
|
async stopChatByTaskId(taskId: string, agentId: string): Promise<ResultDomain<boolean>> {
|
||||||
|
const response = await api.post<boolean>('/ai/chat/stop-by-taskid', {
|
||||||
|
taskId,
|
||||||
|
agentId
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建新会话
|
* 创建新会话
|
||||||
* @param agentId 智能体ID
|
* @param agentId 智能体ID
|
||||||
@@ -152,8 +224,8 @@ export const chatApi = {
|
|||||||
* @param conversationId 会话ID
|
* @param conversationId 会话ID
|
||||||
* @returns Promise<ResultDomain<AiMessage[]>>
|
* @returns Promise<ResultDomain<AiMessage[]>>
|
||||||
*/
|
*/
|
||||||
async listMessages(conversationId: string): Promise<ResultDomain<AiMessage[]>> {
|
async listMessages(conversationId: string): Promise<ResultDomain<AiMessage>> {
|
||||||
const response = await api.get<AiMessage[]>(`/ai/chat/conversation/${conversationId}/messages`);
|
const response = await api.get<AiMessage>(`/ai/chat/conversation/${conversationId}/messages`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -168,46 +240,112 @@ export const chatApi = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 重新生成回答
|
* 重新生成回答(SSE流式)
|
||||||
* @param messageId 原消息ID
|
* @param messageId 原消息ID
|
||||||
* @param callback 流式回调(可选)
|
* @param callback 流式回调
|
||||||
* @returns Promise<ResultDomain<AiMessage>>
|
* @returns Promise<ResultDomain<AiMessage>>
|
||||||
*/
|
*/
|
||||||
async regenerateAnswer(messageId: string, callback?: StreamCallback): Promise<ResultDomain<AiMessage>> {
|
async regenerateAnswer(messageId: string, callback?: StreamCallback): Promise<ResultDomain<AiMessage>> {
|
||||||
if (callback) {
|
const token = localStorage.getItem('token');
|
||||||
// 使用流式方式重新生成
|
const tokenData = token ? JSON.parse(token) : null;
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const eventSource = new EventSource(
|
|
||||||
`${api.defaults.baseURL}/ai/chat/regenerate/${messageId}?stream=true`
|
|
||||||
);
|
|
||||||
|
|
||||||
eventSource.addEventListener('message', (event) => {
|
return new Promise((resolve, reject) => {
|
||||||
callback.onMessage?.(event.data);
|
// 使用相对路径走Vite代理,SSE流式推送
|
||||||
});
|
const eventSource = new EventSource(
|
||||||
|
`/api/ai/chat/regenerate/${messageId}?` +
|
||||||
|
new URLSearchParams({
|
||||||
|
token: tokenData?.value || ''
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
eventSource.addEventListener('end', (event) => {
|
// 通知外部EventSource已创建
|
||||||
const metadata = JSON.parse(event.data);
|
callback?.onStart?.(eventSource);
|
||||||
callback.onMessageEnd?.(metadata);
|
|
||||||
eventSource.close();
|
|
||||||
resolve({
|
|
||||||
success: true,
|
|
||||||
data: metadata as AiMessage,
|
|
||||||
message: '重新生成成功'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
eventSource.addEventListener('error', (event: any) => {
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const error = new Error(event.data || '重新生成失败');
|
let fullMessage = ''; // 累积完整消息内容
|
||||||
callback.onError?.(error);
|
|
||||||
eventSource.close();
|
// 监听初始化事件(包含messageId和conversationId)
|
||||||
reject(error);
|
eventSource.addEventListener('init', (event) => {
|
||||||
|
try {
|
||||||
|
const initData = JSON.parse(event.data);
|
||||||
|
console.log('[初始化数据-重新生成]', initData);
|
||||||
|
// 通知外部保存messageId(用于停止生成)
|
||||||
|
if (callback?.onInit) {
|
||||||
|
callback.onInit(initData);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('解析init事件失败:', event.data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听标准消息事件
|
||||||
|
eventSource.addEventListener('message', (event) => {
|
||||||
|
const data = event.data;
|
||||||
|
fullMessage += data;
|
||||||
|
callback?.onMessage?.(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听结束事件
|
||||||
|
eventSource.addEventListener('end', (event) => {
|
||||||
|
const metadata = JSON.parse(event.data);
|
||||||
|
callback?.onMessageEnd?.(metadata);
|
||||||
|
eventSource.close();
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
code: 200,
|
||||||
|
success: true,
|
||||||
|
login: true,
|
||||||
|
auth: true,
|
||||||
|
data: metadata as AiMessage,
|
||||||
|
message: '重新生成成功'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
// 使用阻塞方式重新生成
|
// 监听所有Dify原始事件(workflow_started, node_started等)
|
||||||
const response = await api.post<AiMessage>(`/ai/chat/regenerate/${messageId}`);
|
const difyEventTypes = [
|
||||||
return response.data;
|
'dify_workflow_started',
|
||||||
}
|
'dify_node_started',
|
||||||
|
'dify_node_finished',
|
||||||
|
'dify_workflow_finished',
|
||||||
|
'dify_message',
|
||||||
|
'dify_agent_message',
|
||||||
|
'dify_message_end',
|
||||||
|
'dify_message_file',
|
||||||
|
'dify_agent_thought',
|
||||||
|
'dify_ping'
|
||||||
|
];
|
||||||
|
|
||||||
|
difyEventTypes.forEach(eventType => {
|
||||||
|
eventSource.addEventListener(eventType, (event: any) => {
|
||||||
|
try {
|
||||||
|
const eventData = JSON.parse(event.data);
|
||||||
|
console.log(`[Dify事件] ${eventType}:`, eventData);
|
||||||
|
|
||||||
|
// 调用自定义的Dify事件回调
|
||||||
|
if (callback?.onDifyEvent) {
|
||||||
|
const cleanEventType = eventType.replace('dify_', '');
|
||||||
|
callback.onDifyEvent(cleanEventType, eventData);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`解析Dify事件失败 ${eventType}:`, event.data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听错误事件
|
||||||
|
eventSource.addEventListener('error', (event: any) => {
|
||||||
|
const error = new Error(event.data || '重新生成失败');
|
||||||
|
callback?.onError?.(error);
|
||||||
|
eventSource.close();
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
eventSource.onerror = (error) => {
|
||||||
|
callback?.onError?.(error as unknown as Error);
|
||||||
|
eventSource.close();
|
||||||
|
reject(error);
|
||||||
|
};
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -5,8 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { api } from '@/apis/index';
|
import { api } from '@/apis/index';
|
||||||
import type { AiUploadFile, ResultDomain, FileUploadResponse, PageDomain, PageParam } from '@/types';
|
import type { AiUploadFile, ResultDomain, FileUploadResponse, PageParam } from '@/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文件上传API服务
|
* 文件上传API服务
|
||||||
*/
|
*/
|
||||||
@@ -22,11 +21,7 @@ export const fileUploadApi = {
|
|||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
formData.append('knowledgeId', knowledgeId);
|
formData.append('knowledgeId', knowledgeId);
|
||||||
|
|
||||||
const response = await api.post<FileUploadResponse>('/ai/file/upload', formData, {
|
const response = await api.post<FileUploadResponse>('/ai/file/upload', formData);
|
||||||
headers: {
|
|
||||||
'Content-Type': 'multipart/form-data'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -43,11 +38,7 @@ export const fileUploadApi = {
|
|||||||
});
|
});
|
||||||
formData.append('knowledgeId', knowledgeId);
|
formData.append('knowledgeId', knowledgeId);
|
||||||
|
|
||||||
const response = await api.post<FileUploadResponse[]>('/ai/file/batch-upload', formData, {
|
const response = await api.post<FileUploadResponse[]>('/ai/file/batch-upload', formData);
|
||||||
headers: {
|
|
||||||
'Content-Type': 'multipart/form-data'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -85,10 +76,10 @@ export const fileUploadApi = {
|
|||||||
* 分页查询文件
|
* 分页查询文件
|
||||||
* @param filter 过滤条件
|
* @param filter 过滤条件
|
||||||
* @param pageParam 分页参数
|
* @param pageParam 分页参数
|
||||||
* @returns Promise<PageDomain<AiUploadFile>>
|
* @returns Promise<ResultDomain<AiUploadFile>>
|
||||||
*/
|
*/
|
||||||
async pageFiles(filter: Partial<AiUploadFile>, pageParam: PageParam): Promise<PageDomain<AiUploadFile>> {
|
async pageFiles(filter: Partial<AiUploadFile>, pageParam: PageParam): Promise<ResultDomain<AiUploadFile>> {
|
||||||
const response = await api.post<PageDomain<AiUploadFile>>('/ai/file/page', {
|
const response = await api.post<AiUploadFile>('/ai/file/page', {
|
||||||
filter,
|
filter,
|
||||||
pageParam
|
pageParam
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -76,8 +76,8 @@ export const knowledgeApi = {
|
|||||||
* @param pageParam 分页参数
|
* @param pageParam 分页参数
|
||||||
* @returns Promise<PageDomain<AiKnowledge>>
|
* @returns Promise<PageDomain<AiKnowledge>>
|
||||||
*/
|
*/
|
||||||
async pageKnowledges(filter: Partial<AiKnowledge>, pageParam: PageParam): Promise<PageDomain<AiKnowledge>> {
|
async pageKnowledges(filter: Partial<AiKnowledge>, pageParam: PageParam): Promise<ResultDomain<AiKnowledge>> {
|
||||||
const response = await api.post<PageDomain<AiKnowledge>>('/ai/knowledge/page', {
|
const response = await api.post<AiKnowledge>('/ai/knowledge/page', {
|
||||||
filter,
|
filter,
|
||||||
pageParam
|
pageParam
|
||||||
});
|
});
|
||||||
@@ -108,6 +108,7 @@ export const knowledgeApi = {
|
|||||||
* 设置知识库权限
|
* 设置知识库权限
|
||||||
* @param params 权限参数
|
* @param params 权限参数
|
||||||
* @returns Promise<ResultDomain<boolean>>
|
* @returns Promise<ResultDomain<boolean>>
|
||||||
|
|
||||||
*/
|
*/
|
||||||
async setPermissions(params: KnowledgePermissionParams): Promise<ResultDomain<boolean>> {
|
async setPermissions(params: KnowledgePermissionParams): Promise<ResultDomain<boolean>> {
|
||||||
const response = await api.post<boolean>('/ai/knowledge/permissions', params);
|
const response = await api.post<boolean>('/ai/knowledge/permissions', params);
|
||||||
@@ -144,3 +145,4 @@ export const knowledgeApi = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -18,26 +18,45 @@ interface CustomAxiosRequestConfig extends Partial<InternalAxiosRequestConfig> {
|
|||||||
* Token管理
|
* Token管理
|
||||||
*/
|
*/
|
||||||
export const TokenManager = {
|
export const TokenManager = {
|
||||||
/** 获取token(优先从localStorage,其次sessionStorage) */
|
/** 获取token(从localStorage获取并检查过期) */
|
||||||
getToken(): string | null {
|
getToken(): string | null {
|
||||||
return localStorage.getItem('token') || sessionStorage.getItem('token');
|
const itemStr = localStorage.getItem('token');
|
||||||
},
|
if (!itemStr) return null;
|
||||||
|
|
||||||
/** 设置token(根据rememberMe决定存储位置) */
|
try {
|
||||||
setToken(token: string, rememberMe = false): void {
|
const item = JSON.parse(itemStr);
|
||||||
if (rememberMe) {
|
const now = Date.now();
|
||||||
localStorage.setItem('token', token);
|
|
||||||
sessionStorage.removeItem('token'); // 清除sessionStorage中的旧token
|
// 检查是否过期
|
||||||
} else {
|
if (item.timestamp && item.expiresIn) {
|
||||||
sessionStorage.setItem('token', token);
|
if (now - item.timestamp > item.expiresIn) {
|
||||||
localStorage.removeItem('token'); // 清除localStorage中的旧token
|
// 已过期,删除
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return item.value || itemStr; // 兼容旧数据
|
||||||
|
} catch {
|
||||||
|
// 如果不是JSON格式,直接返回(兼容旧数据)
|
||||||
|
return itemStr;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 移除token(两个存储都清除) */
|
/** 设置token(始终使用localStorage,根据rememberMe设置过期时间) */
|
||||||
|
setToken(token: string, rememberMe = false): void {
|
||||||
|
const data = {
|
||||||
|
value: token,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
// 如果不勾选"记住我",设置1天过期时间;勾选则7天
|
||||||
|
expiresIn: rememberMe ? 7 * 24 * 60 * 60 * 1000 : 1 * 24 * 60 * 60 * 1000
|
||||||
|
};
|
||||||
|
localStorage.setItem('token', JSON.stringify(data));
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 移除token */
|
||||||
removeToken(): void {
|
removeToken(): void {
|
||||||
localStorage.removeItem('token');
|
localStorage.removeItem('token');
|
||||||
sessionStorage.removeItem('token');
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/** 检查是否有token */
|
/** 检查是否有token */
|
||||||
@@ -81,6 +100,11 @@ request.interceptors.request.use(
|
|||||||
config.headers.Authorization = `Bearer ${token}`;
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 自动处理 FormData:删除 Content-Type,让浏览器自动设置(包含 boundary)
|
||||||
|
if (config.data instanceof FormData && config.headers) {
|
||||||
|
delete config.headers['Content-Type'];
|
||||||
|
}
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
(error: AxiosError) => {
|
(error: AxiosError) => {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1004 B After Width: | Height: | Size: 1004 B |
@@ -28,6 +28,7 @@
|
|||||||
|
|
||||||
<!-- 页面内容 -->
|
<!-- 页面内容 -->
|
||||||
<main class="main-content">
|
<main class="main-content">
|
||||||
|
<AIAgent/>
|
||||||
<router-view />
|
<router-view />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
@@ -35,6 +36,7 @@
|
|||||||
<!-- 没有侧边栏时直接显示内容 -->
|
<!-- 没有侧边栏时直接显示内容 -->
|
||||||
<div class="content-wrapper-full" v-else>
|
<div class="content-wrapper-full" v-else>
|
||||||
<main class="main-content-full">
|
<main class="main-content-full">
|
||||||
|
<AIAgent/>
|
||||||
<router-view />
|
<router-view />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
@@ -48,8 +50,9 @@ import { useRoute, useRouter } from 'vue-router';
|
|||||||
import { useStore } from 'vuex';
|
import { useStore } from 'vuex';
|
||||||
import type { SysMenu } from '@/types';
|
import type { SysMenu } from '@/types';
|
||||||
import { MenuType } from '@/types/enums';
|
import { MenuType } from '@/types/enums';
|
||||||
import { getMenuPath } from '@/utils/route-generator';
|
// import { getMenuPath } from '@/utils/route-generator';
|
||||||
import { TopNavigation, MenuSidebar, Breadcrumb } from '@/components';
|
import { TopNavigation, MenuSidebar } from '@/components';
|
||||||
|
import { AIAgent } from '@/views/public/ai';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -89,16 +92,16 @@ const sidebarMenus = computed(() => {
|
|||||||
// 是否有侧边栏菜单
|
// 是否有侧边栏菜单
|
||||||
const hasSidebarMenus = computed(() => sidebarMenus.value.length > 0);
|
const hasSidebarMenus = computed(() => sidebarMenus.value.length > 0);
|
||||||
|
|
||||||
// 面包屑数据
|
// 面包屑数据(暂时未使用)
|
||||||
const breadcrumbItems = computed(() => {
|
// const breadcrumbItems = computed(() => {
|
||||||
if (!route.meta?.menuId) return [];
|
// if (!route.meta?.menuId) return [];
|
||||||
|
//
|
||||||
const menuPath = getMenuPath(allMenus.value, route.meta.menuId as string);
|
// const menuPath = getMenuPath(allMenus.value, route.meta.menuId as string);
|
||||||
return menuPath.map((menu) => ({
|
// return menuPath.map((menu) => ({
|
||||||
title: menu.name || '',
|
// title: menu.name || '',
|
||||||
path: menu.url || '',
|
// path: menu.url || '',
|
||||||
}));
|
// }));
|
||||||
});
|
// });
|
||||||
|
|
||||||
// 判断路径是否在菜单下
|
// 判断路径是否在菜单下
|
||||||
function isPathUnderMenu(path: string, menu: SysMenu): boolean {
|
function isPathUnderMenu(path: string, menu: SysMenu): boolean {
|
||||||
|
|||||||
@@ -26,24 +26,45 @@ export interface AuthState {
|
|||||||
|
|
||||||
// 存储工具函数
|
// 存储工具函数
|
||||||
const StorageUtil = {
|
const StorageUtil = {
|
||||||
// 保存数据(根据rememberMe选择存储方式)
|
// 保存数据(始终使用localStorage,但根据rememberMe设置过期时间)
|
||||||
setItem(key: string, value: string, rememberMe = false) {
|
setItem(key: string, value: string, rememberMe = false) {
|
||||||
if (rememberMe) {
|
const data = {
|
||||||
localStorage.setItem(key, value);
|
value,
|
||||||
} else {
|
timestamp: Date.now(),
|
||||||
sessionStorage.setItem(key, value);
|
// 如果不勾选"记住我",设置1天过期时间;勾选则7天
|
||||||
|
expiresIn: rememberMe ? 7 * 24 * 60 * 60 * 1000 : 1 * 24 * 60 * 60 * 1000
|
||||||
|
};
|
||||||
|
localStorage.setItem(key, JSON.stringify(data));
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取数据(检查是否过期)
|
||||||
|
getItem(key: string): string | null {
|
||||||
|
const itemStr = localStorage.getItem(key);
|
||||||
|
if (!itemStr) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const item = JSON.parse(itemStr);
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// 检查是否过期
|
||||||
|
if (item.timestamp && item.expiresIn) {
|
||||||
|
if (now - item.timestamp > item.expiresIn) {
|
||||||
|
// 已过期,删除
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return item.value || itemStr; // 兼容旧数据
|
||||||
|
} catch {
|
||||||
|
// 如果不是JSON格式,直接返回(兼容旧数据)
|
||||||
|
return itemStr;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取数据(优先从localStorage,其次sessionStorage)
|
// 删除数据
|
||||||
getItem(key: string): string | null {
|
|
||||||
return localStorage.getItem(key) || sessionStorage.getItem(key);
|
|
||||||
},
|
|
||||||
|
|
||||||
// 删除数据(从两个存储中都删除)
|
|
||||||
removeItem(key: string) {
|
removeItem(key: string) {
|
||||||
localStorage.removeItem(key);
|
localStorage.removeItem(key);
|
||||||
sessionStorage.removeItem(key);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// 清除所有认证相关数据
|
// 清除所有认证相关数据
|
||||||
@@ -51,7 +72,6 @@ const StorageUtil = {
|
|||||||
const keys = ['token', 'loginDomain', 'menus', 'permissions', 'rememberMe'];
|
const keys = ['token', 'loginDomain', 'menus', 'permissions', 'rememberMe'];
|
||||||
keys.forEach(key => {
|
keys.forEach(key => {
|
||||||
localStorage.removeItem(key);
|
localStorage.removeItem(key);
|
||||||
sessionStorage.removeItem(key);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -321,6 +321,10 @@ export interface KnowledgePermissionParams {
|
|||||||
* Streaming 回调接口
|
* Streaming 回调接口
|
||||||
*/
|
*/
|
||||||
export interface StreamCallback {
|
export interface StreamCallback {
|
||||||
|
/** 开始连接 */
|
||||||
|
onStart?: (eventSource: EventSource) => void;
|
||||||
|
/** 初始化数据(包含messageId用于停止生成) */
|
||||||
|
onInit?: (initData: { messageId: string; conversationId: string }) => void;
|
||||||
/** 接收到消息片段 */
|
/** 接收到消息片段 */
|
||||||
onMessage?: (message: string) => void;
|
onMessage?: (message: string) => void;
|
||||||
/** 消息结束 */
|
/** 消息结束 */
|
||||||
@@ -329,6 +333,28 @@ export interface StreamCallback {
|
|||||||
onComplete?: () => void;
|
onComplete?: () => void;
|
||||||
/** 错误 */
|
/** 错误 */
|
||||||
onError?: (error: Error) => void;
|
onError?: (error: Error) => void;
|
||||||
|
/** Dify原始事件回调(包含task_id等完整信息) */
|
||||||
|
onDifyEvent?: (eventType: string, eventData: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dify事件数据接口
|
||||||
|
*/
|
||||||
|
export interface DifyEvent {
|
||||||
|
/** 事件类型 */
|
||||||
|
event: string;
|
||||||
|
/** 任务ID */
|
||||||
|
task_id?: string;
|
||||||
|
/** 工作流运行ID */
|
||||||
|
workflow_run_id?: string;
|
||||||
|
/** 消息ID */
|
||||||
|
message_id?: string;
|
||||||
|
/** 会话ID */
|
||||||
|
conversation_id?: string;
|
||||||
|
/** 创建时间戳 */
|
||||||
|
created_at?: number;
|
||||||
|
/** 其他字段 */
|
||||||
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Dify 文档分段相关类型 ====================
|
// ==================== Dify 文档分段相关类型 ====================
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<div class="agent-icon">
|
<div class="agent-icon">
|
||||||
<img v-if="configForm.avatar" :src="FILE_DOWNLOAD_URL + configForm.avatar" alt="助手头像" />
|
<img v-if="configForm.avatar" :src="FILE_DOWNLOAD_URL + configForm.avatar" alt="助手头像" />
|
||||||
<div v-else class="default-icon">
|
<div v-else class="default-icon">
|
||||||
<img src="@/assets/imgs/assisstent.svg" alt="助手头像" />
|
<img src="@/assets/imgs/assistant.svg" alt="助手头像" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="agent-info">
|
<div class="agent-info">
|
||||||
@@ -45,34 +45,12 @@
|
|||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="模式">
|
<el-form-item label="助手描述">
|
||||||
<el-select
|
|
||||||
v-model="configForm.modelProvider"
|
|
||||||
placeholder="选择模式"
|
|
||||||
style="width: 100%"
|
|
||||||
>
|
|
||||||
<el-option label="OpenAI" value="openai" />
|
|
||||||
<el-option label="Anthropic" value="anthropic" />
|
|
||||||
<el-option label="Azure OpenAI" value="azure" />
|
|
||||||
<el-option label="通义千问" value="qwen" />
|
|
||||||
<el-option label="文心一言" value="wenxin" />
|
|
||||||
<el-option label="Dify" value="dify" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="模型">
|
|
||||||
<el-input
|
|
||||||
v-model="configForm.modelName"
|
|
||||||
placeholder="例如: gpt-4, claude-3-opus"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="系统提示词">
|
|
||||||
<el-input
|
<el-input
|
||||||
v-model="configForm.systemPrompt"
|
v-model="configForm.systemPrompt"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
:rows="8"
|
:rows="8"
|
||||||
placeholder="请输入系统提示词,定义AI助手的角色、行为和回答风格..."
|
placeholder="请输入助手描述,介绍AI助手的功能、特点和用途..."
|
||||||
maxlength="2000"
|
maxlength="2000"
|
||||||
show-word-limit
|
show-word-limit
|
||||||
/>
|
/>
|
||||||
@@ -160,10 +138,6 @@ async function handleSave() {
|
|||||||
ElMessage.warning('请输入助手名称');
|
ElMessage.warning('请输入助手名称');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!configForm.value.modelProvider) {
|
|
||||||
ElMessage.warning('请选择模式');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
saving.value = true;
|
saving.value = true;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user