diff --git a/frontend/app/web-gold/src/components/agents/ChatDrawer.vue b/frontend/app/web-gold/src/components/agents/ChatDrawer.vue
index dfdeb8ad68..a7143270a1 100644
--- a/frontend/app/web-gold/src/components/agents/ChatDrawer.vue
+++ b/frontend/app/web-gold/src/components/agents/ChatDrawer.vue
@@ -20,6 +20,7 @@ import {
AlertDialogTitle
} from '@/components/ui/alert-dialog'
import { sendChatStream } from '@/api/agent'
+import { copyToClipboard } from '@/utils/clipboard'
import HistoryPanel from './HistoryPanel.vue'
import ChatDrawerHeader from './ChatDrawerHeader.vue'
import ChatDrawerEmpty from './ChatDrawerEmpty.vue'
@@ -90,10 +91,10 @@ const handleGenerate = async () => {
}
const handleCopy = async () => {
- try {
- await navigator.clipboard.writeText(generatedContent.value)
+ const success = await copyToClipboard(generatedContent.value)
+ if (success) {
toast.success('已复制')
- } catch {
+ } else {
toast.error('复制失败')
}
}
diff --git a/frontend/app/web-gold/src/components/agents/HistoryPanel.vue b/frontend/app/web-gold/src/components/agents/HistoryPanel.vue
index 62458862d3..3c28d86b54 100644
--- a/frontend/app/web-gold/src/components/agents/HistoryPanel.vue
+++ b/frontend/app/web-gold/src/components/agents/HistoryPanel.vue
@@ -89,6 +89,18 @@
+
+
+
+
+
@@ -171,8 +183,10 @@ const conversationList = ref([])
const selectedConversation = ref(null)
const messageList = ref([])
const messageLoading = ref(false)
+const lastId = ref(null)
+const hasMore = ref(true)
-// 按日期分组
+// 按日期分组(先排序再分组)
const groupedConversations = computed(() => {
const today = dayjs().startOf('day')
const yesterday = dayjs().subtract(1, 'day').startOf('day')
@@ -185,7 +199,14 @@ const groupedConversations = computed(() => {
older: { label: '更早', items: [] }
}
- conversationList.value.forEach(item => {
+ // 按时间降序排序
+ const sortedList = [...conversationList.value].sort((a, b) => {
+ const timeA = (a.updatedAt || a.createdAt) * 1000
+ const timeB = (b.updatedAt || b.createdAt) * 1000
+ return timeB - timeA
+ })
+
+ sortedList.forEach(item => {
const date = dayjs((item.updatedAt || item.createdAt) * 1000)
if (date.isAfter(today)) {
groups.today.items.push(item)
@@ -202,13 +223,27 @@ const groupedConversations = computed(() => {
})
// Methods
-const loadConversations = async () => {
+const loadConversations = async (loadMore = false) => {
if (!props.agentId) return
+ if (loadMore && !hasMore.value) return
+
loading.value = true
try {
- const res = await getConversations({ agentId: props.agentId, limit: 50 })
+ const params = { agentId: props.agentId, limit: 20 }
+ if (loadMore && lastId.value) {
+ params.lastId = lastId.value
+ }
+ const res = await getConversations(params)
if (res.code === 0) {
- conversationList.value = res.data?.data || []
+ const data = res.data?.data || []
+ if (loadMore) {
+ conversationList.value = [...conversationList.value, ...data]
+ } else {
+ conversationList.value = data
+ }
+ // 更新分页状态
+ lastId.value = data.length > 0 ? data[data.length - 1].id : null
+ hasMore.value = data.length === 20 && res.data?.hasMore !== false
}
} catch (e) {
console.error('加载会话列表失败:', e)
@@ -249,7 +284,11 @@ const handleClose = () => {
const copyContent = async (content) => {
const success = await copyToClipboard(content)
- success ? toast.success('已复制到剪贴板') : toast.error('复制失败')
+ if (success) {
+ toast.success('已复制到剪贴板')
+ } else {
+ toast.error('复制失败')
+ }
}
const formatTime = (timestamp) => {
@@ -257,15 +296,10 @@ const formatTime = (timestamp) => {
const date = dayjs(timestamp * 1000)
const now = dayjs()
- if (date.isSame(now, 'day')) {
- return date.format('HH:mm')
- } else if (date.isSame(now.subtract(1, 'day'), 'day')) {
- return '昨天 ' + date.format('HH:mm')
- } else if (date.isAfter(now.subtract(7, 'day'))) {
- return date.format('dddd HH:mm')
- } else {
- return date.format('MM-DD HH:mm')
- }
+ if (date.isSame(now, 'day')) return date.format('HH:mm')
+ if (date.isSame(now.subtract(1, 'day'), 'day')) return '昨天 ' + date.format('HH:mm')
+ if (date.isAfter(now.subtract(7, 'day'))) return date.format('dddd HH:mm')
+ return date.format('MM-DD HH:mm')
}
// Watch
@@ -530,6 +564,36 @@ watch(() => props.visible, (val) => {
gap: 24px;
}
+.load-more {
+ display: flex;
+ justify-content: center;
+ padding-top: 8px;
+}
+
+.load-more-btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 20px;
+ border: 1px solid var(--color-gray-200);
+ background: var(--color-bg-card);
+ border-radius: 10px;
+ font-size: 14px;
+ color: var(--color-gray-600);
+ cursor: pointer;
+ transition: all 0.2s ease;
+
+ &:hover:not(:disabled) {
+ border-color: var(--color-primary-300);
+ color: var(--color-primary-500);
+ }
+
+ &:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ }
+}
+
.conversation-group {
animation: groupFadeIn 0.4s ease-out backwards;
animation-delay: calc(var(--group-index) * 0.1s);
diff --git a/sql/mysql/redeem_code_menu.sql b/sql/mysql/redeem_code_menu.sql
index 59bdd03b40..19781d9b53 100644
--- a/sql/mysql/redeem_code_menu.sql
+++ b/sql/mysql/redeem_code_menu.sql
@@ -8,7 +8,7 @@
-- 1. 兑换码管理菜单(目录)
INSERT INTO `system_menu` (`name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
-VALUES ('兑换码管理', '', 2, 20, 0, 'redeemcode', 'ep:tickets', 'muye/redeemcode/index', 'RedeemCode', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0');
+VALUES ('兑换码管理', '', 2, 20, 0, '/redeemcode', 'ep:tickets', 'muye/redeemcode/index', 'RedeemCode', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0');
-- 获取刚插入的菜单ID(兑换码管理)
SET @redeem_code_menu_id = LAST_INSERT_ID();
@@ -31,7 +31,7 @@ VALUES ('兑换码导出', 'muye:redeem-code:export', 3, 4, @redeem_code_menu_id
-- 2. 兑换记录菜单(目录)
INSERT INTO `system_menu` (`name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
-VALUES ('兑换记录', '', 2, 21, 0, 'redeemrecord', 'ep:document', 'muye/redeemrecord/index', 'RedeemRecord', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0');
+VALUES ('兑换记录', '', 2, 21, 0, '/redeemrecord', 'ep:document', 'muye/redeemrecord/index', 'RedeemRecord', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0');
-- 获取刚插入的菜单ID(兑换记录)
SET @redeem_record_menu_id = LAST_INSERT_ID();
diff --git a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/dify/service/DifyServiceImpl.java b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/dify/service/DifyServiceImpl.java
index 16c22d1ae0..39452e4639 100644
--- a/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/dify/service/DifyServiceImpl.java
+++ b/yudao-module-tik/src/main/java/cn/iocoder/yudao/module/tik/dify/service/DifyServiceImpl.java
@@ -25,6 +25,7 @@ import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
/**
* Dify 服务实现类
@@ -53,8 +54,8 @@ public class DifyServiceImpl implements DifyService {
AtomicReference conversationIdRef = new AtomicReference<>(reqVO.getConversationId());
// 用于存储 token 使用信息
AtomicReference tokenUsageRef = new AtomicReference<>();
- // Dify 用户标识(固定格式)
- String difyUserId = "user-" + userId;
+ // Dify 用户标识(按 agentId 隔离会话)
+ String difyUserId = "user-" + userId + "-agent-" + reqVO.getAgentId();
return Mono.fromCallable(() -> {
// 1. 获取智能体配置
@@ -179,8 +180,10 @@ public class DifyServiceImpl implements DifyService {
AtomicReference conversationIdRef = new AtomicReference<>("");
// 用于存储 token 使用信息
AtomicReference tokenUsageRef = new AtomicReference<>();
- // Dify 用户标识(固定格式)
- String difyUserId = "user-" + userId;
+ // Dify 用户标识(按 agentId 隔离会话,无 agentId 时使用默认)
+ String difyUserId = reqVO.getAgentId() != null
+ ? "user-" + userId + "-agent-" + reqVO.getAgentId()
+ : "user-" + userId;
return Mono.fromCallable(() -> {
// 1. 获取系统提示词
@@ -653,6 +656,78 @@ public class DifyServiceImpl implements DifyService {
*/
private record PromptAnalysisContext(String prompt, String apiKey, Integer consumePoints) {}
+ /**
+ * 通用的流式响应扣费处理
+ */
+ private void handlePointsDeduction(AtomicLong pendingRecordId,
+ AtomicReference tokenUsageRef,
+ String logPrefix) {
+ if (pendingRecordId.get() <= 0) return;
+ try {
+ DifyChatRespVO tokenUsage = tokenUsageRef.get();
+ if (tokenUsage != null) {
+ pointsService.confirmPendingDeductWithTokens(
+ pendingRecordId.get(),
+ tokenUsage.getInputTokens(),
+ tokenUsage.getOutputTokens(),
+ tokenUsage.getTotalTokens()
+ );
+ log.info("[{}] 流结束,确认扣费(带token),记录ID: {}, tokens: {}",
+ logPrefix, pendingRecordId.get(), tokenUsage.getTotalTokens());
+ } else {
+ pointsService.confirmPendingDeduct(pendingRecordId.get());
+ log.info("[{}] 流结束,确认扣费(无token),记录ID: {}", logPrefix, pendingRecordId.get());
+ }
+ } catch (Exception e) {
+ log.error("[{}] 确认扣费失败", logPrefix, e);
+ }
+ }
+
+ /**
+ * 通用的取消预扣处理
+ */
+ private void handlePointsCancellation(AtomicLong pendingRecordId, String logPrefix) {
+ if (pendingRecordId.get() <= 0) return;
+ try {
+ pointsService.cancelPendingDeduct(pendingRecordId.get());
+ log.info("[{}] 流出错,取消预扣,记录ID: {}", logPrefix, pendingRecordId.get());
+ } catch (Exception e) {
+ log.error("[{}] 取消预扣失败", logPrefix, e);
+ }
+ }
+
+ /**
+ * 通用的用户取消扣费处理
+ */
+ private void handleUserCancellation(AtomicLong pendingRecordId, String logPrefix) {
+ if (pendingRecordId.get() <= 0) return;
+ try {
+ pointsService.confirmPendingDeduct(pendingRecordId.get());
+ log.info("[{}] 用户取消,确认扣费,记录ID: {}", logPrefix, pendingRecordId.get());
+ } catch (Exception e) {
+ log.error("[{}] 用户取消后扣费失败", logPrefix, e);
+ }
+ }
+
+ /**
+ * 构建 done 事件 Mono
+ */
+ private Mono buildDoneMono(AtomicReference conversationIdRef,
+ AtomicReference tokenUsageRef) {
+ return Mono.defer(() -> {
+ DifyChatRespVO tokenUsage = tokenUsageRef.get();
+ if (tokenUsage != null) {
+ return Mono.just(DifyChatRespVO.doneWithTokens(
+ conversationIdRef.get(), null,
+ tokenUsage.getInputTokens(),
+ tokenUsage.getOutputTokens(),
+ tokenUsage.getTotalTokens()
+ ));
+ }
+ return Mono.just(DifyChatRespVO.done(conversationIdRef.get(), null));
+ });
+ }
+
@Override
public DifyConversationListRespVO getConversations(Long agentId, String userId, String lastId, Integer limit) {
// 获取智能体配置
@@ -666,8 +741,8 @@ public class DifyServiceImpl implements DifyService {
AiPlatformEnum.DIFY.getPlatform(),
AiModelTypeEnum.DIFY_WRITING_STANDARD.getModelCode());
- // Dify 用户标识
- String difyUserId = "user-" + userId;
+ // Dify 用户标识(按 agentId 隔离会话)
+ String difyUserId = "user-" + userId + "-agent-" + agentId;
DifyConversationListRespVO result = difyClient.getConversations(config.getApiKey(), difyUserId, lastId, limit);
@@ -696,8 +771,8 @@ public class DifyServiceImpl implements DifyService {
AiPlatformEnum.DIFY.getPlatform(),
AiModelTypeEnum.DIFY_WRITING_STANDARD.getModelCode());
- // Dify 用户标识
- String difyUserId = "user-" + userId;
+ // Dify 用户标识(按 agentId 隔离会话)
+ String difyUserId = "user-" + userId + "-agent-" + agentId;
DifyMessageListRespVO result = difyClient.getMessages(config.getApiKey(), conversationId, difyUserId, firstId, limit);