diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/agent/AgentService.java b/data-agent-backend/src/main/java/io/github/malonetalk/agent/AgentService.java index 8a0591d..aebdb89 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/agent/AgentService.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/agent/AgentService.java @@ -40,6 +40,7 @@ import io.github.malonetalk.enums.ChatStreamEventType; import io.github.malonetalk.exception.ErrorResponse; import io.github.malonetalk.exception.ExceptionResponseMapper; +import io.github.malonetalk.service.DatasourceService; import io.github.malonetalk.web.TraceIdFilter; import jakarta.annotation.PostConstruct; import java.util.List; @@ -62,6 +63,7 @@ public class AgentService { private final SkillLoaderService skillLoaderService; private final ExceptionResponseMapper exceptionResponseMapper; private final EventConverter eventConverter; + private final DatasourceService datasourceService; private Toolkit toolkit; private SkillBox skillBox; @@ -73,13 +75,23 @@ public void init() { } public Flux chatStream( - String sessionId, String userInput, List toolResults) { - return Flux.defer(() -> streamAgent(sessionId, userInput, toolResults)) + String sessionId, + String userInput, + List toolResults, + Integer datasourceId) { + return Flux.defer(() -> streamAgent(sessionId, userInput, toolResults, datasourceId)) .onErrorResume(this::toErrorEvent); } private Flux streamAgent( - String sessionId, String userInput, List toolResults) { + String sessionId, + String userInput, + List toolResults, + Integer datasourceId) { + if (datasourceId != null) { + // 首次绑定;已绑定会被 INSERT IGNORE 忽略,锁定语义在 mapper 层保证。 + datasourceService.bindSessionDatasource(sessionId, datasourceId); + } ReActAgent agent = createAgent(ToolCallContext.builder().sessionId(sessionId).build()); Session session = sessionService.getOrCreateSession(sessionId); 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 a001698..fcc8952 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 @@ -190,6 +190,16 @@ public void clearSession(String 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); + } } public void clearAllSessions() { @@ -219,6 +229,21 @@ public List listSessions() { log.error("Error listing session timestamps", e); } + 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); + } + List result = new ArrayList<>(); for (SessionKey key : keys) { String sid = key.toIdentifier(); @@ -241,7 +266,12 @@ public List listSessions() { } String[] times = timestamps.getOrDefault(sid, new String[] {"", ""}); - result.add(new SessionInfo(sid, title, times[0], times[1])); + String[] binding = bindings.get(sid); + Integer datasourceId = + binding == null || binding[0] == null ? null : Integer.valueOf(binding[0]); + String datasourceName = binding == null ? null : binding[1]; + result.add( + new SessionInfo(sid, title, times[0], times[1], datasourceId, datasourceName)); } result.sort((a, b) -> b.lastActiveAt().compareTo(a.lastActiveAt())); diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/ExecuteSqlTool.java b/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/ExecuteSqlTool.java index 64d5ade..8eba015 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/ExecuteSqlTool.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/ExecuteSqlTool.java @@ -20,11 +20,10 @@ import io.agentscope.core.message.ToolResultBlock; import io.agentscope.core.tool.Tool; import io.agentscope.core.tool.ToolParam; +import io.github.malonetalk.agent.ToolCallContext; import io.github.malonetalk.agent.datasource.QueryResult; import io.github.malonetalk.agent.datasource.SqlExecutor; -import io.github.malonetalk.common.ErrorCode; import io.github.malonetalk.entity.Datasource; -import io.github.malonetalk.exception.BusinessException; import io.github.malonetalk.exception.ToolExceptionMapper; import io.github.malonetalk.service.DatasourceService; import lombok.AllArgsConstructor; @@ -46,18 +45,12 @@ public class ExecuteSqlTool implements MarkAgentTool { + " other modification operations.") public ToolResultBlock executeSql( @ToolParam(name = "sql", description = "The SELECT SQL query statement to execute") - String sql) { + String sql, + ToolCallContext ctx) { return toolExceptionMapper.run( () -> { Datasource datasource = - dataSourceService - .getActiveDatasource() - .orElseThrow( - () -> - BusinessException.of( - ErrorCode.NO_ACTIVE_DATASOURCE, - "No active datasource is available." - + " Unable to execute SQL.")); + dataSourceService.getDatasourceForSession(ctx.sessionId()); QueryResult result = sqlExecutor.execute(datasource, sql); return ToolResultBlock.text(formatResult(result)); }); diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/GetTableSchemaTool.java b/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/GetTableSchemaTool.java index da6f1d8..604b1dd 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/GetTableSchemaTool.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/GetTableSchemaTool.java @@ -20,10 +20,9 @@ import io.agentscope.core.message.ToolResultBlock; import io.agentscope.core.tool.Tool; import io.agentscope.core.tool.ToolParam; -import io.github.malonetalk.common.ErrorCode; +import io.github.malonetalk.agent.ToolCallContext; import io.github.malonetalk.dto.prompt.ColumnPromptResponse; import io.github.malonetalk.entity.Datasource; -import io.github.malonetalk.exception.BusinessException; import io.github.malonetalk.exception.ToolExceptionMapper; import io.github.malonetalk.service.DatasourceService; import io.github.malonetalk.service.semantic.column.ColumnSemanticService; @@ -53,19 +52,12 @@ public class GetTableSchemaTool implements MarkAgentTool { """) public ToolResultBlock getTableSchema( @ToolParam(name = "table_name", description = "The table name to query schema for") - String tableName) { + String tableName, + ToolCallContext ctx) { return toolExceptionMapper.run( () -> { Datasource datasource = - dataSourceService - .getActiveDatasource() - .orElseThrow( - () -> - BusinessException.of( - ErrorCode.NO_ACTIVE_DATASOURCE, - "No active datasource is available." - + " Unable to retrieve the" - + " table schema.")); + dataSourceService.getDatasourceForSession(ctx.sessionId()); List columns = columnSemanticService.getMergedTableSchema( datasource.getId(), tableName); diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/GetTablesTool.java b/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/GetTablesTool.java index 7c23094..ecf24ea 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/GetTablesTool.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/agent/tools/GetTablesTool.java @@ -21,9 +21,8 @@ import io.agentscope.core.tool.Tool; import io.agentscope.core.tool.ToolParam; import io.agentscope.core.util.JsonUtils; -import io.github.malonetalk.common.ErrorCode; +import io.github.malonetalk.agent.ToolCallContext; import io.github.malonetalk.entity.Datasource; -import io.github.malonetalk.exception.BusinessException; import io.github.malonetalk.exception.ToolExceptionMapper; import io.github.malonetalk.service.DatasourceService; import io.github.malonetalk.service.semantic.table.TableSemanticService; @@ -58,19 +57,12 @@ public ToolResultBlock getTables( returns all tables.\ """, required = false) - List domains) { + List domains, + ToolCallContext ctx) { return toolExceptionMapper.run( () -> { Datasource dataSource = - dataSourceService - .getActiveDatasource() - .orElseThrow( - () -> - BusinessException.of( - ErrorCode.NO_ACTIVE_DATASOURCE, - "No active datasource is available." - + " Unable to retrieve" - + " tables.")); + dataSourceService.getDatasourceForSession(ctx.sessionId()); return ToolResultBlock.text( JsonUtils.getJsonCodec() .toJson( diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/common/ErrorCode.java b/data-agent-backend/src/main/java/io/github/malonetalk/common/ErrorCode.java index c1034f6..ec5ec1e 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/common/ErrorCode.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/common/ErrorCode.java @@ -72,6 +72,12 @@ public enum ErrorCode { HttpStatus.CONFLICT, "The operation conflicts with the current data state."), + /** 会话绑定的数据源已被删除,会话无法继续使用。 */ + BOUND_DATASOURCE_UNAVAILABLE( + "BOUND_DATASOURCE_UNAVAILABLE", + HttpStatus.CONFLICT, + "The datasource bound to this session no longer exists. Please start a new session."), + /** HTTP 方法不支持。 */ METHOD_NOT_ALLOWED( "METHOD_NOT_ALLOWED", diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/controller/AgentController.java b/data-agent-backend/src/main/java/io/github/malonetalk/controller/AgentController.java index 4d8aa79..91762d4 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/controller/AgentController.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/controller/AgentController.java @@ -54,7 +54,11 @@ public Flux> chatStream( @Valid @RequestBody ChatRequest request) { log.info("SSE chat stream started: sessionId={}", request.sessionId()); return agentService - .chatStream(request.sessionId(), request.message(), request.toolResults()) + .chatStream( + request.sessionId(), + request.message(), + request.toolResults(), + request.datasourceId()) .map( event -> ServerSentEvent.builder() diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/dto/ChatRequest.java b/data-agent-backend/src/main/java/io/github/malonetalk/dto/ChatRequest.java index c65e439..63c190c 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/dto/ChatRequest.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/dto/ChatRequest.java @@ -23,7 +23,8 @@ public record ChatRequest( @NotBlank(message = "sessionId 不能为空") String sessionId, String message, - List toolResults) { + List toolResults, + Integer datasourceId) { public record ToolResultInput(String toolCallId, String toolName, String output) {} } diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/dto/SessionInfo.java b/data-agent-backend/src/main/java/io/github/malonetalk/dto/SessionInfo.java index 04aaa10..073e141 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/dto/SessionInfo.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/dto/SessionInfo.java @@ -17,4 +17,11 @@ */ package io.github.malonetalk.dto; -public record SessionInfo(String sessionId, String title, String createdAt, String lastActiveAt) {} +/** 会话摘要;datasourceId/datasourceName 为绑定的数据源信息,未绑定为 null。 */ +public record SessionInfo( + String sessionId, + String title, + String createdAt, + String lastActiveAt, + Integer datasourceId, + String datasourceName) {} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/entity/SessionDatasource.java b/data-agent-backend/src/main/java/io/github/malonetalk/entity/SessionDatasource.java new file mode 100644 index 0000000..d2be23b --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/entity/SessionDatasource.java @@ -0,0 +1,31 @@ +/* + * 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.entity; + +import java.time.LocalDateTime; +import lombok.Data; + +/** 会话与数据源绑定关系(session_id → datasource_id,一会话一源)。 */ +@Data +public class SessionDatasource { + + private String sessionId; + private Integer datasourceId; + private LocalDateTime createTime; + private LocalDateTime updateTime; +} 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 new file mode 100644 index 0000000..852d6f6 --- /dev/null +++ b/data-agent-backend/src/main/java/io/github/malonetalk/mapper/SessionDatasourceMapper.java @@ -0,0 +1,31 @@ +/* + * 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.mapper; + +import io.github.malonetalk.entity.SessionDatasource; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface SessionDatasourceMapper { + + /** 首次绑定;主键冲突时保留已有绑定(锁定语义)。 */ + int insertIfAbsent(SessionDatasource sessionDatasource); + + SessionDatasource selectBySessionId(@Param("sessionId") String sessionId); +} diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/DatasourceService.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/DatasourceService.java index ae95e9b..fe119cf 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/service/DatasourceService.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/DatasourceService.java @@ -37,6 +37,12 @@ public interface DatasourceService { Optional getActiveDatasource(); + /** 会话维度解析数据源:有绑定用绑定(无视 status),无绑定回退激活源。 */ + Datasource getDatasourceForSession(String sessionId); + + /** 绑定会话到数据源;数据源不存在抛 400,已绑定则保留首次绑定。 */ + void bindSessionDatasource(String sessionId, Integer datasourceId); + List findByType(String type); boolean updateStatus(Integer id, String status); diff --git a/data-agent-backend/src/main/java/io/github/malonetalk/service/DatasourceServiceImpl.java b/data-agent-backend/src/main/java/io/github/malonetalk/service/DatasourceServiceImpl.java index 5562b70..130bcdc 100644 --- a/data-agent-backend/src/main/java/io/github/malonetalk/service/DatasourceServiceImpl.java +++ b/data-agent-backend/src/main/java/io/github/malonetalk/service/DatasourceServiceImpl.java @@ -17,9 +17,13 @@ */ package io.github.malonetalk.service; +import io.github.malonetalk.common.ErrorCode; import io.github.malonetalk.entity.Datasource; +import io.github.malonetalk.entity.SessionDatasource; import io.github.malonetalk.enums.Status; +import io.github.malonetalk.exception.BusinessException; import io.github.malonetalk.mapper.DatasourceMapper; +import io.github.malonetalk.mapper.SessionDatasourceMapper; import java.time.LocalDateTime; import java.util.List; import java.util.Optional; @@ -33,6 +37,7 @@ public class DatasourceServiceImpl implements DatasourceService { private final DatasourceMapper dataSourceMapper; + private final SessionDatasourceMapper sessionDatasourceMapper; @Override public List findAll() { @@ -76,6 +81,36 @@ public Optional getActiveDatasource() { return active.isEmpty() ? Optional.empty() : Optional.of(active.get(0)); } + @Override + public Datasource getDatasourceForSession(String sessionId) { + SessionDatasource binding = sessionDatasourceMapper.selectBySessionId(sessionId); + if (binding != null) { + Datasource datasource = findExistingDatasource(binding.getDatasourceId()); + if (datasource == null) { + throw BusinessException.of(ErrorCode.BOUND_DATASOURCE_UNAVAILABLE); + } + return datasource; + } + return getActiveDatasource() + .orElseThrow(() -> BusinessException.of(ErrorCode.NO_ACTIVE_DATASOURCE)); + } + + @Override + public void bindSessionDatasource(String sessionId, Integer datasourceId) { + if (findExistingDatasource(datasourceId) == null) { + throw BusinessException.of( + ErrorCode.BAD_REQUEST, "Datasource does not exist: " + datasourceId); + } + SessionDatasource binding = new SessionDatasource(); + binding.setSessionId(sessionId); + binding.setDatasourceId(datasourceId); + sessionDatasourceMapper.insertIfAbsent(binding); + } + + private Datasource findExistingDatasource(Integer id) { + return dataSourceMapper.selectById(id); + } + @Override public List findByType(String type) { return dataSourceMapper.selectByType(type); diff --git a/data-agent-backend/src/main/resources/mapper/SessionDatasourceMapper.xml b/data-agent-backend/src/main/resources/mapper/SessionDatasourceMapper.xml new file mode 100644 index 0000000..e43f1ec --- /dev/null +++ b/data-agent-backend/src/main/resources/mapper/SessionDatasourceMapper.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + INSERT IGNORE INTO session_datasource (session_id, datasource_id) + VALUES (#{sessionId}, #{datasourceId}) + + + + + diff --git a/data-agent-frontend/src/api/agent.ts b/data-agent-frontend/src/api/agent.ts index 8366639..e67171b 100644 --- a/data-agent-frontend/src/api/agent.ts +++ b/data-agent-frontend/src/api/agent.ts @@ -60,12 +60,15 @@ export interface SessionInfo { title: string; createdAt: string; lastActiveAt: string; + datasourceId: number | null; + datasourceName: string | null; } export interface ChatRequest { sessionId: string; message?: string; toolResults?: ToolResultInput[]; + datasourceId?: number; } export interface TurnItem { diff --git a/data-agent-frontend/src/composables/useAgentChat.ts b/data-agent-frontend/src/composables/useAgentChat.ts index fe9c698..a0988f8 100644 --- a/data-agent-frontend/src/composables/useAgentChat.ts +++ b/data-agent-frontend/src/composables/useAgentChat.ts @@ -55,6 +55,8 @@ export function useAgentChat(initialSessionId?: string) { const messages = shallowRef([]); const isStreaming = ref(false); const sessionId = ref(initialSessionId || generateSessionId()); + // 待绑定/已绑定的数据源 id;新会话选源后设置,首条消息随请求落库。 + const datasourceId = ref(null); const abortController = shallowRef(null); const pendingQuestion = ref(null); const lastReportContent = ref(null); @@ -130,7 +132,11 @@ export function useAgentChat(initialSessionId?: string) { sessionId: sessionId.value, toolResults: [{ toolCallId: pq.toolCallId, toolName: pq.toolName, output: text }], } - : { sessionId: sessionId.value, message: text }; + : { + sessionId: sessionId.value, + message: text, + datasourceId: datasourceId.value ?? undefined, + }; try { for await (const event of streamChat(request, controller.signal)) { @@ -233,12 +239,14 @@ export function useAgentChat(initialSessionId?: string) { stopStreaming(); clearMessages(); sessionId.value = generateSessionId(); + datasourceId.value = null; } return { messages, isStreaming, sessionId, + datasourceId, pendingQuestion, lastReportContent, loadHistory, diff --git a/data-agent-frontend/src/views/chat/ChatView.vue b/data-agent-frontend/src/views/chat/ChatView.vue index 7975e2b..d11ab95 100644 --- a/data-agent-frontend/src/views/chat/ChatView.vue +++ b/data-agent-frontend/src/views/chat/ChatView.vue @@ -19,6 +19,8 @@ import { ref, nextTick, watch, onMounted, computed } from 'vue'; import { useRoute, useRouter } from 'vue-router'; import { useAgentChat } from '@/composables/useAgentChat'; + import { fetchSessionList } from '@/api/agent'; + import { getDatasourceList, type DatasourceResponse } from '@/api/datasource'; import ChatMessage from '@/views/chat/components/ChatMessage.vue'; import ChatInput from '@/views/chat/components/ChatInput.vue'; import SessionList from '@/views/chat/components/SessionList.vue'; @@ -32,6 +34,7 @@ messages, isStreaming, sessionId, + datasourceId, pendingQuestion, lastReportContent, sendMessage, @@ -47,6 +50,52 @@ const reportListKey = ref(0); const previewVisible = ref(false); const previewContent = ref(''); + const datasources = ref([]); + + // 已发过消息 = 绑定已落库 = 不可再切换数据源(锁定语义)。 + const isBound = computed(() => messages.value.length > 0); + + // 未绑定的旧会话实际跟随全局激活源,展示其名字以免用户误以为可自由选源。 + const activeDatasourceName = computed(() => { + if (datasourceId.value != null) return null; + return datasources.value.find(d => d.status === 'ACTIVE')?.name ?? null; + }); + + const dsPlaceholder = computed(() => { + if (isBound.value) { + return activeDatasourceName.value + ? `跟随全局激活源(${activeDatasourceName.value})` + : '跟随全局激活源'; + } + return '选择数据源'; + }); + + // 绑定源已被删除时下拉列表里没有对应项,补一个占位项。 + const boundUnknown = computed(() => { + if (datasourceId.value == null) return false; + return !datasources.value.some(d => d.id === datasourceId.value); + }); + + async function loadDatasources() { + try { + const res = await getDatasourceList(); + datasources.value = res.data.data ?? []; + } catch { + datasources.value = []; + } + } + + // 重进会话时从会话列表回显绑定源;无绑定(旧会话/新会话未落库)也要重置为 null, + // 避免残留上一个会话的绑定。 + async function resolveBoundDatasource(sid: string) { + try { + const list = await fetchSessionList(); + const found = list.find(s => s.sessionId === sid); + datasourceId.value = found?.datasourceId ?? null; + } catch { + // 会话列表加载失败不阻断会话展示。 + } + } function showReportPreview(content: string) { previewContent.value = content; @@ -78,9 +127,11 @@ watch(messages, () => scrollToBottom(), { deep: true }); onMounted(async () => { + loadDatasources(); const sid = route.params.sessionId as string | undefined; if (sid) { await loadHistory(sid); + await resolveBoundDatasource(sid); } }); @@ -89,6 +140,7 @@ async newSid => { if (newSid && typeof newSid === 'string') { await loadHistory(newSid); + await resolveBoundDatasource(newSid); } }, ); @@ -133,6 +185,22 @@
+ + + +
{{ s.title || s.sessionId }}
+
{{ s.datasourceName ?? '未绑定数据源' }}
{{ formatTime(s.lastActiveAt) }}
@@ -176,6 +177,15 @@ margin-top: 4px; } + .session-item__ds { + font-size: 11px; + color: var(--app-text-muted); + margin-top: 2px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .session-list__footer { padding: 12px 16px; border-top: 1px solid var(--app-border); diff --git a/sql/data_source.sql b/sql/data_source.sql index 5b93723..a69291a 100644 --- a/sql/data_source.sql +++ b/sql/data_source.sql @@ -105,3 +105,12 @@ CREATE TABLE IF NOT EXISTS `report` ( PRIMARY KEY (`id`), KEY `idx_session_id` (`session_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='报告表'; + +CREATE TABLE IF NOT EXISTS `session_datasource` ( + `session_id` VARCHAR(255) NOT NULL COMMENT '会话ID(主键,一会话一源)', + `datasource_id` INT NOT NULL COMMENT '绑定的数据源ID', + `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间(绑定一次性写入,实际不会更新,列可留可删)', + PRIMARY KEY (`session_id`), + KEY `idx_datasource_id` (`datasource_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='会话与数据源绑定表';