diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/agent/SessionService.java b/data-agent-backend/src/main/java/io/github/malonetalk/agent/SessionService.java index fcc8952..5cfe530 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/agent/SessionService.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/agent/SessionService.java @@ -30,9 +30,11 @@ import io.agentscope.core.state.SimpleSessionKey; import io.github.malonetalk.convertor.handler.ToolResultHandler; import io.github.malonetalk.dto.ChatStreamEvent; +import io.github.malonetalk.dto.SessionDatasourceBinding; import io.github.malonetalk.dto.SessionInfo; import io.github.malonetalk.dto.TurnItem; import io.github.malonetalk.enums.ChatStreamEventType; +import io.github.malonetalk.mapper.SessionDatasourceMapper; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -47,22 +49,27 @@ import javax.sql.DataSource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Slf4j @Service public class SessionService { private final DataSource dataSource; + private final SessionDatasourceMapper sessionDatasourceMapper; private final Map sessionCache = new ConcurrentHashMap<>(); - public SessionService(DataSource dataSource) { + public SessionService(DataSource dataSource, SessionDatasourceMapper sessionDatasourceMapper) { this.dataSource = dataSource; + this.sessionDatasourceMapper = sessionDatasourceMapper; } public Session getOrCreateSession(String sessionId) { - return sessionCache.computeIfAbsent( - sessionId, - k -> new MysqlSession(dataSource, "data_agent", "agentscope_sessions", false)); + return sessionCache.computeIfAbsent(sessionId, k -> createMysqlSession()); + } + + private MysqlSession createMysqlSession() { + return new MysqlSession(dataSource, "data_agent", "agentscope_sessions", false); } public List getSessionDebug(String sessionId) { @@ -185,30 +192,28 @@ private static ChatStreamEvent thinkingEvent(String thinking) { .build(); } + // TODO 2026/08/11 这里保留agentscope 的 MysqlSession.delete() ,虽然大概率不走 Spring 事务, + // 但注解至少明确了这是跨表操作的语义边界,后续如果有人接手知道这里需要考虑事务一致性。后期有时间回来完善这里的双表操作的事务 + @Transactional public void clearSession(String sessionId) { Session session = sessionCache.remove(sessionId); - if (session != null) { - session.delete(SimpleSessionKey.of(sessionId)); - } - // 一并清理会话的数据源绑定,避免孤儿绑定。 - try (Connection conn = dataSource.getConnection(); - PreparedStatement ps = - conn.prepareStatement( - "DELETE FROM session_datasource WHERE session_id = ?")) { - ps.setString(1, sessionId); - ps.executeUpdate(); - } catch (Exception e) { - log.error("Error deleting session datasource binding", e); + if (session == null) { + session = createMysqlSession(); } + session.delete(SimpleSessionKey.of(sessionId)); + sessionDatasourceMapper.deleteBySessionId(sessionId); } + @Transactional public void clearAllSessions() { - sessionCache.clear(); + Set sessionIds = Set.copyOf(sessionCache.keySet()); + for (String sessionId : sessionIds) { + clearSession(sessionId); + } } public List listSessions() { - MysqlSession session = - new MysqlSession(dataSource, "data_agent", "agentscope_sessions", false); + MysqlSession session = createMysqlSession(); Set keys = session.listSessionKeys(); if (keys == null || keys.isEmpty()) { @@ -230,18 +235,16 @@ public List listSessions() { } Map bindings = new HashMap<>(); - try (Connection conn = dataSource.getConnection(); - PreparedStatement ps = - conn.prepareStatement( - "SELECT sd.session_id, sd.datasource_id, d.name" - + " FROM session_datasource sd" - + " LEFT JOIN datasource d ON sd.datasource_id = d.id"); - ResultSet rs = ps.executeQuery()) { - while (rs.next()) { - bindings.put(rs.getString(1), new String[] {rs.getString(2), rs.getString(3)}); - } - } catch (Exception e) { - log.error("Error listing session datasource bindings", e); + for (SessionDatasourceBinding row : + sessionDatasourceMapper.listBindingsWithDatasourceName()) { + bindings.put( + row.getSessionId(), + new String[] { + row.getDatasourceId() != null + ? String.valueOf(row.getDatasourceId()) + : null, + row.getDatasourceName() + }); } List result = new ArrayList<>(); diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/dto/SessionDatasourceBinding.java b/data-agent-backend/src/main/java/io/github/malonetalk/dto/SessionDatasourceBinding.java new file mode 100644 index 0000000..8438234 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/dto/SessionDatasourceBinding.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2026 github.com/MaloneTalk + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * limitations under the License. + */ +package io.github.malonetalk.dto; + +import lombok.Data; + +/** {@link io.github.malonetalk.mapper.SessionDatasourceMapper#listBindingsWithDatasourceName()} 的结果行。 */ +@Data +public class SessionDatasourceBinding { + + private String sessionId; + private Integer datasourceId; + private String datasourceName; +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/mapper/SessionDatasourceMapper.java b/data-agent-backend/src/main/java/io/github/malonetalk/mapper/SessionDatasourceMapper.java index 852d6f6..396736e 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/mapper/SessionDatasourceMapper.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/mapper/SessionDatasourceMapper.java @@ -17,7 +17,9 @@ */ package io.github.malonetalk.mapper; +import io.github.malonetalk.dto.SessionDatasourceBinding; import io.github.malonetalk.entity.SessionDatasource; +import java.util.List; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; @@ -28,4 +30,9 @@ public interface SessionDatasourceMapper { int insertIfAbsent(SessionDatasource sessionDatasource); SessionDatasource selectBySessionId(@Param("sessionId") String sessionId); + + int deleteBySessionId(@Param("sessionId") String sessionId); + + /** 列出所有绑定并关联数据源名称。 */ + List listBindingsWithDatasourceName(); } diff --git a/data-agent-backend/src/main/resources/mapper/SessionDatasourceMapper.xml b/data-agent-backend/src/main/resources/mapper/SessionDatasourceMapper.xml index e43f1ec..ac49a1f 100644 --- a/data-agent-backend/src/main/resources/mapper/SessionDatasourceMapper.xml +++ b/data-agent-backend/src/main/resources/mapper/SessionDatasourceMapper.xml @@ -18,4 +18,16 @@ SELECT * FROM session_datasource WHERE session_id = #{sessionId} + + DELETE FROM session_datasource WHERE session_id = #{sessionId} + + + + diff --git a/data-agent-frontend/eslint.config.js b/data-agent-frontend/eslint.config.js index 8cf3f91..932d978 100644 --- a/data-agent-frontend/eslint.config.js +++ b/data-agent-frontend/eslint.config.js @@ -194,6 +194,8 @@ export default tseslint.config( parseFloat: 'readonly', isNaN: 'readonly', isFinite: 'readonly', + Event: 'readonly', + MouseEvent: 'readonly', }, }, rules: { @@ -253,6 +255,8 @@ export default tseslint.config( isFinite: 'readonly', AbortController: 'readonly', AbortSignal: 'readonly', + Event: 'readonly', + MouseEvent: 'readonly', }, }, rules: { diff --git a/data-agent-frontend/src/api/agent.ts b/data-agent-frontend/src/api/agent.ts index bffe3a4..b723b83 100644 --- a/data-agent-frontend/src/api/agent.ts +++ b/data-agent-frontend/src/api/agent.ts @@ -108,8 +108,11 @@ function authHeaders(): Record { return headers; } -async function fetchJson(url: string, fallback: string): Promise { - const response = await fetch(url, { headers: authHeaders() }); +async function fetchJson(url: string, fallback: string, init?: RequestInit): Promise { + const response = await fetch(url, { + ...init, + headers: { ...authHeaders(), ...(init?.headers as Record) }, + }); if (response.status === 401) { handleUnauthorized(); throw new ApiError('登录已过期,请重新登录', { code: 401 }); @@ -139,6 +142,14 @@ export async function fetchSessionHistory(sessionId: string): Promise { + await fetchJson( + `/api/agent/session/${encodeURIComponent(sessionId)}`, + 'Failed to delete session', + { method: 'DELETE' }, + ); +} + export async function* streamChat( request: ChatRequest, abortSignal?: AbortSignal, diff --git a/data-agent-frontend/src/views/chat/ChatView.vue b/data-agent-frontend/src/views/chat/ChatView.vue index a0dc4af..9bc60bf 100644 --- a/data-agent-frontend/src/views/chat/ChatView.vue +++ b/data-agent-frontend/src/views/chat/ChatView.vue @@ -180,6 +180,7 @@ ref="sessionListRef" :active-session-id="activeSessionId" @new-session="handleNewSession" + @session-deleted="sessionListRef?.loadList()" /> diff --git a/data-agent-frontend/src/views/chat/components/SessionList.vue b/data-agent-frontend/src/views/chat/components/SessionList.vue index 206d557..83c5256 100644 --- a/data-agent-frontend/src/views/chat/components/SessionList.vue +++ b/data-agent-frontend/src/views/chat/components/SessionList.vue @@ -18,14 +18,16 @@