对话、重新生成、评价完成

This commit is contained in:
2025-11-05 16:55:58 +08:00
parent 8850a06fea
commit d9d62e22de
34 changed files with 1658 additions and 965 deletions

View File

@@ -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 '关联文件IDJSON数组', `file_ids` VARCHAR(500) DEFAULT NULL COMMENT '关联文件IDJSON数组',
@@ -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使用统计表

View File

@@ -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;
}
}
} }

View File

@@ -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) {
// 默认实现:不处理
}
/** /**
* 流式响应完成 * 流式响应完成
*/ */

View File

@@ -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);
} }

View File

@@ -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);
} }

View File

@@ -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);
} }

View File

@@ -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);
} }

View File

@@ -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);

View File

@@ -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);

View File

@@ -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,101 +107,94 @@ 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); // 标记消息是否已创建
AtomicReference<Boolean> isStopped = new AtomicReference<>(false); // 标记是否已停止
difyApiClient.streamChat(chatRequest, agent.getDifyApiKey(), new StreamCallback() { difyApiClient.streamChat(chatRequest, agent.getDifyApiKey(), new StreamCallback() {
@Override @Override
public void onMessage(String message) { public void onMessage(String message) {
if (isStopped.get()) {
return; // 已停止,不再处理
}
try {
fullAnswer.append(message); fullAnswer.append(message);
// 发给前端回调 // 发送消息片段给前端
if (callback != null) { emitter.send(SseEmitter.event().name("message").data(message));
callback.onMessage(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) {
if (isStopped.get()) {
return; // 已停止,不再处理
}
try { try {
// 解析metadata获取会话ID和消息ID // 解析metadata
JsonNode json = objectMapper.readTree(metadata); JsonNode json = objectMapper.readTree(metadata);
if (json.has("conversation_id")) { if (json.has("conversation_id")) {
difyConversationId.set(json.get("conversation_id").asText()); difyConversationId.set(json.get("conversation_id").asText());
@@ -210,67 +203,161 @@ public class AiChatServiceImpl implements AiChatService {
difyMessageId.set(json.get("id").asText()); 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 // 更新会话的Dify会话ID
if (StringUtils.hasText(difyConversationId.get())) { if (StringUtils.hasText(difyConversationId.get())) {
TbAiConversation updateConv = new TbAiConversation(); TbAiConversation updateConv = new TbAiConversation();
updateConv.setID(finalConversationId); updateConv.setID(finalConversationId);
updateConv.setDifyConversationId(difyConversationId.get()); updateConv.setDifyConversationId(difyConversationId.get());
updateConv.setMessageCount((conversation.getMessageCount() != null ? updateConv.setMessageCount((finalConversation.getMessageCount() != null ?
conversation.getMessageCount() : 0) + 2); // 用户问题+AI回答 finalConversation.getMessageCount() : 0) + 2);
updateConv.setUpdateTime(new Date()); updateConv.setUpdateTime(new Date());
conversationMapper.updateConversation(updateConv); conversationMapper.updateConversation(updateConv);
} }
if (callback != null) { // 发送结束事件(保持兼容)
callback.onMessageEnd(metadata); emitter.send(SseEmitter.event().name("end").data(metadata));
} } catch (IllegalStateException e) {
// SseEmitter已关闭标记为已停止
isStopped.set(true);
log.debug("SSE连接已关闭停止处理");
} catch (Exception e) { } catch (Exception e) {
log.error("处理流式响应metadata失败", 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 @Override
public void onComplete() { public void onComplete() {
log.info("流式对话完成: {} - {}", finalConversationId, finalAiMessageId); if (isStopped.get()) {
if (callback != null) { log.debug("SSE连接已关闭跳过完成处理");
callback.onComplete(); 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 @Override
public void onError(Throwable error) { 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
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public ResultDomain<TbAiMessage> blockingChat( public ResultDomain<TbAiMessage> blockingChat(
@@ -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.getConversationID(),
userQuestion.getContent(),
null,
callback
);
} else {
return blockingChat(
originalMessage.getAgentID(), originalMessage.getAgentID(),
originalMessage.getConversationID(), originalMessage.getConversationID(),
userQuestion.getContent(), userQuestion.getContent(),
null 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);

View File

@@ -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();

View File

@@ -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 {
// 查询知识库的所有文件 // 查询知识库的所有文件

View File

@@ -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查询会话 -->

View File

@@ -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);
/** /**
* 分页查询智能体列表 * 分页查询智能体列表

View File

@@ -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);
/** /**
* 异步生成会话摘要 * 异步生成会话摘要

View File

@@ -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);
} }

View File

@@ -57,7 +57,7 @@ public interface AiKnowledgeService {
* @param filter 过滤条件 * @param filter 过滤条件
* @return 知识库列表 * @return 知识库列表
*/ */
ResultDomain<List<TbAiKnowledge>> listKnowledges(TbAiKnowledge filter); ResultDomain<TbAiKnowledge> listKnowledges(TbAiKnowledge filter);
/** /**
* 分页查询知识库 * 分页查询知识库

View File

@@ -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;
} }

View File

@@ -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);

View File

@@ -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);
} }

View File

@@ -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()) {

View File

@@ -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请求上下文或类不存在忽略
} }

View File

@@ -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;
}, },

View File

@@ -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;

View File

@@ -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) => { return new Promise((resolve, reject) => {
// 使用相对路径走Vite代理SSE流式推送
const eventSource = new EventSource( const eventSource = new EventSource(
`${api.defaults.baseURL}/ai/chat/regenerate/${messageId}?stream=true` `/api/ai/chat/regenerate/${messageId}?` +
new URLSearchParams({
token: tokenData?.value || ''
})
); );
eventSource.addEventListener('message', (event) => { // 通知外部EventSource已创建
callback.onMessage?.(event.data); 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) => {
const data = event.data;
fullMessage += 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);
eventSource.close(); eventSource.close();
reject(error); reject(error);
}); });
eventSource.onerror = (error) => {
callback?.onError?.(error as unknown as Error);
eventSource.close();
reject(error);
};
}); });
} else {
// 使用阻塞方式重新生成
const response = await api.post<AiMessage>(`/ai/chat/regenerate/${messageId}`);
return response.data;
}
}, },
/** /**

View File

@@ -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
}); });

View File

@@ -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;
} }
}; };

View File

@@ -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) => {

View File

Before

Width:  |  Height:  |  Size: 1004 B

After

Width:  |  Height:  |  Size: 1004 B

View File

@@ -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 {

View File

@@ -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);
}); });
} }
}; };

View File

@@ -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 文档分段相关类型 ====================

View File

@@ -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;

View File

@@ -1,14 +1,8 @@
<template> <template>
<div class="ai-agent" :class="{ expanded: !isBall }"> <div v-if="hasAgent" class="ai-agent" :class="{ expanded: !isBall }">
<div v-if="isBall" class="ball-container" ref="ballRef"> <div v-if="isBall" class="ball-container" ref="ballRef">
<!-- 悬浮球 --> <!-- 悬浮球 -->
<div <div class="chat-ball" @mousedown="startDrag" @touchstart="startDrag" :style="ballStyle">
class="chat-ball"
@mousedown="startDrag"
@touchstart="startDrag"
@click="expandChat"
:style="ballStyle"
>
<img src="@/assets/imgs/chat-ball.svg" alt="AI助手" class="ball-icon" /> <img src="@/assets/imgs/chat-ball.svg" alt="AI助手" class="ball-icon" />
<!-- 未读消息提示 --> <!-- 未读消息提示 -->
<div v-if="unreadCount > 0" class="unread-badge">{{ unreadCount }}</div> <div v-if="unreadCount > 0" class="unread-badge">{{ unreadCount }}</div>
@@ -27,24 +21,16 @@
<div v-if="!historyCollapsed" class="history-list"> <div v-if="!historyCollapsed" class="history-list">
<!-- 新建对话按钮 --> <!-- 新建对话按钮 -->
<button class="new-chat-btn" @click="createNewConversation"> <button class="new-chat-btn" @click="() => createNewConversation()">
+ 新建对话 + 新建对话
</button> </button>
<!-- 历史对话列表 --> <!-- 历史对话列表 -->
<div <div v-for="conv in conversations" :key="conv.id" class="conversation-item"
v-for="conv in conversations" :class="{ active: currentConversation?.id === conv.id }" @click="selectConversation(conv)">
:key="conv.id"
class="conversation-item"
:class="{ active: currentConversation?.id === conv.id }"
@click="selectConversation(conv)"
>
<div class="conv-title">{{ conv.title || '新对话' }}</div> <div class="conv-title">{{ conv.title || '新对话' }}</div>
<div class="conv-time">{{ formatTime(conv.updateTime) }}</div> <div class="conv-time">{{ formatTime(conv.updateTime) }}</div>
<button <button class="delete-conv-btn" @click.stop="deleteConversationConfirm(conv.id || '')">
class="delete-conv-btn"
@click.stop="deleteConversationConfirm(conv.id || '')"
>
× ×
</button> </button>
</div> </div>
@@ -60,14 +46,8 @@
<div class="ai-agent-current-chat"> <div class="ai-agent-current-chat">
<div class="current-chat-header"> <div class="current-chat-header">
<div class="current-chat-title"> <div class="current-chat-title">
<input <input v-if="editingTitle" v-model="editTitleValue" @blur="saveTitle" @keyup.enter="saveTitle"
v-if="editingTitle" class="title-input" autofocus />
v-model="editTitleValue"
@blur="saveTitle"
@keyup.enter="saveTitle"
class="title-input"
autofocus
/>
<span v-else @dblclick="startEditTitle"> <span v-else @dblclick="startEditTitle">
{{ currentConversation?.title || '新对话' }} {{ currentConversation?.title || '新对话' }}
</span> </span>
@@ -86,9 +66,12 @@
<div class="current-chat-content" ref="chatContentRef"> <div class="current-chat-content" ref="chatContentRef">
<!-- 欢迎消息 --> <!-- 欢迎消息 -->
<div v-if="messages.length === 0" class="welcome-message"> <div v-if="messages.length === 0" class="welcome-message">
<div class="welcome-icon">🤖</div> <div class="welcome-icon">
<h2>你好我是AI助手</h2> <img v-if="agentAvatarUrl" :src="agentAvatarUrl" alt="AI助手" class="welcome-avatar" />
<p>有什么可以帮助你的吗</p> <img v-else src="@/assets/imgs/assistant.svg" alt="AI助手" class="welcome-avatar" />
</div>
<h2>你好我是{{ agentConfig?.name || 'AI助手' }}</h2>
<p>{{ agentConfig?.systemPrompt || '有什么可以帮助你的吗?' }}</p>
</div> </div>
<!-- 消息列表 --> <!-- 消息列表 -->
@@ -107,53 +90,53 @@
<!-- AI 消息 --> <!-- AI 消息 -->
<div v-else class="message ai-message"> <div v-else class="message ai-message">
<div class="message-avatar"> <div class="message-avatar">
<div class="avatar-circle ai-avatar">🤖</div> <div class="avatar-circle ai-avatar">
<img v-if="agentAvatarUrl" :src="agentAvatarUrl" alt="AI助手" class="ai-avatar-img" />
<span v-else>🤖</span>
</div>
</div> </div>
<div class="message-content"> <div class="message-content">
<!-- 如果内容为空且正在生成显示加载动画 -->
<div v-if="!message.content && isGenerating" class="typing-indicator">
<span></span>
<span></span>
<span></span>
</div>
<!-- 否则显示实际内容 -->
<template v-else>
<div class="message-text" v-html="formatMarkdown(message.content || '')"></div> <div class="message-text" v-html="formatMarkdown(message.content || '')"></div>
<div class="message-footer"> <div class="message-footer">
<div class="message-time">{{ formatMessageTime(message.createTime) }}</div> <div class="message-time">{{ formatMessageTime(message.createTime) }}</div>
<div class="message-actions"> <div class="message-actions">
<button <button @click="copyMessage(message.content || '')" class="msg-action-btn" title="复制">
@click="copyMessage(message.content || '')"
class="msg-action-btn"
title="复制"
>
📋 📋
</button> </button>
<button <button @click="regenerateMessage(message.id || '')" class="msg-action-btn" title="重新生成">
@click="regenerateMessage(message.id || '')"
class="msg-action-btn"
title="重新生成"
>
🔄 🔄
</button> </button>
<button <button @click="rateMessage(message.id || '', 1)" class="msg-action-btn"
@click="rateMessage(message.id || '', 1)" :class="{ active: message.rating === 1 }" title="好评">
class="msg-action-btn"
:class="{ active: message.rating === 1 }"
title="好评"
>
👍 👍
</button> </button>
<button <button @click="rateMessage(message.id || '', -1)" class="msg-action-btn"
@click="rateMessage(message.id || '', -1)" :class="{ active: message.rating === -1 }" title="差评">
class="msg-action-btn"
:class="{ active: message.rating === -1 }"
title="差评"
>
👎 👎
</button> </button>
</div> </div>
</div> </div>
</template>
</div> </div>
</div> </div>
</div> </div>
<!-- 加载中提示 --> <!-- 加载中提示只在还没有AI消息时显示 -->
<div v-if="isGenerating" class="message ai-message generating"> <div v-if="isGenerating && (!messages.length || messages[messages.length - 1]?.role !== 'assistant')"
class="message ai-message generating">
<div class="message-avatar"> <div class="message-avatar">
<div class="avatar-circle ai-avatar">🤖</div> <div class="avatar-circle ai-avatar">
<img v-if="agentAvatarUrl" :src="agentAvatarUrl" alt="AI助手" class="ai-avatar-img" />
<img v-else src="@/assets/imgs/assistant.svg" alt="AI助手" class="ai-avatar-img" />
</div>
</div> </div>
<div class="message-content"> <div class="message-content">
<div class="typing-indicator"> <div class="typing-indicator">
@@ -169,11 +152,7 @@
<div class="current-chat-input"> <div class="current-chat-input">
<!-- 已上传文件列表 --> <!-- 已上传文件列表 -->
<div v-if="uploadedFiles.length > 0" class="input-files"> <div v-if="uploadedFiles.length > 0" class="input-files">
<div <div v-for="(file, index) in uploadedFiles" :key="index" class="uploaded-file-item">
v-for="(file, index) in uploadedFiles"
:key="index"
class="uploaded-file-item"
>
<span class="file-name">{{ file.name }}</span> <span class="file-name">{{ file.name }}</span>
<button @click="removeUploadedFile(index)" class="remove-file-btn">×</button> <button @click="removeUploadedFile(index)" class="remove-file-btn">×</button>
</div> </div>
@@ -181,40 +160,28 @@
<!-- 输入框 --> <!-- 输入框 -->
<div class="input-area"> <div class="input-area">
<textarea <textarea v-model="inputMessage" class="input-text" placeholder="请输入问题..."
v-model="inputMessage" @keydown.enter.exact.prevent="sendMessage" @keydown.shift.enter="handleShiftEnter" ref="inputRef"
class="input-text" rows="1" />
placeholder="请输入问题..."
@keydown.enter.exact.prevent="sendMessage"
@keydown.shift.enter="handleShiftEnter"
ref="inputRef"
rows="1"
/>
<div class="input-action"> <div class="input-action">
<button @click="triggerFileUpload" class="action-icon-btn" title="上传文件"> <button @click="triggerFileUpload" class="action-icon-btn" title="上传文件">
<img src="@/assets/imgs/link.svg" alt="上传文件" class="link-icon" /> <img src="@/assets/imgs/link.svg" alt="上传文件" class="link-icon" />
</button> </button>
<button <!-- 停止生成按钮 -->
@click="sendMessage" <button v-if="isGenerating" @click="stopGenerating" class="action-icon-btn stop-btn" title="停止生成">
class="action-icon-btn send-btn" <span class="stop-icon"></span>
:disabled="!canSend" </button>
title="发送 (Enter)" <!-- 发送按钮 -->
> <button v-else @click="sendMessage" class="action-icon-btn send-btn" :disabled="!canSend" title="发送 (Enter)">
<img src="@/assets/imgs/send.svg" alt="发送" class="send-icon" /> <img src="@/assets/imgs/send.svg" alt="发送" class="send-icon" />
</button> </button>
</div> </div>
</div> </div>
<!-- 隐藏的文件上传input --> <!-- 隐藏的文件上传input -->
<input <input type="file" ref="fileInputRef" @change="handleFileUpload" style="display: none" multiple
type="file" accept=".txt,.pdf,.doc,.docx,.md" />
ref="fileInputRef"
@change="handleFileUpload"
style="display: none"
multiple
accept=".txt,.pdf,.doc,.docx,.md"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -224,23 +191,84 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'; import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
import { chatApi, chatHistoryApi } from '../../../apis/ai'; import { chatApi, chatHistoryApi, aiAgentConfigApi } from '@/apis/ai';
import type { AiConversation, AiMessage } from '../../../types/ai'; import type { AiConversation, AiMessage, AiAgentConfig } from '@/types/ai';
interface AIAgentProps { interface AIAgentProps {
isBall?: boolean;
expanded?: boolean;
agentId?: string; agentId?: string;
} }
const props = withDefaults(defineProps<AIAgentProps>(), { const props = withDefaults(defineProps<AIAgentProps>(), {
isBall: true, agentId: undefined
expanded: false,
agentId: 'default'
}); });
// ===== AI助手配置 =====
const hasAgent = ref(true);
const agentConfig = ref<AiAgentConfig | null>(null);
// 缓存头像URL为blob避免重复下载
const agentAvatarUrl = ref<string>('');
const cachedAvatarPath = ref<string>('');
// 加载并缓存头像
async function loadAndCacheAvatar(avatarPath: string) {
if (!avatarPath || cachedAvatarPath.value === avatarPath) {
return; // 已缓存,跳过
}
try {
const response = await fetch("/api/file/download/" + avatarPath);
if (response.ok) {
const blob = await response.blob();
// 释放旧的blob URL
if (agentAvatarUrl.value && agentAvatarUrl.value.startsWith('blob:')) {
URL.revokeObjectURL(agentAvatarUrl.value);
}
// 创建新的blob URL
agentAvatarUrl.value = URL.createObjectURL(blob);
cachedAvatarPath.value = avatarPath;
}
} catch (error) {
console.warn('加载头像失败:', error);
agentAvatarUrl.value = '';
}
}
// 加载AI助手配置
async function loadAgentConfig() {
try {
// 优先根据agentId获取如果没有则获取启用的助手列表
if (props.agentId) {
const result = await aiAgentConfigApi.getAgentById(props.agentId);
if (result.success && result.data) {
agentConfig.value = result.data;
// 加载并缓存头像
if (result.data.avatar) {
await loadAndCacheAvatar(result.data.avatar);
}
} else {
hasAgent.value = false;
}
} else {
// 获取启用的助手列表,使用第一个
const result = await aiAgentConfigApi.listEnabledAgents();
if (result.success && result.dataList && result.dataList.length > 0) {
agentConfig.value = result.dataList[0];
// 加载并缓存头像
if (result.dataList[0].avatar) {
await loadAndCacheAvatar(result.dataList[0].avatar);
}
} else {
hasAgent.value = false;
}
}
} catch (error) {
console.error('加载AI助手配置失败:', error);
}
}
// ===== 悬浮球相关 ===== // ===== 悬浮球相关 =====
const isBall = ref(props.isBall); const isBall = ref(true);
const ballRef = ref<HTMLElement | null>(null); const ballRef = ref<HTMLElement | null>(null);
const isDragging = ref(false); const isDragging = ref(false);
const dragStartX = ref(0); const dragStartX = ref(0);
@@ -249,21 +277,72 @@ const ballX = ref(0);
const ballY = ref(0); const ballY = ref(0);
const unreadCount = ref(0); const unreadCount = ref(0);
// 存储悬浮球的相对位置百分比用于窗口resize时保持相对位置
const ballXPercent = ref(1); // 1 表示右侧
const ballYPercent = ref(0.5); // 0.5 表示垂直居中
const isUserDragged = ref(false); // 标记用户是否手动拖动过
// 拖拽检测相关
const dragStartPosX = ref(0); // 记录拖拽开始时的实际位置
const dragStartPosY = ref(0);
const ballStyle = computed(() => ({ const ballStyle = computed(() => ({
left: `${ballX.value}px`, left: `${ballX.value}px`,
top: `${ballY.value}px` top: `${ballY.value}px`
})); }));
// 根据百分比计算实际位置
function updateBallPosition() {
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
const ballWidth = 40;
const ballHeight = 40;
const margin = 20;
// 根据百分比计算位置
if (ballXPercent.value < 0.5) {
// 左侧
ballX.value = margin;
} else {
// 右侧
ballX.value = windowWidth - ballWidth - margin;
}
// 计算Y位置确保不超出边界
let targetY = windowHeight * ballYPercent.value - ballHeight / 2;
targetY = Math.max(margin, Math.min(targetY, windowHeight - ballHeight - margin));
ballY.value = targetY;
}
// 窗口resize监听器
function handleResize() {
updateBallPosition();
}
// 初始化悬浮球位置 // 初始化悬浮球位置
onMounted(() => { onMounted(() => {
// 默认位置:右下角 // 默认位置:右垂直居中50vh
ballX.value = window.innerWidth - 100; updateBallPosition();
ballY.value = window.innerHeight - 100;
// 监听窗口resize事件
window.addEventListener('resize', handleResize);
// 加载AI助手配置
loadAgentConfig();
// 加载最近对话 // 加载最近对话
loadRecentConversations(); loadRecentConversations();
}); });
// 清理监听器和资源
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
// 释放blob URL
if (agentAvatarUrl.value && agentAvatarUrl.value.startsWith('blob:')) {
URL.revokeObjectURL(agentAvatarUrl.value);
}
});
// 开始拖动 // 开始拖动
function startDrag(e: MouseEvent | TouchEvent) { function startDrag(e: MouseEvent | TouchEvent) {
isDragging.value = true; isDragging.value = true;
@@ -274,6 +353,10 @@ function startDrag(e: MouseEvent | TouchEvent) {
dragStartX.value = clientX - ballX.value; dragStartX.value = clientX - ballX.value;
dragStartY.value = clientY - ballY.value; dragStartY.value = clientY - ballY.value;
// 记录起始位置,用于判断是点击还是拖拽
dragStartPosX.value = ballX.value;
dragStartPosY.value = ballY.value;
document.addEventListener('mousemove', onDrag); document.addEventListener('mousemove', onDrag);
document.addEventListener('touchmove', onDrag); document.addEventListener('touchmove', onDrag);
document.addEventListener('mouseup', stopDrag); document.addEventListener('mouseup', stopDrag);
@@ -306,32 +389,53 @@ function stopDrag() {
document.removeEventListener('mouseup', stopDrag); document.removeEventListener('mouseup', stopDrag);
document.removeEventListener('touchend', stopDrag); document.removeEventListener('touchend', stopDrag);
// 自动吸附到左右两侧 // 计算移动距离
const windowWidth = window.innerWidth; const moveDistanceX = Math.abs(ballX.value - dragStartPosX.value);
const ballWidth = 80; const moveDistanceY = Math.abs(ballY.value - dragStartPosY.value);
const totalDistance = Math.sqrt(moveDistanceX * moveDistanceX + moveDistanceY * moveDistanceY);
// 判断是点击还是拖拽移动距离阈值为5px
const isClick = totalDistance <= 5;
if (isClick) {
// 如果是点击,展开对话框
expandChat();
} else {
// 如果是拖拽,执行吸附和位置调整
isUserDragged.value = true;
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
const ballWidth = 40;
const ballHeight = 40;
const margin = 20;
// 自动吸附到左右两侧
if (ballX.value < windowWidth / 2) { if (ballX.value < windowWidth / 2) {
// 吸附到左侧 // 吸附到左侧
ballX.value = 20; ballX.value = margin;
ballXPercent.value = 0;
} else { } else {
// 吸附到右侧 // 吸附到右侧
ballX.value = windowWidth - ballWidth - 20; ballX.value = windowWidth - ballWidth - margin;
ballXPercent.value = 1;
} }
// 限制垂直位置 // 限制垂直位置并保存百分比
const windowHeight = window.innerHeight; if (ballY.value < margin) {
const ballHeight = 80; ballY.value = margin;
} else if (ballY.value > windowHeight - ballHeight - margin) {
ballY.value = windowHeight - ballHeight - margin;
}
if (ballY.value < 20) { // 保存Y位置的百分比以中心点计算
ballY.value = 20; ballYPercent.value = (ballY.value + ballHeight / 2) / windowHeight;
} else if (ballY.value > windowHeight - ballHeight - 20) {
ballY.value = windowHeight - ballHeight - 20;
} }
} }
// 展开对话 // 展开对话
function expandChat() { function expandChat() {
if (isDragging.value) return; // 拖动不触发 if (isDragging.value) return; // 拖动过程中不触发
isBall.value = false; isBall.value = false;
} }
@@ -351,8 +455,14 @@ const conversationPage = ref(1);
async function loadRecentConversations() { async function loadRecentConversations() {
try { try {
const result = await chatHistoryApi.getRecentConversations(10); const result = await chatHistoryApi.getRecentConversations(10);
if (result.success && result.dataList) { if (result.success) {
conversations.value = result.dataList; // 后端返回List所以数据在dataList字段
const conversationList = result.dataList || result.data;
if (conversationList && Array.isArray(conversationList)) {
conversations.value = conversationList;
} else {
conversations.value = [];
}
// 如果有对话,自动选中第一个 // 如果有对话,自动选中第一个
if (conversations.value.length > 0 && !currentConversation.value) { if (conversations.value.length > 0 && !currentConversation.value) {
@@ -371,15 +481,17 @@ async function loadMoreConversations() {
} }
// 新建对话 // 新建对话
async function createNewConversation() { async function createNewConversation(title?: string) {
try { try {
const result = await chatApi.createConversation(props.agentId); const result = await chatApi.createConversation(agentConfig.value!.id!, title);
if (result.success && result.data) { if (result.success && result.data) {
currentConversation.value = result.data; currentConversation.value = result.data;
conversations.value.unshift(result.data); conversations.value.unshift(result.data);
messages.value = []; messages.value = [];
if (!title) {
ElMessage.success('已创建新对话'); ElMessage.success('已创建新对话');
} }
}
} catch (error) { } catch (error) {
console.error('创建对话失败:', error); console.error('创建对话失败:', error);
ElMessage.error('创建对话失败'); ElMessage.error('创建对话失败');
@@ -475,6 +587,10 @@ async function saveTitle() {
const messages = ref<AiMessage[]>([]); const messages = ref<AiMessage[]>([]);
const inputMessage = ref(''); const inputMessage = ref('');
const isGenerating = ref(false); const isGenerating = ref(false);
const currentEventSource = ref<EventSource | null>(null); // 当前的EventSource连接
const currentMessageId = ref<string | null>(null); // 当前AI消息的数据库ID用于停止生成
const currentTaskId = ref<string | null>(null); // 当前任务的task_idDify的消息ID
const difyEventData = ref<Record<string, any>>({}); // 存储Dify事件数据
const chatContentRef = ref<HTMLElement | null>(null); const chatContentRef = ref<HTMLElement | null>(null);
const inputRef = ref<HTMLTextAreaElement | null>(null); const inputRef = ref<HTMLTextAreaElement | null>(null);
@@ -482,8 +598,10 @@ const inputRef = ref<HTMLTextAreaElement | null>(null);
async function loadMessages(conversationId: string) { async function loadMessages(conversationId: string) {
try { try {
const result = await chatApi.listMessages(conversationId); const result = await chatApi.listMessages(conversationId);
if (result.success && result.data) { if (result.success) {
messages.value = result.data; // 后端返回List所以数据在dataList字段
const messageList = result.dataList || result.data || [];
messages.value = Array.isArray(messageList) ? messageList : [];
await nextTick(); await nextTick();
scrollToBottom(); scrollToBottom();
} }
@@ -499,16 +617,19 @@ async function sendMessage() {
const message = inputMessage.value.trim(); const message = inputMessage.value.trim();
if (!message) return; if (!message) return;
// 如果没有当前对话,创建新对话 // 如果没有当前对话,创建新对话,使用第一个问题作为标题
if (!currentConversation.value) { const isFirstMessage = !currentConversation.value;
await createNewConversation(); if (isFirstMessage) {
// 限制标题长度为50字符
const title = message.length > 50 ? message.substring(0, 50) + '...' : message;
await createNewConversation(title);
if (!currentConversation.value) return; if (!currentConversation.value) return;
} }
// 添加用户消息到界面 // 添加用户消息到界面
const userMessage: AiMessage = { const userMessage: AiMessage = {
id: `temp-${Date.now()}`, id: `temp-user-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
conversationID: currentConversation.value.id, conversationID: currentConversation.value?.id || '',
role: 'user', role: 'user',
content: message, content: message,
createTime: new Date().toISOString(), createTime: new Date().toISOString(),
@@ -527,42 +648,89 @@ async function sendMessage() {
try { try {
let aiMessageContent = ''; let aiMessageContent = '';
await chatApi.streamChat( await chatApi.streamChat({
{ agentId: agentConfig.value!.id!,
agentId: props.agentId, conversationId: currentConversation.value?.id || '',
conversationId: currentConversation.value.id,
query: message, query: message,
knowledgeIds: [] knowledgeIds: []
}, },
{ {
onStart: (eventSource: EventSource) => {
// 保存EventSource引用用于中止
currentEventSource.value = eventSource;
// 清空之前的数据
difyEventData.value = {};
currentTaskId.value = null;
currentMessageId.value = null;
},
onInit: (initData: { messageId: string; conversationId: string }) => {
// 保存AI消息的数据库IDtask_id用于停止生成
currentMessageId.value = initData.messageId;
console.log('[保存MessageID(TaskID)]', initData.messageId);
},
onMessage: (chunk: string) => { onMessage: (chunk: string) => {
aiMessageContent += chunk; // 确保AI消息已创建即使内容为空
// 更新或创建AI消息
const lastMessage = messages.value[messages.value.length - 1]; const lastMessage = messages.value[messages.value.length - 1];
if (lastMessage && lastMessage.role === 'assistant') { if (!lastMessage || lastMessage.role !== 'assistant') {
lastMessage.content = aiMessageContent;
} else {
messages.value.push({ messages.value.push({
id: `temp-ai-${Date.now()}`, id: `temp-ai-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
conversationID: currentConversation.value!.id, conversationID: currentConversation.value!.id,
role: 'assistant', role: 'assistant',
content: aiMessageContent, content: '',
createTime: new Date().toISOString(), createTime: new Date().toISOString(),
updateTime: new Date().toISOString() updateTime: new Date().toISOString()
}); });
} }
// 累加内容包括空chunk因为后端可能分块发送
if (chunk) {
aiMessageContent += chunk;
}
// 更新AI消息内容
const aiMessage = messages.value[messages.value.length - 1];
if (aiMessage && aiMessage.role === 'assistant') {
aiMessage.content = aiMessageContent;
}
nextTick(() => scrollToBottom()); nextTick(() => scrollToBottom());
}, },
onDifyEvent: (eventType: string, eventData: any) => {
// 处理Dify原始事件包含完整信息
console.log(`[Dify事件] ${eventType}:`, eventData);
// 存储事件数据
difyEventData.value[eventType] = eventData;
// 特别处理workflow_started事件提取task_id
if (eventType === 'workflow_started' && eventData.task_id) {
currentTaskId.value = eventData.task_id;
console.log('[Task ID]', eventData.task_id);
}
// 可以根据需要处理其他事件类型
// 例如node_started, node_finished, agent_thought等
},
onMessageEnd: () => { onMessageEnd: () => {
// 消息结束 // 消息结束,如果是第一条消息,更新会话列表中的标题
if (isFirstMessage && currentConversation.value) {
const convIndex = conversations.value.findIndex(c => c.id === currentConversation.value!.id);
if (convIndex !== -1) {
conversations.value[convIndex].title = currentConversation.value.title;
}
}
isGenerating.value = false; isGenerating.value = false;
currentEventSource.value = null;
currentTaskId.value = null;
currentMessageId.value = null;
}, },
onError: (error: Error) => { onError: (error: Error) => {
console.error('对话失败:', error); console.error('对话失败:', error);
ElMessage.error('对话失败,请重试'); ElMessage.error('对话失败,请重试');
isGenerating.value = false; isGenerating.value = false;
currentEventSource.value = null;
currentTaskId.value = null;
currentMessageId.value = null;
} }
} }
); );
@@ -570,6 +738,7 @@ async function sendMessage() {
console.error('发送消息失败:', error); console.error('发送消息失败:', error);
ElMessage.error('发送消息失败'); ElMessage.error('发送消息失败');
isGenerating.value = false; isGenerating.value = false;
currentEventSource.value = null;
} }
} }
@@ -590,6 +759,53 @@ function scrollToBottom() {
} }
} }
// 停止生成
async function stopGenerating() {
// 优先使用 taskId如果没有则使用 messageId
const taskIdToStop = currentTaskId.value || currentMessageId.value;
if (!taskIdToStop || !agentConfig.value?.id) {
ElMessage.warning('无法停止:缺少必要信息');
return;
}
try {
// 只调用后端API停止Dify生成不关闭EventSource
// EventSource会在收到Dify的停止/完成事件后自动关闭
const result = await chatApi.stopChatByTaskId(taskIdToStop, agentConfig.value.id);
if (result.success) {
ElMessage.success('正在停止生成...');
// 注意不在这里关闭EventSource和清理状态
// 等待Dify发送complete/error事件由onMessageEnd/onError处理
} else {
ElMessage.warning(result.message || '停止生成失败');
// 如果后端返回失败,手动清理状态
if (currentEventSource.value) {
currentEventSource.value.close();
currentEventSource.value = null;
}
isGenerating.value = false;
currentTaskId.value = null;
currentMessageId.value = null;
difyEventData.value = {};
}
} catch (error) {
console.error('停止生成失败:', error);
ElMessage.error('停止生成失败');
// API调用失败时手动清理状态
if (currentEventSource.value) {
currentEventSource.value.close();
currentEventSource.value = null;
}
isGenerating.value = false;
currentTaskId.value = null;
currentMessageId.value = null;
difyEventData.value = {};
}
}
// 复制消息 // 复制消息
async function copyMessage(content: string) { async function copyMessage(content: string) {
try { try {
@@ -604,6 +820,16 @@ async function copyMessage(content: string) {
async function regenerateMessage(messageId: string) { async function regenerateMessage(messageId: string) {
if (isGenerating.value) return; if (isGenerating.value) return;
// 清空原有AI消息内容
const messageIndex = messages.value.findIndex(m => m.id === messageId);
if (messageIndex === -1) {
ElMessage.error('消息不存在');
return;
}
// 清空内容,准备重新生成
messages.value[messageIndex].content = '';
isGenerating.value = true; isGenerating.value = true;
try { try {
@@ -612,24 +838,59 @@ async function regenerateMessage(messageId: string) {
await chatApi.regenerateAnswer( await chatApi.regenerateAnswer(
messageId, messageId,
{ {
onStart: (eventSource: EventSource) => {
// 保存EventSource引用用于中止
currentEventSource.value = eventSource;
// 清空之前的数据
difyEventData.value = {};
currentTaskId.value = null;
currentMessageId.value = null;
},
onInit: (initData: { messageId: string; conversationId: string }) => {
// 保存AI消息的数据库IDtask_id用于停止生成
currentMessageId.value = initData.messageId;
console.log('[保存MessageID(TaskID)-重新生成]', initData.messageId);
},
onMessage: (chunk: string) => { onMessage: (chunk: string) => {
// 累加内容包括空chunk因为后端可能分块发送
if (chunk) {
aiMessageContent += chunk; aiMessageContent += chunk;
}
// 找到对应消息并更新 // 找到对应消息并更新
const messageIndex = messages.value.findIndex(m => m.id === messageId); const msgIndex = messages.value.findIndex(m => m.id === messageId);
if (messageIndex !== -1) { if (msgIndex !== -1) {
messages.value[messageIndex].content = aiMessageContent; messages.value[msgIndex].content = aiMessageContent;
} }
nextTick(() => scrollToBottom()); nextTick(() => scrollToBottom());
}, },
onDifyEvent: (eventType: string, eventData: any) => {
// 处理Dify原始事件包含完整信息
console.log(`[Dify事件-重新生成] ${eventType}:`, eventData);
// 存储事件数据
difyEventData.value[eventType] = eventData;
// 特别处理workflow_started事件提取task_id
if (eventType === 'workflow_started' && eventData.task_id) {
currentTaskId.value = eventData.task_id;
console.log('[Task ID-重新生成]', eventData.task_id);
}
},
onMessageEnd: () => { onMessageEnd: () => {
isGenerating.value = false; isGenerating.value = false;
currentEventSource.value = null;
currentTaskId.value = null;
currentMessageId.value = null;
}, },
onError: (error: Error) => { onError: (error: Error) => {
console.error('重新生成失败:', error); console.error('重新生成失败:', error);
ElMessage.error('重新生成失败'); ElMessage.error('重新生成失败');
isGenerating.value = false; isGenerating.value = false;
currentEventSource.value = null;
currentTaskId.value = null;
currentMessageId.value = null;
} }
} }
); );
@@ -637,6 +898,7 @@ async function regenerateMessage(messageId: string) {
console.error('重新生成失败:', error); console.error('重新生成失败:', error);
ElMessage.error('重新生成失败'); ElMessage.error('重新生成失败');
isGenerating.value = false; isGenerating.value = false;
currentEventSource.value = null;
} }
} }
@@ -746,16 +1008,14 @@ onUnmounted(() => {
<style scoped lang="scss"> <style scoped lang="scss">
.ai-agent { .ai-agent {
position: fixed; position: fixed;
z-index: 9999; z-index: 50;
&.expanded { &.expanded {
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
width: 90vw; width: 90vw;
max-width: 1200px;
height: 80vh; height: 80vh;
max-height: 800px;
} }
} }
@@ -766,8 +1026,8 @@ onUnmounted(() => {
} }
.chat-ball { .chat-ball {
width: 80px; width: 40px;
height: 80px; height: 40px;
cursor: pointer; cursor: pointer;
position: relative; position: relative;
transition: transform 0.3s ease; transition: transform 0.3s ease;
@@ -818,7 +1078,7 @@ onUnmounted(() => {
/* ===== 左侧对话历史 ===== */ /* ===== 左侧对话历史 ===== */
.ai-agent-history { .ai-agent-history {
width: 280px; width: 240px;
background: #F9FAFB; background: #F9FAFB;
border-right: 1px solid #E5E7EB; border-right: 1px solid #E5E7EB;
display: flex; display: flex;
@@ -848,7 +1108,7 @@ onUnmounted(() => {
border: none; border: none;
cursor: pointer; cursor: pointer;
font-size: 12px; font-size: 12px;
color: #6B7280; color: #6B7240;
&:hover { &:hover {
color: #E7000B; color: #E7000B;
@@ -918,7 +1178,7 @@ onUnmounted(() => {
.conv-time { .conv-time {
font-size: 12px; font-size: 12px;
color: #6B7280; color: #6B7240;
} }
.delete-conv-btn { .delete-conv-btn {
@@ -958,6 +1218,28 @@ onUnmounted(() => {
} }
} }
.avatar-circle {
width: 36px;
height: 36px;
border-radius: 50%;
background: #E5E7EB;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
overflow: hidden;
&.ai-avatar {
background: #EFF6FF;
}
.ai-avatar-img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
/* ===== 右侧当前对话 ===== */ /* ===== 右侧当前对话 ===== */
.ai-agent-current-chat { .ai-agent-current-chat {
flex: 1; flex: 1;
@@ -1026,7 +1308,7 @@ onUnmounted(() => {
span { span {
font-size: 20px; font-size: 20px;
color: #6B7280; color: #6B7240;
} }
} }
} }
@@ -1044,6 +1326,16 @@ onUnmounted(() => {
.welcome-icon { .welcome-icon {
font-size: 64px; font-size: 64px;
margin-bottom: 16px; margin-bottom: 16px;
display: flex;
align-items: center;
justify-content: center;
.welcome-avatar {
width: 80px;
height: 80px;
border-radius: 50%;
object-fit: cover;
}
} }
h2 { h2 {
@@ -1055,7 +1347,7 @@ onUnmounted(() => {
p { p {
font-size: 16px; font-size: 16px;
color: #6B7280; color: #6B7240;
margin: 0; margin: 0;
} }
} }
@@ -1066,24 +1358,12 @@ onUnmounted(() => {
.message { .message {
display: flex; display: flex;
gap: 12px; gap: 12px;
align-items: flex-start;
.message-avatar { .message-avatar {
flex-shrink: 0; flex-shrink: 0;
.avatar-circle {
width: 36px;
height: 36px;
border-radius: 50%;
background: #E5E7EB;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
&.ai-avatar {
background: #EFF6FF;
}
}
} }
.message-content { .message-content {
@@ -1097,6 +1377,17 @@ onUnmounted(() => {
line-height: 1.6; line-height: 1.6;
word-wrap: break-word; word-wrap: break-word;
// 图片样式约束
:deep(img) {
max-width: 100%;
max-height: 400px;
height: auto;
border-radius: 8px;
margin: 8px 0;
display: block;
object-fit: contain;
}
:deep(code) { :deep(code) {
background: #F3F4F6; background: #F3F4F6;
padding: 2px 6px; padding: 2px 6px;
@@ -1163,9 +1454,14 @@ onUnmounted(() => {
} }
} }
// 用户消息靠右(头像在右侧)
.user-message { .user-message {
flex-direction: row-reverse;
.message-content { .message-content {
margin-left: auto; display: flex;
flex-direction: column;
align-items: flex-end;
.message-text { .message-text {
background: #E7000B; background: #E7000B;
@@ -1174,6 +1470,7 @@ onUnmounted(() => {
} }
} }
// AI消息靠左头像在左侧默认布局
.ai-message { .ai-message {
.message-content { .message-content {
.message-text { .message-text {
@@ -1308,6 +1605,21 @@ onUnmounted(() => {
} }
} }
&.stop-btn {
background: #FEF2F2;
border-color: #FCA5A5;
.stop-icon {
font-size: 18px;
color: #DC2626;
}
&:hover {
background: #FEE2E2;
border-color: #F87171;
}
}
.link-icon, .link-icon,
.send-icon { .send-icon {
width: 20px; width: 20px;
@@ -1320,9 +1632,13 @@ onUnmounted(() => {
} }
@keyframes typing { @keyframes typing {
0%, 60%, 100% {
0%,
60%,
100% {
transform: translateY(0); transform: translateY(0);
} }
30% { 30% {
transform: translateY(-8px); transform: translateY(-8px);
} }