cc 3 tygodni temu
rodzic
commit
02fba6b05a
20 zmienionych plików z 135 dodań i 102 usunięć
  1. 10 10
      ai-server/src/main/java/com/zsjz/ai/common/cache/CaseDataCache.java
  2. 1 1
      ai-server/src/main/java/com/zsjz/ai/common/config/ReactorCaseContextConfig.java
  3. 10 10
      ai-server/src/main/java/com/zsjz/ai/common/context/CaseContextHolder.java
  4. 1 1
      ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseDataSourceReaper.java
  5. 7 7
      ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseDataSourceRegistry.java
  6. 1 1
      ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseRoutingDataSource.java
  7. 1 1
      ai-server/src/main/java/com/zsjz/ai/common/listener/CaseSessionListener.java
  8. 1 1
      ai-server/src/main/java/com/zsjz/ai/common/model/plat/entity/CaseInfo.java
  9. 16 16
      ai-server/src/main/java/com/zsjz/ai/common/utils/LuceneManager.java
  10. 4 4
      ai-server/src/main/java/com/zsjz/ai/common/utils/StateManager.java
  11. 4 4
      ai-server/src/main/java/com/zsjz/ai/module/agent/WorkspaceManagerFactory.java
  12. 32 9
      ai-server/src/main/java/com/zsjz/ai/module/agent/insight/agent/InsightAgentFactory.java
  13. 2 2
      ai-server/src/main/java/com/zsjz/ai/module/agent/python/PythonExecutor.java
  14. 30 20
      ai-server/src/main/java/com/zsjz/ai/module/agent/service/AgentService.java
  15. 2 2
      ai-server/src/main/java/com/zsjz/ai/module/agent/sql/SqlResultStore.java
  16. 4 4
      ai-server/src/main/java/com/zsjz/ai/module/dm/service/DmService.java
  17. 1 1
      ai-server/src/main/java/com/zsjz/ai/module/govern/controller/GovernController.java
  18. 3 3
      ai-server/src/main/java/com/zsjz/ai/module/govern/serivce/GovernService.java
  19. 1 1
      ai-server/src/main/java/com/zsjz/ai/module/plat/controller/CaseInfoController.java
  20. 4 4
      ai-server/src/main/java/com/zsjz/ai/module/plat/service/CaseInfoService.java

+ 10 - 10
ai-server/src/main/java/com/zsjz/ai/common/cache/CaseDataCache.java

@@ -42,7 +42,7 @@ public class CaseDataCache {
     public static final Map<String, String> GOVERN_CONFIG = Maps.newConcurrentMap();
 
     /** caseId → 案件级缓存 */
-    private static final Map<Integer, CaseBucket> BUCKETS = new ConcurrentHashMap<>();
+    private static final Map<Long, CaseBucket> BUCKETS = new ConcurrentHashMap<>();
 
     private CaseDataCache() {
     }
@@ -61,8 +61,8 @@ public class CaseDataCache {
     /**
      * 初始化指定案件的人员库缓存(开案时调用)
      */
-    public static void initCache(Integer caseId) {
-        Integer resolved = requireCaseId(caseId, "initCache");
+    public static void initCache(Long caseId) {
+        Long resolved = requireCaseId(caseId, "initCache");
         CaseBucket bucket = BUCKETS.computeIfAbsent(resolved, key -> new CaseBucket());
         synchronized (bucket) {
             bucket.personLibNoMap.clear();
@@ -72,7 +72,7 @@ public class CaseDataCache {
         initPersonLibNoCache(resolved, bucket);
     }
 
-    private static void initPersonLibNoCache(Integer caseId, CaseBucket bucket) {
+    private static void initPersonLibNoCache(Long    caseId, CaseBucket bucket) {
         PersonLibNoMapper mapper = SpringUtil.getBean(PersonLibNoMapper.class);
         // 显式绑定案件上下文:查的是案件库,@DS("slave") 需要知道是哪个案件
         List<PersonLibNo> personLibNos = CaseContextHolder.callWith(caseId,
@@ -184,7 +184,7 @@ public class CaseDataCache {
     }
 
     /** 清理指定案件的缓存(退出案件 / 换案时调用) */
-    public static void cleanCache(Integer caseId) {
+    public static void cleanCache(Long caseId) {
         if (caseId == null) {
             return;
         }
@@ -200,15 +200,15 @@ public class CaseDataCache {
     // 内部工具
     // ------------------------------------------------------------------
 
-    private static Integer resolveCaseId(Integer explicit) {
+    private static Long resolveCaseId(Long explicit) {
         if (explicit != null) {
             return explicit;
         }
         return CaseContextHolder.get();
     }
 
-    private static Integer requireCaseId(Integer explicit, String action) {
-        Integer caseId = resolveCaseId(explicit);
+    private static Long requireCaseId(Long explicit, String action) {
+        Long caseId = resolveCaseId(explicit);
         if (caseId == null) {
             throw new IllegalStateException(
                     "CaseDataCache." + action + " 缺少案件上下文:异步线程必须用 CaseContextHolder.runWith/callWith 显式绑定案件 ID");
@@ -218,13 +218,13 @@ public class CaseDataCache {
 
     /** 写操作用:取不到案件上下文直接抛异常 */
     private static CaseBucket requireBucket(String action) {
-        Integer caseId = requireCaseId(null, action);
+        Long caseId = requireCaseId(null, action);
         return BUCKETS.computeIfAbsent(caseId, key -> new CaseBucket());
     }
 
     /** 读操作用:取不到案件上下文时打日志并返回 null,由调用方降级 */
     private static CaseBucket bucketOrNull(String action) {
-        Integer caseId = resolveCaseId(null);
+        Long caseId = resolveCaseId(null);
         if (caseId == null) {
             log.error("CaseDataCache.{} 缺少案件上下文,本次读取降级返回空结果", action);
             return null;

+ 1 - 1
ai-server/src/main/java/com/zsjz/ai/common/config/ReactorCaseContextConfig.java

@@ -41,7 +41,7 @@ public class ReactorCaseContextConfig {
     @PostConstruct
     public void registerScheduleHook() {
         Schedulers.onScheduleHook(HOOK_KEY, runnable -> {
-            Integer caseId = CaseContextHolder.get();
+            Long caseId = CaseContextHolder.get();
             if (caseId == null) {
                 // 提交线程没有案件上下文(如系统级定时任务),保持原样
                 return runnable;

+ 10 - 10
ai-server/src/main/java/com/zsjz/ai/common/context/CaseContextHolder.java

@@ -27,13 +27,13 @@ public final class CaseContextHolder {
     /** sa-token Token-Session 中存放当前案件 ID 的键 */
     public static final String SESSION_KEY_CASE_ID = "caseId";
 
-    private static final ThreadLocal<Integer> CASE_ID = new ThreadLocal<>();
+    private static final ThreadLocal<Long> CASE_ID = new ThreadLocal<>();
 
     private CaseContextHolder() {
     }
 
     /** 绑定当前线程的案件 ID;传 null 等价于清理 */
-    public static void set(Integer caseId) {
+    public static void set(Long caseId) {
         if (caseId == null) {
             CASE_ID.remove();
         } else {
@@ -42,7 +42,7 @@ public final class CaseContextHolder {
     }
 
     /** 仅读 ThreadLocal(不做任何兜底),用于判断「本线程是否已显式绑定」 */
-    public static Integer get() {
+    public static Long get() {
         return CASE_ID.get();
     }
 
@@ -55,8 +55,8 @@ public final class CaseContextHolder {
      *
      * @return 案件 ID;无法解析(未登录 / 未开案 / 非请求线程)时返回 {@code null}
      */
-    public static Integer currentCaseId() {
-        Integer id = CASE_ID.get();
+    public static Long currentCaseId() {
+        Long id = CASE_ID.get();
         if (id != null) {
             return id;
         }
@@ -68,8 +68,8 @@ public final class CaseContextHolder {
      *
      * <p>用于异步/流式链路:Agent 的工具调用收口、Python 执行等场景。
      */
-    public static <T> T callWith(Integer caseId, Supplier<T> action) {
-        Integer previous = CASE_ID.get();
+    public static <T> T callWith(Long caseId, Supplier<T> action) {
+        Long previous = CASE_ID.get();
         try {
             set(caseId);
             return action.get();
@@ -79,7 +79,7 @@ public final class CaseContextHolder {
     }
 
     /** {@link #callWith(Integer, Supplier)} 的无返回值版本 */
-    public static void runWith(Integer caseId, Runnable action) {
+    public static void runWith(Long caseId, Runnable action) {
         callWith(caseId, () -> {
             action.run();
             return null;
@@ -93,7 +93,7 @@ public final class CaseContextHolder {
      * 这里整体吞掉返回 null —— 这类线程必须由调用方通过 {@link #callWith} 显式绑定,
      * 不要指望这里的兜底。
      */
-    private static Integer caseIdFromTokenSession() {
+    private static Long caseIdFromTokenSession() {
         try {
             if (!StpUtil.isLogin()) {
                 return null;
@@ -102,7 +102,7 @@ public final class CaseContextHolder {
             if (value == null) {
                 return null;
             }
-            return Integer.valueOf(String.valueOf(value));
+            return Long.valueOf(String.valueOf(value));
         } catch (Exception e) {
             // 非 Web 上下文 / 未登录 / 异步线程,统一按「无案件上下文」处理
             return null;

+ 1 - 1
ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseDataSourceReaper.java

@@ -38,7 +38,7 @@ public class CaseDataSourceReaper {
         if (caseDataSourceRegistry.openedCount() == 0) {
             return;
         }
-        for (Integer caseId : caseDataSourceRegistry.openedCaseIds()) {
+        for (Long caseId : caseDataSourceRegistry.openedCaseIds()) {
             Long ownerUserId = caseDataSourceRegistry.ownerOf(caseId);
             if (ownerUserId == null) {
                 // 无归属记录(理论上不会出现),交给开案方主动关闭,不在这里处理

+ 7 - 7
ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseDataSourceRegistry.java

@@ -42,7 +42,7 @@ public class CaseDataSourceRegistry {
     private final DuckdbBudgetProperties budget;
 
     /** caseId → 已打开的案件数据源 */
-    private final Map<Integer, OpenedCase> opened = new ConcurrentHashMap<>();
+    private final Map<Long, OpenedCase> opened = new ConcurrentHashMap<>();
 
     public CaseDataSourceRegistry(CaseRoutingDataSource routingDataSource, DuckdbBudgetProperties budget) {
         this.routingDataSource = routingDataSource;
@@ -50,7 +50,7 @@ public class CaseDataSourceRegistry {
     }
 
     /** 案件 ID → 物理数据源 key(如 12 → case12) */
-    public static String physicalKey(Integer caseId) {
+    public static String physicalKey(Long caseId) {
         return PHYSICAL_PREFIX + caseId;
     }
 
@@ -61,7 +61,7 @@ public class CaseDataSourceRegistry {
      * @param dbPath      DuckDB 库文件绝对路径
      * @param ownerUserId 归属用户 ID,可为 null(不校验归属)
      */
-    public synchronized void open(Integer caseId, String dbPath, Long ownerUserId) {
+    public synchronized void open(Long caseId, String dbPath, Long ownerUserId) {
         OpenedCase existing = opened.get(caseId);
         if (existing != null) {
             Long owner = existing.ownerUserId();
@@ -86,7 +86,7 @@ public class CaseDataSourceRegistry {
     }
 
     /** 关闭并移除案件数据源;未打开时静默返回 */
-    public synchronized void close(Integer caseId) {
+    public synchronized void close(Long caseId) {
         if (caseId == null) {
             return;
         }
@@ -108,18 +108,18 @@ public class CaseDataSourceRegistry {
                 caseId, System.currentTimeMillis() - removed.openedAt());
     }
 
-    public boolean isOpen(Integer caseId) {
+    public boolean isOpen(Long caseId) {
         return caseId != null && opened.containsKey(caseId);
     }
 
     /** 案件数据源的归属用户;未打开返回 null */
-    public Long ownerOf(Integer caseId) {
+    public Long ownerOf(Long caseId) {
         OpenedCase entry = caseId == null ? null : opened.get(caseId);
         return entry == null ? null : entry.ownerUserId();
     }
 
     /** 当前已打开的全部案件 ID(只读快照) */
-    public Set<Integer> openedCaseIds() {
+    public Set<Long> openedCaseIds() {
         return Collections.unmodifiableSet(opened.keySet());
     }
 

+ 1 - 1
ai-server/src/main/java/com/zsjz/ai/common/datasource/CaseRoutingDataSource.java

@@ -48,7 +48,7 @@ public class CaseRoutingDataSource extends DynamicRoutingDataSource {
             // master 及其它 key 走原逻辑
             return super.getDataSource(ds);
         }
-        Integer caseId = CaseContextHolder.currentCaseId();
+        Long caseId = CaseContextHolder.currentCaseId();
         if (caseId == null) {
             // 用 400 而非 ServerException.spe 的默认 500:这是「用户没开案」而非服务端故障
             throw new ServerException(400, "请先打开案件后再操作数据");

+ 1 - 1
ai-server/src/main/java/com/zsjz/ai/common/listener/CaseSessionListener.java

@@ -58,7 +58,7 @@ public class CaseSessionListener extends SaTokenListenerForSimple {
         } catch (NumberFormatException e) {
             return;
         }
-        for (Integer caseId : caseDataSourceRegistry.openedCaseIds()) {
+        for (Long caseId : caseDataSourceRegistry.openedCaseIds()) {
             if (userId.equals(caseDataSourceRegistry.ownerOf(caseId))) {
                 log.info("会话结束({}),释放案件数据源: userId={}, caseId={}", reason, userId, caseId);
                 caseDataSourceRegistry.close(caseId);

+ 1 - 1
ai-server/src/main/java/com/zsjz/ai/common/model/plat/entity/CaseInfo.java

@@ -26,7 +26,7 @@ public class CaseInfo implements Serializable {
      * 案件ID
      */
     @TableId(value = "id", type = IdType.AUTO)
-    private Integer id;
+    private Long id;
 
     /**
      * 案件名称

+ 16 - 16
ai-server/src/main/java/com/zsjz/ai/common/utils/LuceneManager.java

@@ -36,7 +36,7 @@ import java.util.concurrent.ConcurrentHashMap;
  *
  * <p><b>案件上下文从哪来</b>:{@link CaseContextHolder#get()}(只读 ThreadLocal)。
  * 清洗任务跑在虚拟线程 / 线程池上,调用方必须先用
- * {@link CaseContextHolder#runWith(Integer, Runnable)} 显式绑定案件 ID;
+ * {@link CaseContextHolder#runWith(Long, Runnable)} 显式绑定案件 ID;
  * 取不到时本类会抛异常明确失败,而不是静默写到错误的索引里。
  */
 public class LuceneManager {
@@ -48,10 +48,10 @@ public class LuceneManager {
     private static final int BATCH_SIZE = 1000;
 
     /** caseId → 索引资源 */
-    private static final Map<Integer, Holder> HOLDERS = new ConcurrentHashMap<>();
+    private static final Map<Long, Holder> HOLDERS = new ConcurrentHashMap<>();
 
     /** caseId → 攒批缓冲 */
-    private static final Map<Integer, List<SearchDTO>> BUFFERS = new ConcurrentHashMap<>();
+    private static final Map<Long, List<SearchDTO>> BUFFERS = new ConcurrentHashMap<>();
 
     private LuceneManager() {
     }
@@ -65,7 +65,7 @@ public class LuceneManager {
      * @param caseId    案件 ID
      * @param indexPath 该案件的索引目录(通常是 {@code workspace/{caseId}/lucene})
      */
-    public static void createWriter(Integer caseId, Path indexPath) {
+    public static void createWriter(Long caseId, Path indexPath) {
         if (caseId == null) {
             logger.error("创建 IndexWriter 失败:案件 ID 为空,索引路径: {}", indexPath);
             return;
@@ -128,7 +128,7 @@ public class LuceneManager {
         if (searcher == null) {
             return;
         }
-        Integer caseId = CaseContextHolder.get();
+        Long caseId = CaseContextHolder.get();
         Holder holder = caseId == null ? null : HOLDERS.get(caseId);
         if (holder != null) {
             holder.searcherManager().release(searcher);
@@ -140,7 +140,7 @@ public class LuceneManager {
      * 将多个小的 segment 合并为一个大的 segment,优化查询性能
      */
     public static void forceMerge() throws IOException {
-        Integer caseId = requireCaseId();
+        Long caseId = requireCaseId();
         Holder holder = holderOrNull(caseId);
         if (holder == null) {
             logger.info("IndexWriter 未初始化,无法执行强制合并, caseId={}", caseId);
@@ -163,7 +163,7 @@ public class LuceneManager {
     }
 
     /** 关闭指定案件的索引资源(退出案件时调用) */
-    public static void close(Integer caseId) {
+    public static void close(Long caseId) {
         if (caseId == null) {
             return;
         }
@@ -183,7 +183,7 @@ public class LuceneManager {
         logger.info("IndexWriter 及相关资源已全部关闭");
     }
 
-    private static void closeHolder(Holder holder, Integer caseId) {
+    private static void closeHolder(Holder holder, Long caseId) {
         try {
             if (holder.searcherManager() != null) {
                 holder.searcherManager().close();
@@ -200,7 +200,7 @@ public class LuceneManager {
     // --- 攒批写入 ---
 
     public static void add(SearchDTO dto) {
-        Integer caseId = requireCaseId();
+        Long caseId = requireCaseId();
         if (!HOLDERS.containsKey(caseId)) {
             return;
         }
@@ -219,11 +219,11 @@ public class LuceneManager {
     }
 
     public static void flush() {
-        Integer caseId = requireCaseId();
+        Long caseId = requireCaseId();
         flush(caseId);
     }
 
-    private static void flush(Integer caseId) {
+    private static void flush(Long caseId) {
         List<SearchDTO> buffer = BUFFERS.get(caseId);
         if (buffer == null) {
             return;
@@ -239,7 +239,7 @@ public class LuceneManager {
         flushBatch(caseId, batch);
     }
 
-    private static void flushBatch(Integer caseId, List<SearchDTO> batch) {
+    private static void flushBatch(Long caseId, List<SearchDTO> batch) {
         List<Document> docs = new ArrayList<>(batch.size());
         for (SearchDTO dto : batch) {
             docs.add(convertToDocument(dto));
@@ -285,7 +285,7 @@ public class LuceneManager {
         addDocuments(requireCaseId(), docs);
     }
 
-    private static void addDocuments(Integer caseId, Iterable<? extends Document> docs) throws IOException {
+    private static void addDocuments(Long caseId, Iterable<? extends Document> docs) throws IOException {
         Holder holder = holderOrNull(caseId);
         if (holder == null) {
             return;
@@ -326,7 +326,7 @@ public class LuceneManager {
 
     // --- 内部工具 ---
 
-    private static Holder holderOrNull(Integer caseId) {
+    private static Holder holderOrNull(Long caseId) {
         return caseId == null ? null : HOLDERS.get(caseId);
     }
 
@@ -337,8 +337,8 @@ public class LuceneManager {
      * {@code CaseContextHolder.runWith(caseId, ...)} 绑定,这里静默降级会把数据写进
      * 别人的索引,比失败更糟。
      */
-    private static Integer requireCaseId() {
-        Integer caseId = CaseContextHolder.get();
+    private static Long requireCaseId() {
+        Long caseId = CaseContextHolder.get();
         if (caseId == null) {
             throw new IllegalStateException(
                     "Lucene 操作缺少案件上下文:异步线程必须用 CaseContextHolder.runWith/callWith 显式绑定案件 ID");

+ 4 - 4
ai-server/src/main/java/com/zsjz/ai/common/utils/StateManager.java

@@ -43,8 +43,8 @@ public class StateManager {
      *
      * @return 案件 ID;未开案时返回哨兵 {@code 888888}
      */
-    public Integer getCaseId() {
-        Integer caseId = CaseContextHolder.currentCaseId();
+    public Long getCaseId() {
+        Long caseId = CaseContextHolder.currentCaseId();
         return caseId != null ? caseId : NO_CASE;
     }
 
@@ -70,7 +70,7 @@ public class StateManager {
      * (由 {@code CaseContextInterceptor} 解析)都能恢复出当前案件。
      */
     public void setCaseInfo(CaseInfo caseInfo) {
-        Integer caseId = caseInfo == null ? null : caseInfo.getId();
+        Long caseId = caseInfo == null ? null : caseInfo.getId();
         CaseContextHolder.set(caseId);
         bindCaseIdToTokenSession(caseId);
     }
@@ -96,7 +96,7 @@ public class StateManager {
     }
 
     /** 把案件 ID 写入当前 token 的会话;非请求线程上静默失败 */
-    private void bindCaseIdToTokenSession(Integer caseId) {
+    private void bindCaseIdToTokenSession(Long caseId) {
         try {
             if (!StpUtil.isLogin()) {
                 return;

+ 4 - 4
ai-server/src/main/java/com/zsjz/ai/module/agent/WorkspaceManagerFactory.java

@@ -33,14 +33,14 @@ public final class WorkspaceManagerFactory {
      * @param agentRowId  dataagent_agent.row_id
      * @return 隔离目录的绝对路径(调用方负责按需创建)
      */
-    public Path resolveAgentDataPath(String userId, Long workspaceId, Long agentRowId) {
-        validateSegment("userId", userId);
+    public Path resolveAgentDataPath(Path cwd ,Long userId, Long workspaceId, Long agentRowId) {
+        validateSegment("userId", String.valueOf(userId));
         validateSegment("workspaceId", workspaceId == null ? null : String.valueOf(workspaceId));
         validateSegment("agentRowId", agentRowId == null ? null : String.valueOf(agentRowId));
 
-        Path cwd = Paths.get(System.getProperty("user.dir")).toAbsolutePath().normalize();
+
         return cwd.resolve(".agentscope")
-                .resolve("users").resolve(userId.trim())
+                .resolve("users").resolve(userId.toString())
                 .resolve("workspaces").resolve(String.valueOf(workspaceId))
                 .resolve("agents").resolve(String.valueOf(agentRowId))
                 .normalize();

+ 32 - 9
ai-server/src/main/java/com/zsjz/ai/module/agent/insight/agent/InsightAgentFactory.java

@@ -1,7 +1,10 @@
 package com.zsjz.ai.module.agent.insight.agent;
 
+import cn.dev33.satoken.stp.StpUtil;
 import com.zsjz.ai.common.constants.PathConst;
+import com.zsjz.ai.common.context.CaseContextHolder;
 import com.zsjz.ai.common.exception.ServerException;
+import com.zsjz.ai.module.agent.WorkspaceManagerFactory;
 import com.zsjz.ai.module.agent.entity.AgentModel;
 import com.zsjz.ai.module.agent.mapper.AgentModelMapper;
 import com.zsjz.ai.module.agent.mapper.SqlQueryMapper;
@@ -27,6 +30,7 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Component;
 
 import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
@@ -51,7 +55,9 @@ import java.util.concurrent.locks.ReentrantLock;
 @Component
 public class InsightAgentFactory {
 
-    /** Agent 名称(前端/日志可见) */
+    /**
+     * Agent 名称(前端/日志可见)
+     */
     public static final String AGENT_NAME = "graph-relation-insight";
 
     /**
@@ -62,10 +68,14 @@ public class InsightAgentFactory {
      */
     private static final int MAX_ITERS = 6;
 
-    /** 实例池容量上限 */
+    /**
+     * 实例池容量上限
+     */
     private static final int POOL_MAX = 64;
 
-    /** 空闲回收时长(分钟) */
+    /**
+     * 空闲回收时长(分钟)
+     */
     private static final int IDLE_TIMEOUT_MINUTES = 30;
 
     private static final long IDLE_TIMEOUT_MS = IDLE_TIMEOUT_MINUTES * 60_000L;
@@ -88,10 +98,14 @@ public class InsightAgentFactory {
     private final PythonExecutor pythonExecutor;
     private final AgentToolRegistry agentToolRegistry;
 
-    /** 独立实例池:key = insight-s{sessionId} */
+    /**
+     * 独立实例池:key = insight-s{sessionId}
+     */
     private final Map<String, HarnessAgent> agentPool = new ConcurrentHashMap<>();
 
-    /** 最近访问时间(LRU / 空闲回收依据) */
+    /**
+     * 最近访问时间(LRU / 空闲回收依据)
+     */
     private final Map<String, Long> lastAccessAt = new ConcurrentHashMap<>();
 
     private final ReentrantLock poolLock = new ReentrantLock();
@@ -148,7 +162,9 @@ public class InsightAgentFactory {
         }
     }
 
-    /** 会话删除时清理实例(记忆由状态存储侧管理,这里只丢实例) */
+    /**
+     * 会话删除时清理实例(记忆由状态存储侧管理,这里只丢实例)
+     */
     public void evict(Long sessionId) {
         String key = KEY_PREFIX + sessionId;
         agentPool.remove(key);
@@ -197,14 +213,19 @@ public class InsightAgentFactory {
         b.enableMetaTool(true);
 
         // 工作空间与通用 Agent 保持一致:共用 .agentscope,但记忆槽位按 sessionId 隔离
-        Path workspace = PathConst.ROOT_PATH.resolve(".agentscope").resolve("insight");
+        Path cwd = PathConst.ROOT_PATH.resolve(".agentscope").resolve("insight");
+        WorkspaceManagerFactory workspaceManagerFactory = new WorkspaceManagerFactory();
+        Long userId = StpUtil.getLoginIdAsLong();
+        Path workspace = workspaceManagerFactory.resolveAgentDataPath(cwd, userId, CaseContextHolder.get(), 9527L);
         b.workspace(workspace);
         b.stateStore(agentStateStore);
         b.filesystem(new LocalFilesystemSpec().isolationScope(IsolationScope.USER));
         return b.build();
     }
 
-    /** 回收空闲超时的实例(持锁调用) */
+    /**
+     * 回收空闲超时的实例(持锁调用)
+     */
     private void evictIdleLocked(long now) {
         if (lastAccessAt.isEmpty()) {
             return;
@@ -219,7 +240,9 @@ public class InsightAgentFactory {
         }
     }
 
-    /** 容量超限时淘汰最久未访问的实例(持锁调用;protectKey 为刚插入的 key,不淘汰) */
+    /**
+     * 容量超限时淘汰最久未访问的实例(持锁调用;protectKey 为刚插入的 key,不淘汰)
+     */
     private void evictOverflowLocked(String protectKey) {
         while (agentPool.size() > POOL_MAX) {
             String oldestKey = null;

+ 2 - 2
ai-server/src/main/java/com/zsjz/ai/module/agent/python/PythonExecutor.java

@@ -96,7 +96,7 @@ public class PythonExecutor {
         // AgentToolRegistry 在工具调用收口处用 CaseContextHolder.runWith 显式绑定。
         // 这里不能用 StateManager.getCaseId() —— 它取不到时返回哨兵 888888,
         // 会导致 selectById(888888) 拿到 null 再 NPE,报错信息毫无指向性。
-        Integer workspaceId = CaseContextHolder.get();
+        Long workspaceId = CaseContextHolder.get();
         if (workspaceId == null) {
             return PythonResult.error("当前没有打开的案件,无法执行 Python 分析");
         }
@@ -199,7 +199,7 @@ public class PythonExecutor {
     /**
      * 捕获沙箱内图片:移动到 py-out/{execId}/ 并生成访问 URL,LRU 清理超额执行目录
      */
-    private List<String> collectImages(int workspaceId, String execId, Path sandbox) {
+    private List<String> collectImages(long workspaceId, String execId, Path sandbox) {
         List<String> urls = new ArrayList<>();
         try (Stream<Path> files = Files.list(sandbox)) {
             List<Path> images = files.filter(p -> p.getFileName().toString().toLowerCase().endsWith(".png"))

+ 30 - 20
ai-server/src/main/java/com/zsjz/ai/module/agent/service/AgentService.java

@@ -1,11 +1,13 @@
 package com.zsjz.ai.module.agent.service;
 
+import cn.dev33.satoken.stp.StpUtil;
 import com.baomidou.mybatisplus.spring.service.impl.ServiceImpl;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.core.type.TypeReference;
 import com.zsjz.ai.common.constants.PathConst;
 import com.zsjz.ai.common.exception.ServerException;
 import com.zsjz.ai.common.utils.Json;
+import com.zsjz.ai.module.agent.WorkspaceManagerFactory;
 import com.zsjz.ai.module.agent.config.EtlProperties;
 import com.zsjz.ai.module.agent.config.SkillRepositoryConfigEntry;
 import com.zsjz.ai.module.agent.config.SkillRepositorySupport;
@@ -38,6 +40,7 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 
 import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.List;
 import java.util.Optional;
 import java.util.concurrent.ConcurrentHashMap;
@@ -68,8 +71,11 @@ public class AgentService extends ServiceImpl<AgentMapper, AgentEntity> {
     private final PythonExecutor pythonExecutor;
     private final AgentChatSessionMapper chatSessionMapper;
     private final CaseInfoMapper caseInfoMapper;
-    /** 业务工具注册中心:把 module/{call,trans,track,otg,graph,person} 的只读 service 注册为工具并按域分组 */
+    /**
+     * 业务工具注册中心:把 module/{call,trans,track,otg,graph,person} 的只读 service 注册为工具并按域分组
+     */
     private final AgentToolRegistry agentToolRegistry;
+    private final WorkspaceManagerFactory workspaceManagerFactory;
 
     /**
      * agent 实例池:key = w{workspaceId}-a{agentRowId}-m{modelId}
@@ -77,21 +83,22 @@ public class AgentService extends ServiceImpl<AgentMapper, AgentEntity> {
     private final ConcurrentHashMap<String, HarnessAgent> agentPool = new ConcurrentHashMap<>();
 
     public AgentService(
-                        AgentModelFactory agentModelFactory,
-                        AgentModelMapper modelMapper,
-                        Optional<AgentStateStore> stateStoreOpt,
-                        RagSchemaService ragSchemaService,
-                        EtlProperties etlProperties,
-                        IntentService intentService,
-                        FollowupService followupService,
-                        SqlQueryMapper sqlQueryMapper,
-                        SqlResultStore sqlResultStore,
-                        TableInfoMapper tableInfoMapper,
-                        TableFieldMapper tableFieldMapper,
-                        PythonExecutor pythonExecutor,
-                        AgentChatSessionMapper agentChatSessionMapper,
-                        CaseInfoMapper caseInfoMapper,
-                        AgentToolRegistry agentToolRegistry) {
+            AgentModelFactory agentModelFactory,
+            AgentModelMapper modelMapper,
+            Optional<AgentStateStore> stateStoreOpt,
+            RagSchemaService ragSchemaService,
+            EtlProperties etlProperties,
+            IntentService intentService,
+            FollowupService followupService,
+            SqlQueryMapper sqlQueryMapper,
+            SqlResultStore sqlResultStore,
+            TableInfoMapper tableInfoMapper,
+            TableFieldMapper tableFieldMapper,
+            PythonExecutor pythonExecutor,
+            AgentChatSessionMapper agentChatSessionMapper,
+            CaseInfoMapper caseInfoMapper,
+            WorkspaceManagerFactory workspaceManagerFactory,
+            AgentToolRegistry agentToolRegistry) {
         this.agentModelFactory = agentModelFactory;
         this.modelMapper = modelMapper;
         this.agentStateStore = stateStoreOpt.orElseGet(InMemoryAgentStateStore::new);
@@ -106,6 +113,7 @@ public class AgentService extends ServiceImpl<AgentMapper, AgentEntity> {
         this.pythonExecutor = pythonExecutor;
         this.chatSessionMapper = agentChatSessionMapper;
         this.caseInfoMapper = caseInfoMapper;
+        this.workspaceManagerFactory = workspaceManagerFactory;
         this.agentToolRegistry = agentToolRegistry;
     }
 
@@ -134,14 +142,14 @@ public class AgentService extends ServiceImpl<AgentMapper, AgentEntity> {
             Model model = agentModelFactory.create(agentModelConfig);
             log.info("构建 agent 实例: workspaceId={}, agentRowId={}, modelId={}, model={}",
                     workspaceId, agentRowId, modelId, agentModelConfig.getName());
-            return buildAgent(entry, modelId, model);
+            return buildAgent(entry, modelId, workspaceId, model);
         });
     }
 
     /**
      * 构建 HarnessAgent(官方 builder)。
      */
-    private HarnessAgent buildAgent(AgentEntity entry, Long modelId, Model model) {
+    private HarnessAgent buildAgent(AgentEntity entry, Long modelId, Long workspaceId, Model model) {
         HarnessAgent.Builder b = HarnessAgent.builder();
 
         String name = entry.getName() != null ? entry.getName() : entry.getRowId().toString();
@@ -186,8 +194,10 @@ public class AgentService extends ServiceImpl<AgentMapper, AgentEntity> {
         // 下一步引导建议:onAgent 钩子包裹整个推理流,对话结束后生成建议并追加 suggestions 事件
         b.middleware(new FollowupMiddleware(followupService, etlProperties.getFollowup(), modelId));
 
-        //Path workspace = workspaceManagerFactory.resolveAgentDataPath(userId, workspaceId, entry.getRowId());
-        Path workspace = PathConst.ROOT_PATH.resolve(".agentscope");
+        Long userId = StpUtil.getLoginIdAsLong();
+        Path cwd = Paths.get(System.getProperty("user.dir")).toAbsolutePath().normalize();
+        Path workspace = workspaceManagerFactory.resolveAgentDataPath(cwd, userId, workspaceId, entry.getRowId());
+        //Path workspace = PathConst.ROOT_PATH.resolve(".agentscope");
         //scaffoldWorkspace(workspace, entry.getSysPrompt());
         b.workspace(workspace);
 

+ 2 - 2
ai-server/src/main/java/com/zsjz/ai/module/agent/sql/SqlResultStore.java

@@ -28,7 +28,7 @@ public class SqlResultStore {
 
     /** 缓存条目:原始 SQL + 列定义 + 全量行 + 归属案件 + 过期时间戳(按插入序淘汰最旧) */
     private record CachedResult(String sql, List<Map<String, Object>> columns,
-                                List<Map<String, Object>> rows, Integer caseId, long expireAt) {
+                                List<Map<String, Object>> rows, Long caseId, long expireAt) {
     }
 
     /** 按页取数结果 */
@@ -63,7 +63,7 @@ public class SqlResultStore {
             }
             return null;
         }
-        Integer currentCaseId = CaseContextHolder.get();
+        Long currentCaseId = CaseContextHolder.get();
         if (cached.caseId() != null && !cached.caseId().equals(currentCaseId)) {
             // 越权访问他人案件的结果集:按不存在处理
             return null;

+ 4 - 4
ai-server/src/main/java/com/zsjz/ai/module/dm/service/DmService.java

@@ -118,7 +118,7 @@ public class DmService {
 
         // 虚拟线程不会继承请求线程的 ThreadLocal,必须显式把案件上下文带下去,
         // 否则线程内 @DS("slave") 会因找不到案件而抛「请先打开案件后再操作数据」。
-        Integer caseId = CaseContextHolder.get();
+        Long caseId = CaseContextHolder.get();
         try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
             List<CompletableFuture<List<FileInfo>>> futures = targetFiles.stream()
                     .map(path -> CompletableFuture.supplyAsync(
@@ -597,7 +597,7 @@ public class DmService {
         sseService.sendSee(new SseDTO(101, slowDownProgress.calculateNextProgress(), "[数据清洗]开始数据清洗!", new SeeDataDTO(0, 0)));
         log.info("启动后台数据清洗任务,总数: {}, 并发度: {}", totalTaskCount, maxConcurrency);
         // 虚拟线程不继承请求线程的 ThreadLocal,先把案件上下文捕获下来带进去
-        Integer cleanCaseId = CaseContextHolder.get();
+        Long cleanCaseId = CaseContextHolder.get();
         Thread.startVirtualThread(() -> CaseContextHolder.runWith(cleanCaseId,
                 () -> executeCleanJobs(cleanJobs, totalTaskCount, maxConcurrency, completedTaskCount, callback)));
         //重新清洗清理旧数据
@@ -611,7 +611,7 @@ public class DmService {
                                   AtomicInteger completedTaskCount, TaskCompleteCallback callback) {
         Semaphore semaphore = new Semaphore(maxConcurrency, true);
         // 再往下一层虚拟线程池提交时同样要重新绑定(ThreadLocal 不会跨线程继承)
-        Integer caseId = CaseContextHolder.get();
+        Long caseId = CaseContextHolder.get();
         try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
             List<CompletableFuture<Void>> futures = cleanJobs.stream()
                     .map(job -> CompletableFuture.runAsync(
@@ -805,7 +805,7 @@ public class DmService {
         if (CollUtil.isEmpty(tableNames)) {
             return;
         }
-        Integer caseId = CaseContextHolder.get();
+        Long caseId = CaseContextHolder.get();
         try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
             List<CompletableFuture<Void>> futures = tableNames.stream()
                     .map(tableName -> CompletableFuture.runAsync(

+ 1 - 1
ai-server/src/main/java/com/zsjz/ai/module/govern/controller/GovernController.java

@@ -55,7 +55,7 @@ public class GovernController {
      */
     @GetMapping("/calcTask")
     public Result<Void> start(Integer taskProcess) {
-        Integer caseId = CaseContextHolder.currentCaseId();
+        Long caseId = CaseContextHolder.currentCaseId();
         if (caseId == null) {
             throw ServerException.spe("请先打开案件后再执行数据治理");
         }

+ 3 - 3
ai-server/src/main/java/com/zsjz/ai/module/govern/serivce/GovernService.java

@@ -88,7 +88,7 @@ public class GovernService {
      * 池线程上取不到登录态,必须显式指定,否则进度会推给错误的人。
      */
     private record GovernTaskContext(SseService sseService, SlowDownProgress slowDownProgress,
-                                     Integer caseId, Long userId) {
+                                     Long caseId, Long userId) {
         /**
          * 治理服务是单例,进度对象不能挂在实例字段上,否则多个治理任务并发时会互相覆盖。
          */
@@ -139,7 +139,7 @@ public class GovernService {
      *                    请求线程的案件上下文已丢失,必须由调用方(Controller)显式传入。
      * @param userId      进度推送目标用户。SSE 连接按用户维护,池线程上取不到登录态。
      */
-    public void calcTask(Integer taskProcess, Integer caseId, Long userId) {
+    public void calcTask(Integer taskProcess, Long caseId, Long userId) {
         log.info("【数据治理】开始执行数据治理任务, caseId={}, userId={}", caseId, userId);
         CaseContextHolder.set(caseId);
         try {
@@ -203,7 +203,7 @@ public class GovernService {
         // 任务跑在 GlobalPool.EXC_POOL 上,线程不会继承请求线程的 ThreadLocal,
         // 这里用 context.caseId() 显式绑定案件上下文(@DS("slave") 与 Lucene 索引都依赖它)。
         // 注意不要在 lambda 里调 CaseContextHolder.currentCaseId() —— 那时已在新线程上,取不到。
-        Integer caseId = context == null ? null : context.caseId();
+        Long caseId = context == null ? null : context.caseId();
         return CompletableFuture.runAsync(() -> CaseContextHolder.runWith(caseId, task), GlobalPool.EXC_POOL)
                 .orTimeout(timeout, TimeUnit.MINUTES)
                 .handle((ignored, e) -> {

+ 1 - 1
ai-server/src/main/java/com/zsjz/ai/module/plat/controller/CaseInfoController.java

@@ -61,7 +61,7 @@ public class CaseInfoController {
      * @param id 案件ID
      */
     @DeleteMapping("/delete")
-    public Result<Void> delete(Integer id) {
+    public Result<Void> delete(Long id) {
         caseInfoService.delete(id);
         return Result.succeed();
     }

+ 4 - 4
ai-server/src/main/java/com/zsjz/ai/module/plat/service/CaseInfoService.java

@@ -128,7 +128,7 @@ public class CaseInfoService extends ServiceImpl<CaseInfoMapper, CaseInfo> {
      *
      * <p>删文件前先关闭数据源,否则 Windows 上文件被占用会删不掉。
      */
-    public void delete(Integer id) {
+    public void delete(Long id) {
         Long ownerId = requireUserId();
         CaseInfo caseInfo = getById(id);
         if (caseInfo == null) {
@@ -186,7 +186,7 @@ public class CaseInfoService extends ServiceImpl<CaseInfoMapper, CaseInfo> {
      */
     public void exit() {
         Long ownerId = currentUserId();
-        Integer caseId = CaseContextHolder.currentCaseId();
+        Long caseId = CaseContextHolder.currentCaseId();
         if (caseId != null) {
             caseDataSourceRegistry.close(caseId);
             LuceneManager.close(caseId);
@@ -204,7 +204,7 @@ public class CaseInfoService extends ServiceImpl<CaseInfoMapper, CaseInfo> {
      * @return 未开案时返回 null
      */
     public CaseInfo current() {
-        Integer caseId = CaseContextHolder.currentCaseId();
+        Long caseId = CaseContextHolder.currentCaseId();
         if (caseId == null) {
             return null;
         }
@@ -217,7 +217,7 @@ public class CaseInfoService extends ServiceImpl<CaseInfoMapper, CaseInfo> {
 
     /** 关闭该用户此前打开的全部案件(保证「一人一案」) */
     private void closeOwnedCases(Long ownerId) {
-        for (Integer openedCaseId : caseDataSourceRegistry.openedCaseIds()) {
+        for (Long openedCaseId : caseDataSourceRegistry.openedCaseIds()) {
             if (ownerId.equals(caseDataSourceRegistry.ownerOf(openedCaseId))) {
                 caseDataSourceRegistry.close(openedCaseId);
                 LuceneManager.close(openedCaseId);